[Перевод] Портируем make.c на D
Уолтер Брайт — «великодушный пожизненный диктатор» языка программирования D и основатель Digital Mars. За его плечами не один десяток лет опыта в разработке компиляторов и интерпретаторов для нескольких языков, в числе которых Zortech C++ — первый нативный компилятор C++. Он также создатель игры Empire, послужившей основным источником вдохновения для Sid Meier«s Civilization.
Better C — это способ перенести существующие проекты на языке C на язык D в последовательной манере. В этой статье показан пошаговый процесс конвертации нетривиального проекта из C в D и показывает частые проблемы, которые при этом возникают.
Хотя фронтенд компилятора D dmd уже сконвертирован в D, это настолько большой проект, что его трудно целиком охватить. Мне был нужен более мелкий и скромный проект, который можно было бы полностью уяснить, но чтобы он не был умозрительным примером.
Мне пришла на ум старая make-программа, которую я написал для компилятора C Datalight в начале 1980-х. Это реальная имплементация классической программы make, которая постоянно использовалась с начала 80-х. Она написана на C ещё до его стандартизации, была портирована из одной системы в другую и укладывается всего в 1961 строчку кода, включая комментарии. Она до сих пор регулярно используется.
Вот документация и исходный код. Размер исполняемого файла make.exe — 49 692 байта, и последнее изменение было 19 августа 2012 г.
Наш Злобный план:
- Свести к минимуму diff«ы между C- и D-версиями. Таким образом, если поведение программ будет различаться, проще будет найти источник различия.
- Во время переноса не будет предпринято попыток исправить или улучшить код на C. Это делается во исполнение пункта № 1.
- Не будет предпринято попыток рефакторинга кода. Опять же, см. пункт № 1.
- Воспроизвести поведение программы на C насколько это возможно, со всеми багами.
- Сделать всё, что необходимо ради исполнения пункта № 4.
Только когда мы закончим, можно будет приступить к исправлениям, рефакторингу, подчистке и т.д.
Законченный перенос с C на D. Размер исполняемого файла — 52 252 байт (сравнимо с оригиналом — 49 692 байт). Я не анализировал увеличение в размере, но вероятно, оно взялось из-за экземпляров шаблона NEWOBJ (в C-версии это макрос) и изменений в рантайме DMC после 2012 года.
Вот отличия между версиями. Изменились 664 строки из 1961, примерно треть — кажется, что много, но надеюсь, я смогу вас убедить, что почти все отличия тривиальны.
Включение файлов через #include
заменено на импортирование соответствующих модулей D: например, #include
заменено на import core.stdc.stdio;
. К сожалению, некоторые из включаемых файлов специфичны для Digital Mars C, и для них не существует версий на D (это надо исправить). Чтобы не останавливаться на этом, я просто вставил соответствующие объявления с 29-й строки по 64-ю. (См. документацию по объявлению import
).
Конструкция #if _WIN32
заменена на version (Windows)
. (См. документацию по условной компиляции версий и список предопределённых версий).
Объявление extern(C):
помечает последующие объявления в файле как совместимые с C. (См. документацию по аттрибуту линковки).
При помощи глобального поиска и замены макросы debug1, debug2 и debug3 заменены на debug prinf
. В целом, директивы препроцессора #ifdef DEBUG
заменены на условную компиляцию при помощи debug
. (См. документацию по выражению debug
).
/* Delete these old C macro definitions...
#ifdef DEBUG
-#define debug1(a) printf(a)
-#define debug2(a,b) printf(a,b)
-#define debug3(a,b,c) printf(a,b,c)
-#else
-#define debug1(a)
-#define debug2(a,b)
-#define debug3(a,b,c)
-#endif
*/
// And replace their usage with the debug statement
// debug2("Returning x%lx\n",datetime);
debug printf("Returning x%lx\n",datetime);
Макросы TRUE, FALSE и NULL при помощи поиска и замены заменены на true
, false
и null
.
Макрос ESC заменён константой времени компиляции. (См. документацию по константам
).
// #define ESC '!'
enum ESC = '!';
Макрос NEWOBJ заменён шаблонной функцией.
// #define NEWOBJ(type) ((type *) mem_calloc(sizeof(type)))
type* NEWOBJ(type)() { return cast(type*) mem_calloc(type.sizeof); }
Макрос filenamecmp
заменён функцией.
Убрана поддержка устаревших платформ.
Глобальные переменные в D по умолчанию помещаются в локальное хранилище потока (thread-local storage, TLS). Но поскольку make
— однопоточная программа, их можно поместить в глобальное хранилище при помощи класса хранилища __gshared
. (См. документацию по атрибуту __gshared
).
// int CMDLINELEN;
__gshared int CMDLINELEN
В D нет отдельного пространства имён для структур, так что в typedef
нет необходимости. Вместо этого можно использовать alias
. (См. документацию по объявлению alias
). Кроме того, из объявлений переменных убрано слово struct
.
/*
typedef struct FILENODE
{ char *name,genext[EXTMAX+1];
char dblcln;
char expanding;
time_t time;
filelist *dep;
struct RULE *frule;
struct FILENODE *next;
} filenode;
*/
struct FILENODE
{
char *name;
char[EXTMAX1] genext;
char dblcln;
char expanding;
time_t time;
filelist *dep;
RULE *frule;
FILENODE *next;
}
alias filenode = FILENODE;
В языке D macro
— это ключевое слово, поэтому вместо этого будем использовать MACRO
.
В отличие от C, в языке D звёздочка в объявлении указателя является частью типа, поэтому при объявлении нескольких указателей звёздочка применяется к каждому символу:
// char *name,*text;
// In D, the * is part of the type and
// applies to each symbol in the declaration.
char* name, text;
Объявления массивов в стиле C преобразованы в объявления в стиле D. (См. документацию по синтаксису объявлений в D).
Слово static
на уровне модуля в D ничего не значит. В C статические глобальные переменные эквивалентны приватным переменным уровня модуля в D, но это неважно, если модуль никогда не импортируется. Их всё ещё нужно обозначить как __gshared
, и этот можно сделать целым блоком. (См. документацию по атрибуту static
).
/*
static ignore_errors = FALSE;
static execute = TRUE;
static gag = FALSE;
static touchem = FALSE;
static debug = FALSE;
static list_lines = FALSE;
static usebuiltin = TRUE;
static print = FALSE;
...
*/
__gshared
{
bool ignore_errors = false;
bool execute = true;
bool gag = false;
bool touchem = false;
bool xdebug = false;
bool list_lines = false;
bool usebuiltin = true;
bool print = false;
...
}
Предварительные объявления функций в языке D не нужны. Функцию, определённую на уровне модуля, можно вызывать из любого места в этом модуле, даже до её определения.
В расширении символов подстановки в make-программе нет большого смысла.
Параметры функций, определённые с синтаксисом массивов, на самом деле являются указателями, и в D объявляются как указатели.
// int cdecl main(int argc,char *argv[])
int main(int argc,char** argv)
Макрос mem_init()
ни во что не расширяется, и до этого мы его убрали.
В C можно грязно играть с аргументами, но D требует, чтобы они соответствовали прототипу функции.
void cmderr(const char* format, const char* arg) {...}
// cmderr("can't expand response file\n");
cmderr("can't expand response file\n", null);
При помощи глобального поиска и замены оператор-стрелка (->
) языка C заменён на точку (.
), поскольку в D доступ к членам осуществляется одинаково.
Директивы условной компиляции заменены на version
.
/*
#if TERMCODE
...
#endif
*/
version (TERMCODE)
{
...
}
Отсутствие прототипов функций свидетельствует о древности этого кода. D требует полноценных прототипов.
// doswitch(p)
// char *p;
void doswitch(char* p)
В языке D слово debug
зарезервировано. Переименуем в xdebug
.
Многострочные литералы в C требуют \n\
в конце каждой строки. В D этого не требуется.
Неиспользуемый код закомментирован при помощи вложенного блока комментариев /+ +/
. (См. документацию по строчным, блочным и вложенным комментариям).
Выражение static if
может во многих случаях заменить #if
. (См. документацию по static if
).
Массивы в D не сводятся к указателю автоматически, следует использовать .ptr
.
// utime(name,timep);
utime(name,timep.ptr);
Использование const
для строк в стиле C проистекает из строковых литералов в D, поскольку D не позволяет брать изменяемые указатели на строковые литералы. (См. документацию по const
и immutable
).
// linelist **readmakefile(char *makefile,linelist **rl)
linelist **readmakefile(const char *makefile,linelist **rl)
Преобразование void*
в char*
в D должно быть явным.
// buf = mem_realloc(buf,bufmax);
buf = cast(char*)mem_realloc(buf,bufmax);
Тип unsigned
заменён на uint
.
Атрибут inout
можно использовать, чтобы передать «константность» аргумента функции на возвращаемый тип. Если параметр обозначен как const
, то таким же будет возвращаемое значение, и наоборот. (См. документацию по inout
-функциям).
// char *skipspace(p) {...}
inout(char) *skipspace(inout(char)* p) {...}
Макрос arraysize
можно заменить на свойство .length
. (См. документацию по свойствам массивов).
// useCOMMAND |= inarray(p,builtin,arraysize(builtin));
useCOMMAND |= inarray(p,builtin.ptr,builtin.length)
Строковые литералы неизменяемы (immutable
), поэтому изменяемые строки необходимо заменить на массивы, выделенные на стеке. (См. документацию по строковым литералам).
// static char envname[] = "@_CMDLINE";
char[10] envname = "@_CMDLINE";
Свойство .sizeof
служит заменой оператору sizeof()
из C. (См. документацию по .sizeof
).
// q = (char *) mem_calloc(sizeof(envname) + len);
q = cast(char *) mem_calloc(envname.sizeof + len)
Старые версии Windows нас не интересуют.
Доисторическое применение char *
заменено на void*
.
И вот и все изменения! Как видите, не так уж плохо. Я не выставлял таймер, но сомневаюсь, что всё это заняло у меня больше часа — включая исправление нескольких ошибок, которые я сделал в процессе.
У нас остаётся только файл man.c, который был нужен, чтобы открывать в браузере документацию по make при запуске с опцией -man
. К счастью, он уже портирован на D, так что я могу просто скопировать код.
Собрать make так просто, что для этого даже не требуется make-файл:
\dmd2.079\windows\bin\dmd make.d dman.d -O -release -betterC -I. -I\dmd2.079\src\druntime\import\ shell32.lib
Резюме
Мы придерживались нашего Злобного плана по портированию нетривильной программы на олдскульном C на язык D, и смогли сделать это быстро и корректно. Мы получили эквивалентный исполняемый файл.
Проблемы, с которыми мы столкнулись, типичны и легко решаются следующими способами:
- замена
#include
наimport
; - замена отсутствующих D-версий включаемых файлов;
- глобальный поиск и замена вещей вроде
->
; - замена макросов препроцессора на:
- константы времени компиляции,
- простые шаблоны,
- функции,
- спецификаторы версий,
- спецификаторы отладки;
- замена зарезервированных слов;
- изменение объявлений массивов и указателей;
- удаление ненужных прототипов функций;
- более строгое соблюдение типов;
- использование свойств массивов;
- замена типов C типами D.
Не потребовалось ничего из следующего:
- реорганизация кода,
- изменения в структурах данных,
- изменение хода выполнения программы,
- изменения в работе программы,
- изменение управления памятью.
Будущее
Теперь, когда у нас есть Better C, нам доступны многие современные возможности, которые позволят нам улучшить наш код:
- модули!
- безопасное обращение к памяти (включая проверку переполнения буфера),
- метапрограммирование,
- RAII,
- Юникод,
- вложенные функции,
- методы,
- перегрузка операторов,
- генератор документации,
- функциональное программирование,
- выполнение функций во время компиляции (CTFE),
- и многое другое.
К действию
Если вы знаете английский, заходите на форум D и расскажите нам, как продвигается ваш проект на Better C!