Уолтер Брайт великодушный пожизненный диктатор языка программирования D и основатель Digital Mars. За его плечами не один десяток лет опыта в разработке компиляторов и интерпретаторов для нескольких языков, в числе которых Zortech C++ первый нативный компилятор C++. Он также создатель игры Empire, послужившей основным источником вдохновения для Sid Meiers Civilization.
- D как улучшенный C
- Баги, которые разрушили ваш замок
- Портируем make.c на D
Better C это способ перенести существующие проекты на языке C на язык D в последовательной манере. В этой статье показан пошаговый процесс конвертации нетривиального проекта из C в D и показывает частые проблемы, которые при этом возникают.
Хотя фронтенд компилятора D dmd уже сконвертирован в D, это настолько большой проект, что его трудно целиком охватить. Мне был нужен более мелкий и скромный проект, который можно было бы полностью уяснить, но чтобы он не был умозрительным примером.
Мне пришла на ум старая make-программа, которую я написал для компилятора C Datalight в начале 1980-х. Это реальная имплементация классической программы make, которая постоянно использовалась с начала 80-х. Она написана на C ещё до его стандартизации, была портирована из одной системы в другую и укладывается всего в 1961 строчку кода, включая комментарии. Она до сих пор регулярно используется.
Вот документация и исходный код. Размер исполняемого файла make.exe 49692 байта, и последнее изменение было 19августа2012 г.
Наш Злобный план:
- Свести к минимуму diffы между C- и D-версиями. Таким образом, если поведение программ будет различаться, проще будет найти источник различия.
- Во время переноса не будет предпринято попыток исправить или улучшить код на C. Это делается во исполнение пункта 1.
- Не будет предпринято попыток рефакторинга кода. Опять же, см. пункт 1.
- Воспроизвести поведение программы на C насколько это возможно, со всеми багами.
- Сделать всё, что необходимо ради исполнения пункта 4.
Только когда мы закончим, можно будет приступить к исправления, рефакторингу, подчистке и т.д.
Спойлеры!
Законченный перенос с C на D. Размер исполняемого файла 52252 байт (сравнимо с оригиналом 49692 байт). Я не анализировал увеличение в размере, но вероятно, оно взялось из-за экземпляров шаблона NEWOBJ (в C-версии это макрос) и изменений в рантайме DMC после 2012 года.
Шаг за шагом
Вот отличия между версиями. Изменились 664 строки из 1961, примерно треть кажется, что много, но надеюсь, я смогу вас убедить, что почти все отличия тривиальны.
Включение файлов через #include
заменено на
импортирование соответствующих модулей D: например, #include
<stdio.h>
заменено на 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);
Атрибут 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!