Небольшие заметки о портировании в BetterC

Две с половиной недели назад мы закончили один из наших проектов — портирование программы с C в BetterC, и программой этой оказалась совсем крошечная утилита redo. Об этой утилите мы уже писали и даже сделали ее версию на чистом D, казалось бы с какой стати мы взялись за порт точно такой же утилиты?

На вопрос ответить довольно легко — утилита на чистом C нас заинтриговала в буквальном смысле. Смотрите сами: компактный размер — утилита занимает около 1830 Кб (в зависимости от платформы и варианта сборки), умеет делать параллельные сборки, содержит в себе полную реализацию хэширования SHA-256 и использует его, а также время изменения файлов для мониторинга свежести целей, а также сама утилита умеет без каких-то сторонних библиотек запускать скрипты на любых языках. При этом программа не использует сторонних библиотек и написана на чистом C99, кроме стандартной библиотеки и некоторых функций из поставки POSIX здесь не используется вообще. Наша версия на D гораздо скромнее в возможностях и имеет размер в 2701900 Кб (в зависимости от компилятора и варианта сборки), что несколько убивает шарм этой утилиты.

Кроме того, с момента нашего знакомства с 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 от нашей команды можно найти тут.

aquaratixc

Программист-самоучка и программист-любитель

Добавить комментарий