[Перевод] Портируем make.c на D

?v=1

Уолтер Брайт — «великодушный пожизненный диктатор» языка программирования 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 г.

Наш Злобный план:


  1. Свести к минимуму diff«ы между C- и D-версиями. Таким образом, если поведение программ будет различаться, проще будет найти источник различия.
  2. Во время переноса не будет предпринято попыток исправить или улучшить код на C. Это делается во исполнение пункта № 1.
  3. Не будет предпринято попыток рефакторинга кода. Опять же, см. пункт № 1.
  4. Воспроизвести поведение программы на C насколько это возможно, со всеми багами.
  5. Сделать всё, что необходимо ради исполнения пункта № 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!

© Habrahabr.ru