Модули для Limbo написанные на C так же иногда называют драйверами OS Inferno т.к. они встроены в ядро OS. Необходимость в таких модулях обычно вызвана либо желанием добавить к Limbo отсутствующую в Inferno функциональность (подключить существующие 3rd-party C/C++ библиотеки, дать доступ к специфичным для конкретной host OS syscall-ам) либо желанием выжать максимально возможную производительность (по моим наблюдениям разница в скорости между Limbo с включенным JIT и C примерно 1.3-1.5 раза, но иногда и это может оказаться критичным).
К сожалению, пока в Inferno нет возможности динамически подгружать модули реализованные на C, поэтому приходится встраивать его прямо в ядро OS. (Это немного осложняет разработку, т.к. после каждого изменения приходится пересобирать Inferno. К счастью, необходимая частичная пересборка занимает примерно секунд 10.)
Для встраивания своего модуля необходимо модифицировать несколько файлов:
Для решения этой проблемы я набросал скриптик на Perl — в большинстве случаев достаточно изменить в нём название добавляемого модуля в строчке
и он внесёт необходимые изменения во все вышеупомянутые файлы встроив ваш модуль в ядро OS Inferno. В более сложных случаях, например когда необходимо подключить к Inferno дополнительные C/C++ библиотеки, этот скрипт придётся модифицировать под ваши нужды (пример такой модификации для подключения C++ библиотеки re2 можно увидеть в модуле Re2). Скрипт можно запускать с параметром
Итак, скачиваем скрипт, кладём его в
Для начала, создадим два файла:
И запустим пересборку OS Inferno:
Теперь мы можем написать программу на Limbo, которая успешно подгрузит наш, пока ничего полезного не делающий, модуль:
В процессе сборки файл
Далее, в процессе загрузки OS Inferno будет однократно вызвана функция
Начнём с простых типов данных, чтобы не усложнять пример работой со ссылками.
Пересобираем Inferno.
Не забываем перезапустить
текущий запущенный
При сборке, для функции
Я пока не разбирался, что такое
Вот что получилось у нас в
С
Аналогично реализуется работа с другими Limbo-вскими типами данных: через
Пользовательские adt объявленные в
Скорее всего после изменения
Для генерации исключения достаточно вызвать функцию
Разумеется, если вы выделяли самостоятельно память через
Один неявный момент при возвращении результата из функции связан с тем, что
Для решения второй проблемы в C-функциях перед сохранением возвращаемого значения в
либо так (
Насколько я понял, на данный момент разницы между этими вариантами нет, но если Dis будет переписан для работы одновременно на нескольких CPU/Core, то второй вариант будет корректно работать, а первый нет.
В Inferno используется глобальная блокировка Dis (вероятно, аналогичная широко известному GIL в питоне). C-шные функции вызываются с установленной блокировкой, т.к. безопасно работать со структурами данных Dis (т.е. любыми значениями и переменными доступными из Limbo — включая параметры и возвращаемые значения C-шных функций) можно только с установленной блокировкой.
Но если ваша функция должна выполнить некоторую долгую операцию (например чтение/запись или вызов «тяжёлой» функции из внешней библиотеки или выполнить какие-то длительные вычисления), то необходимо перед этой операцией снять
Часть 2.
Содержание
Встраиваем модуль в ядро
К сожалению, пока в Inferno нет возможности динамически подгружать модули реализованные на C, поэтому приходится встраивать его прямо в ядро OS. (Это немного осложняет разработку, т.к. после каждого изменения приходится пересобирать Inferno. К счастью, необходимая частичная пересборка занимает примерно секунд 10.)
Для встраивания своего модуля необходимо модифицировать несколько файлов:
libinterp/mkfile
, module/runt.m
, emu/Linux/emu
и emu/Linux/emu-g
. А поскольку каждый новый модуль пытается встраиваться в эти же файлы в одни и те же места, и таких модулей пользователь может захотеть добавить несколько, причём в неизвестном заранее порядке, то стандартная команда patch
не сможет внести необходимые изменения. Один-два модуля она добавит, но со следующими у неё возникнет проблема т.к. редактируемое место в этих файлах начнёт слишком сильно отличаться от того, что она ожидала увидеть.Для решения этой проблемы я набросал скриптик на Perl — в большинстве случаев достаточно изменить в нём название добавляемого модуля в строчке
my $MODNAME = 'CJSON';
и он внесёт необходимые изменения во все вышеупомянутые файлы встроив ваш модуль в ядро OS Inferno. В более сложных случаях, например когда необходимо подключить к Inferno дополнительные C/C++ библиотеки, этот скрипт придётся модифицировать под ваши нужды (пример такой модификации для подключения C++ библиотеки re2 можно увидеть в модуле Re2). Скрипт можно запускать с параметром
-R
для отката внесённых изменений.Итак, скачиваем скрипт, кладём его в
$INFERNO_ROOT
, переименовываем в patch.example
, изменяем в нём имя модуля на «Example», и запускаем. Теперь (несуществующий пока) модуль Example подключён к ядру, осталось его создать и пересобрать Inferno вместе с ним.Для начала, создадим два файла:
module/example.m
Example: module { PATH: con "$Example"; };
libinterp/example.c
#include <lib9.h> #include <isa.h> #include <interp.h> #include "runt.h" #include "examplemod.h" void examplemodinit(void) { builtinmod("$Example", Examplemodtab, Examplemodlen); }
И запустим пересборку OS Inferno:
$ (cd libinterp/; mk nuke)
$ rm Linux/386/bin/emu # work around "text file busy" error
$ mk install
Теперь мы можем написать программу на Limbo, которая успешно подгрузит наш, пока ничего полезного не делающий, модуль:
testexample.b
implement TestExample; include "sys.m"; include "draw.m"; include "example.m"; TestExample: module { init: fn(nil: ref Draw->Context, nil: list of string); }; init(nil: ref Draw->Context, nil: list of string) { sys := load Sys Sys->PATH; example := load Example Example->PATH; if(example == nil) sys->print("fail to load Example: %r\n"); else sys->print("Example module loaded\n"); }
$ emu
; limbo testexample.b
; testexample
Example module loaded
;
Как это работает
В процессе сборки файл
module/example.m
анализируется, и генерируются необходимые C-шные структуры описывающие этот модуль — в отдельном файле libinterp/examplemod.h
— и весь его публичный интерфейс (константы, adt-шки, функции) — добавляются в файл libinterp/runt.h
, содержащий информацию по всем C-модулям. Эти два .h-файла уже подключены к нашему libinterp/example.c
.Далее, в процессе загрузки OS Inferno будет однократно вызвана функция
examplemodinit()
, которая должна проинициализировать глобальные данные нашего модуля (если таковые есть) и подключить его (вызовом builtinmod(…)
) к ядру Inferno. Вызов builtinmod()
устанавливает связь между нашим модулем и псевдо-путём к нему $Example указанным в константе PATH
, используемой из Limbo при загрузке этого модуля командой load
.Функции: приём параметров и возврат результата
Числа
Начнём с простых типов данных, чтобы не усложнять пример работой со ссылками.
module/example.m
Example: module { ... increment: fn(i: int): int; };
libinterp/example.c
... void Example_increment(void *fp) { F_Example_increment *f; int i; f = fp; i = f->i; *f->ret = i + 1; }
Пересобираем Inferno.
testexample.b
... init(nil: ref Draw->Context, nil: list of string) { ... sys->print("increment(5) = %d\n", example->increment(5)); }
Не забываем перезапустить
emu
перед запуском нашего примера, т.к.текущий запущенный
emu
не содержит в себе модифицированный C-модуль.$ emu
; limbo testexample.b
; testexample
Example module loaded
increment(5) = 6
;
Как это работает
При сборке, для функции
increment()
найденной в module/example.m
, в файл libinterp/runt.h
было автоматически добавлено описание этой функции, её параметров и возвращаемых значений:void Example_increment(void*);
typedef struct F_Example_increment F_Example_increment;
struct F_Example_increment
{
WORD regs[NREG-1];
WORD* ret;
uchar temps[12];
WORD i;
};
Я пока не разбирался, что такое
regs
; temps
добавлен явно для выравнивания; ret
это указатель на возвращаемое значение; а i
это наш параметр.Строки
module/example.m
Example: module { ... say: fn(s: string); };
libinterp/example.c
... void Example_say(void *fp) { F_Example_say *f; String *s; char *str; f = fp; s = f->s; str = string2c(s); print("%s\n", str); }
testexample.b
... init(nil: ref Draw->Context, nil: list of string) { ... example->say("Hello!"); }
$ emu
; limbo testexample.b
; testexample
Example module loaded
increment(5) = 6
Hello!
;
Как это работает
Вот что получилось у нас в
libinterp/runt.h
:void Example_say(void*);
typedef struct F_Example_say F_Example_say;
struct F_Example_say
{
WORD regs[NREG-1];
WORD noret;
uchar temps[12];
String* s;
};
С
noret
вместо ret
всё понятно, функция say()
ничего не возвращает. Тип String*
это C-реализация Limbo-вский строк. Найти struct String
можно в include/interp.h
, функции для работы со строками (вроде использованной в нашем примере string2c()
) находятся в libinterp/string.c
.Аналогично реализуется работа с другими Limbo-вскими типами данных: через
Array*
, List*
, etc. Не для всех структур есть готовые вспомогательные функции как для работы со строками, но можно найти достаточно примеров в реализации опкодов виртуальной машины libinterp/xec.c
(например, как работать со срезами массивов).Пользовательские adt объявленные в
module/example.m
преобразуются в обычные C-шные struct (а pick adt в union). Кортежи так же преобразуются в обычные struct.Скорее всего после изменения
module/example.m
вам придётся запустить сборку (которая провалится по ошибке) чтобы обновился libinterp/runt.h
и вы увидели какие именно структуры были созданы для ваших данных и поняли как реализовывать работу с ними в libinterp/example.c
.Исключения
Для генерации исключения достаточно вызвать функцию
error()
. Можно подключить raise.h
для возврата стандартных ошибок описанных в libinterp/raise.c
или объявить аналогичным образом свои собственные в libinterp/example.c
.Разумеется, если вы выделяли самостоятельно память через
malloc()
, то перед вызовом error()
необходимо эту память освободить, иначе будет утечка. Объекты выделяемые стандартным образом через heap (вроде String*
и Array*
) освобождать не обязательно, их всё-равно чуть позже найдёт и удалит сборщик мусора. (Более детально о работе heap и сборщика мусора в части 2.)Возвращаем ссылку
Один неявный момент при возвращении результата из функции связан с тем, что
*f->ret
физически указывает на ячейку памяти, где должен будет находиться результат выполнения функции после её успешного завершения. Из этого вытекают два следствия:- Если вы сначала положите результат в
*f->ret
, а потом решите что произошла ошибка и сгенерируете исключение, то произойдёт кое-что невозможное с точки зрения Limbo: функция И вернёт значение И вызовет исключение. - Если в переменной, куда возвращается результат вашей функции, уже лежит какое-то значение (которое тоже является ссылкой, разумеется, ведь тип у этой переменной такой же, как у возвращаемого вашей функцией значения), то вы должны его освободить из памяти до того, как перепишете эту ссылку своей.
increment()
вот таким образом:libinterp/example.c
... void Example_increment(void *fp) { ... *f->ret = i + 1; error("some error"); }
testexample.b
... init(nil: ref Draw->Context, nil: list of string) { ... i := 0; { i = example->increment(5); } exception e { "*" => sys->print("catched: %s\n", e); } sys->print("i = %d\n", i); }
; testexample
...
catched: some error
i = 6
;
Для решения второй проблемы в C-функциях перед сохранением возвращаемого значения в
*f->ret
необходимо освободить текущее значение. Обычно это делается либо так:destroy(*f->ret);
*f->ret = new_value;
либо так (
H
это C-шный аналог Limbo-вского nil
):void *tmp;
...
tmp = *f->ret;
*f->ret = H;
destroy(tmp);
...
*f->ret = new_value;
Насколько я понял, на данный момент разницы между этими вариантами нет, но если Dis будет переписан для работы одновременно на нескольких CPU/Core, то второй вариант будет корректно работать, а первый нет.
Блокирование Dis
В Inferno используется глобальная блокировка Dis (вероятно, аналогичная широко известному GIL в питоне). C-шные функции вызываются с установленной блокировкой, т.к. безопасно работать со структурами данных Dis (т.е. любыми значениями и переменными доступными из Limbo — включая параметры и возвращаемые значения C-шных функций) можно только с установленной блокировкой.
Но если ваша функция должна выполнить некоторую долгую операцию (например чтение/запись или вызов «тяжёлой» функции из внешней библиотеки или выполнить какие-то длительные вычисления), то необходимо перед этой операцией снять
release()
блокировку, чтобы Dis продолжил выполняться в другой нити параллельно с вашей функцией, а после снова её поставить acquire()
(иначе нельзя будет вернуть результат и вернуться в вызвавший эту функцию код на Limbo). Пример можно найти в реализации sys->read()
в файле emu/port/inferno.c
:void
Sys_read(void *fp)
{
...
release();
*f->ret = kread(fdchk(f->fd), f->buf->data, n);
acquire();
}
Часть 2.