Две с половиной недели назад мы закончили один из наших проектов — портирование программы с C в BetterC, и программой этой оказалась совсем крошечная утилита redo. Об этой утилите мы уже писали и даже сделали ее версию на чистом D, казалось бы с какой стати мы взялись за порт точно такой же утилиты?
На вопрос ответить довольно легко — утилита на чистом C нас заинтриговала в буквальном смысле. Смотрите сами: компактный размер — утилита занимает около 18 — 30 Кб (в зависимости от платформы и варианта сборки), умеет делать параллельные сборки, содержит в себе полную реализацию хэширования SHA-256 и использует его, а также время изменения файлов для мониторинга свежести целей, а также сама утилита умеет без каких-то сторонних библиотек запускать скрипты на любых языках. При этом программа не использует сторонних библиотек и написана на чистом C99, кроме стандартной библиотеки и некоторых функций из поставки POSIX здесь не используется вообще. Наша версия на D гораздо скромнее в возможностях и имеет размер в 270 — 1900 Кб (в зависимости от компилятора и варианта сборки), что несколько убивает шарм этой утилиты.
Кроме того, с момента нашего знакомства с redo, мы пробовали разные варианты ее переноса или портирования, и у нас уже был неудачный опыт портирования того варианта с C99, о котором говорилось выше — порт мы завершили и он компилируется, но вот работоспособность его под весьма большим сомнением. Все желающие могут ознакомится с нашей первой попыткой переноса кодовой базы с C в BetterC здесь.
Для начала давайте познакомимся с BetterC, поскольку немногие из нас сталкивались с ним.
BetterC — это такое подмножество языка программирования D, которое не использует библиотеку исполнения D и его сборщик мусора, и нацелено оно на наибольшую совместимость с библиотеками C. Данный вариант D действительно во многом напоминает классический C, использует библиотеку времени исполнения C и поэтому следующие возможности D не будут доступны:
- сборщик мусора,
- TypeInfo и ModuleInfo,
- классы,
- встроенное управление потоками (например, core.thread)
- динамические (но срезы статических массивов по прежнему доступны) и ассоциативные массивы,
- исключения,
- synchronized и core.sync,
- статические конструкторы и деструкторы модулей
В общем, получается, что большая высокоуровневая часть D недоступна в таком режиме, однако, несмотря на это осталось многое из удобного и нужного:
- неограниченное использование compile-time инструментария,
- все возможности метапрограммирования,
- вложенные функции, вложенные структуры, делегаты и лямбда-функции,
- полная система модулей,
- срезы массивов и проверка их границ,
- RAII,
- scope(exit),
- модификаторы защиты памяти,
- взаимодействие с C++,
- COM- и C++-классы,
- сбои assert направляются прямо в библиотеку времени исполнения C,
- switch со строками,
- final switch,
- unittest,
- проверка формата в printf
и как говорится, многое другое.
Исходный код для такого «диалекта» D также хранятся в файлах с расширением *.d и компиляция в режиме BetterC осуществляется через передачу компилятору специального флага -betterC.
На этом краткий обзор закончен и мы далее покажем вам ряд моментов с которыми мы столкнулись при портировании утилиты redo, а также поделимся некоторыми собственными заметками о BetterC в принципе.
Всякая программа в BetterC, как и в D, начинает свою работу с функции main(). Вот только, сама сигнатура функции main несколько отличается от стандартной и обычно выглядит так:
extern (C) int main(int argc, char** argv) { /// .... return 0; }
Как видите, все тоже самое, но структура аргументов иная, поскольку не используется среда времени исполнения D и по этой причине, если программа принимает что-то в качестве аргументов командной строки, то разбор всего попавшего внутрь main придется делать вручную.
Заметили блок extern(C) в main ? Данный блок говорит о том, что для вызова функции используется соглашение о вызове в стиле C, и используется данная декларация отнюдь не только в main…
При переходе в BetterC можно практически сразу забыть о существовании Phobos, стандартной библиотеки D, поскольку большая часть ее полагается на встроенную сборку мусора и некоторые другие возможности, которые теперь недоступны. Это прежде всего означает то, что придется использовать стандартную библиотеку C, которая уже включена в D.
Вот к примеру, использование стандартной printf из <stdio.h>:
import core.stdc.stdio : printf; extern(C) void main() { printf("Hello betterC\n"); }
Видите, импорт идет из уже приспособленных для D заголовков, однако, есть и иной вариант для которого как раз и требуется декларация extern(C) — прямой импорт нужного из стандартной библиотеки C! Достигается это через задание прототипов функций в стиле C с некоторыми доработками сигнатур прототипов, вставка scope и const, а также модификаторов pure и nothrow, @nogc.
Выглядит это вот так:
extern(C) { void* memcpy(scope return void* s1, scope const(void*) s2, ulong n) pure nothrow @nogc; }
делается это так — открываем документацию на стандартную библиотеку C, копируем прототип нужной функции, адаптируем декларацию к D и вставляем модификаторы — и все. Функцию можно использовать и за пределами extern(C) блока.
Несмотря на то, что BetterC очень похож на чистый C, все равно будет требоваться адаптация — приведение типов к актуальным, учет некоторых особенностей дизайна стандартной библиотеки C, строгость компилятора D. Помимо этого можно столкнуться с тем, о чем раньше не приходилось думать — например, с ручным управлением памятью, о чем мы чуть попозже немного упомянем.
Все это надо учитывать, и прежде всего типы данных. Стандартные типы D почти полностью совпадают с таким же в C, однако, есть и различия в указании типов. Больше это относится к сложным типам, к массивам и типам для указателей, поскольку здесь декларация переменных с этими типам в D переделана и BetterC использует именно переделанный вариант. В C размерность массива (и скобки в которых она прописывается) находится после имени переменной, а в D/BetterC она является частью типа и находится после имени базового типа:
// массив в C-стиле char test[42]; // массив в D-стиле char[42] test;
То же правило работает и для указателей, а также для иных составных типов данных.
С указателями связана отдельная история… В D, как и в C, они тоже есть, и как следствие, почти у каждого типа есть свойство .ptr, которое позволяет получить указатель на объект данного типа. Однако, при работе с кодом на C это свойство приобретает особое значение и довольно часто его можно увидеть вот в такой идиоме:
cast(<базовый тип>*) obj.ptr;
Нужно это для корректного «узнавания» типа указателя компилятором, а иногда для правильного приведения полученного указателя к нужному типу. Оба варианта вполне себе используются, как порой и вот такая странная конструкция:
cast(char*) 0;
Вы, наверное, не поверите, но иногда замена этого оригинального конструктива стандартным null из D просто ломает всю программу!
Также здесь хочется привести один занимательный пример для иллюстрации того, что хоть работа с указателями в BetterC (и даже в D) допускает особые вольности, но тем не менее стандартные подходы и слишком свободная трактовка указателей и чисел как указателей в D не пройдет.
Вот пример функции из оригинальной утилиты redo на C99:
static char * check_dofile(const char *fmt, ...) { static char dofile[PATH_MAX]; va_list ap; va_start(ap, fmt); vsnprintf(dofile, sizeof dofile, fmt, ap); va_end(ap); if (access(dofile, F_OK) == 0) { return dofile; } else { redo_ifcreate(dep_fd, dofile); return 0; } }
А вот ее вариант в BetterC:
extern(C) static char* check_dofile(const char *fmt, ...) { static char[PATH_MAX] dofile; va_list ap; va_start(ap, fmt); vsnprintf(cast(char*) dofile.ptr, dofile.sizeof, fmt, ap); va_end(ap); if (access(cast(char*) dofile.ptr, F_OK) == 0) { return cast(char*) dofile.ptr; } else { redo_ifcreate(dep_fd, cast(char*) dofile.ptr); return cast(char*) 0; } }
Как видите приходиться явно указать тип возвращаемого указателя внутри блока else, а также использовать явное приведение типов для всех вызовов свойства .ptr. В изначальном варианте этой функции был вариант как и в коде на C, но компилятор D просто откажется такое компилировать!
Кстати, в этой функции еще есть и работа с переменным количеством аргументов в функции с помощью традиционных для C va_list и прочих va_* функций/макросов. В самой утилите как и в порте достаточно часто встречаются функции с переменным числом аргументов, но такая обработка как тут только одна — и используется она лишь для перехвата формата и захвата данных для печати в файл.
Но самые занимательные вещи у нас происходили с импортами и использованием стандартной библиотеки C!
Помните, мы упоминали прямой импорт? Прямой импорт возможен не только для функций, но и для переменных, типов и иногда и важных констант, используемых стандартной библиотекой — и тут, как всегда это бывает, если архитектура сильно отличается от классических x86/x86_64, то потребуются усилия…
Дело в том, что обычный способ импорта модулей core.stdc.* или похожего core.sys.* для некоторых архитектур, а иногда и для x86/x86_64, не срабатывает по очень простой причине — сконвертированные заголовки из библиотеки C не полностью включены в стандартную библиотеку D. Многие элементы в таком случае могут отсутствовать поскольку не всегда требуются и потому что некому это сопровождать. Также, иногда бывает и так, что о нехватке чего-либо узнаешь не при компиляции, а при выполнении или попытке линковки программы…
Именно, поэтому порой приходится делать так:
module archbc; version (AArch64) { // from <fcntl.h> enum F_SETFL = 0x00000004; enum O_APPEND = 0x00000400; enum O_CLOEXEC = 0x00080000; enum O_CREAT = 0x00000040; enum O_DIRECTORY = 0x00004000; enum O_NONBLOCK = 0x00000800; enum O_TRUNC = 0x00000200; enum O_WRONLY = 0x00000001; enum O_RDONLY = 0x00000000; // from <unistd.h> enum F_LOCK = 0x00000001; enum F_OK = 0x00000000; enum F_TLOCK = 0x00000002; enum X_OK = 0x00000001; // from <bits/waitstatus.h> int WEXITSTATUS(int status) { return ((status) & 0xff00) >> 8; } int WIFEXITED(int status) { return (WTERMSIG(status) == 0); } int WTERMSIG(int status) { return (status) & 0x7f; } } version (X86_64) { // from <fcntl.h> enum F_SETFL = 0x00000004; enum O_APPEND = 0x00000400; enum O_CLOEXEC = 0x00080000; enum O_CREAT = 0x00000040; enum O_DIRECTORY = 0x00010000; enum O_NONBLOCK = 0x00000800; enum O_TRUNC = 0x00000200; enum O_WRONLY = 0x00000001; enum O_RDONLY = 0x00000000; // from <unistd.h> enum F_LOCK = 0x00000001; enum F_OK = 0x00000000; enum F_TLOCK = 0x00000002; enum X_OK = 0x00000001; // from <bits/waitstatus.h> int WEXITSTATUS(int status) { return ((status) & 0xff00) >> 8; } int WIFEXITED(int status) { return (WTERMSIG(status) == 0); } int WTERMSIG(int status) { return (status) & 0x7f; } }
Не поверите, но это необходимо для работы с файлами, а также содержит переделанные макросы для работы со статусами запуска процессов, которые были запущены через системные вызовы Linux. Более того, чтобы все эти значения достать пришлось покопаться в заголовочных файлах gcc на обеих платформах (к примеру, /usr/include/aarch64-linux-gnu, для AArch64, т.е arm наподобие Raspberry Pi 4)!
Таким образом, при портировании следует быть готовым к извлечению собственными руками необходимых типов и констант.
Но самой большой свиньей при переносе кода может стать и строгость компилятора D, и особенности поведения некоторых функций из C. В нашем случае, мы столкнулись с малоизвестной особенностью функции mkstemp, которая генерирует временные файлы с именем собранным по определенному шаблону. И кстати сказать, это одна из причин нашего провала в первой попытке портирования redo — наша версия на BetterC заканчивала свою работу с Segmentation Fault, вне зависимости от поданных в нее аргументов!
Дело в том, что аргумент этой функции не должен быть константным и должен быть массивом с переменной длиной или просто массивом, и выглядит это так (исправленная версия):
char[16] temp_depfile = cast(char[]) ".depend.XXXXXX\0"; /// ...код далее dep_fd = mkstemp(cast(char*) temp_depfile);
И над правильной подачей мы думали настолько долго, что пришлось почитать мануалы GNU и провести ряд экспериментов, в частности, с самой функцией mkstemp и аргументами main():
version(Posix) { extern(C) { int printf (scope const(char*) format, ...) nothrow @nogc; import core.sys.posix.stdlib: malloc,mkstemp; import core.sys.posix.string: memcpy; int main(int argc, char** argv) { //char[12] p = [ //'H', 'e', 'l', 'l', 'o', 'X', 'X', 'X', 'X', 'X', 'X', '\0' //]; //char[12] d = cast(char[]) "HelloXXXXXX\0"; //int dep_fd = mkstemp(cast(char*) d.ptr); //printf("fd = %d", dep_fd); char* all = cast(char*) "all".ptr; char*[1] argv_def = [all]; if (argc == 0) { argc = 1; argv = argv_def.ptr; // printf("-%s\n", argv_def[0]); } printf("%d\n", argv.length); return 0; } } }
Конечно же, мы описали только то, что наиболее запомнилось и далеко не все интересные детали, но одно мы можем сказать точно: BetterC — это очень достойная альтернатива C и если бы мы знали ранее некоторые детали, то было бы намного проще. Однако, мы и дальше продолжим работу с BetterC и возможно выпустим еще ряд статей…
P.S: Удачный порт redo от нашей команды можно найти тут.