bmstu-iu9 / bmstu-iu9/refal-5-lambda

Компоновка единиц трансляции на Си

Open
#324 2 comments 0 reactions 1 assignee Claimed by @Mazdaywik View on GitHub
task
Dominant language
C++
Stars
97
Forks
40
PR merge metrics
No merged PRs in 30d

Description

Эта задача — подзадача #197 и #313.

Цель
====
Нужно разработать API и механизм компоновки единиц трансляции на Си. При этом механизм должен быть
* простым — удобен для ручного написания (нативных) функций на Си,
* безопасным — если программист допустит простые ошибки, программа не должна компилироваться,
* не требовать синхронного `.rasl`-файла — сейчас он требуется, файлы рантайма имеют пустой `.rasl`-файл.

Текущая ситуация
============
На данный момент единственный способ написать примитивную функцию на C++ — написать исходник на Рефале и написать в нём функцию с нативной вставкой. В результате будет скомпилирована пара файлов `.rasl`+`.cpp`.

В первом файле будет находиться список «нативных» функций, которые нужно будет найти и подгрузить, список идентификаторов, которые нужно будет аллоцировать, и список пустых функций, функций-условий и swap’ов, которые аллоцируются кодом на Рефале. Второй файл будет содержать, помимо кода нативной вставки, enum’ы с номерами функций (включая внешние) и идентификаторов из списков. Для нативных функций будут написаны конструкторы глобальных объектов, которые регистрируют их в односвязном списке.

Рантайм при загрузке сначала прочитает модуль на RASL, аллоцирует всё, что там необходимо (идентификаторы, пустые функции, статические ящики, дескрипторы функций для нативных функций). Затем произведёт разрешение ссылок на нативные функции.

Таким образом, для написания внешних функций нужно писать исходник на Рефале с нативными вставками, скомпилированная единица трансляции будет представлять собой пару `.rasl`+`.cpp`.

Из-за этого файлы рантайма приходится создавать пустые файлы `.rasl`, иначе файлы не прикомпонуются.

Реализация привязана к C++, поскольку регистрация нативных функций выполняется конструкторами глобальных переменных.

Подход уродливый, нативные вставки уродливые (см. #318), привязка к C++.

Цель (развёрнуто)
============
Надо сделать так, чтобы исходники можно было писать на Си (C89) и подключать к программе естественным образом — указывая в командной строке компилятора. Без дополнительных файлов `.rasl` и прочей обработки.

Нужно сделать удобный и красивый интерфейс. Идеал — Рефал-05, но отличие в том, что Рефал-05 поддерживает только компиляцию в Си и компоновка программ у него статическая сишная. У Рефала-5λ компоновка динамическая, поскольку нужно подгружать файлы с байткодом и их интерпретировать.

Интерфейс должен упрощать написание пользователем «обвязки» — минимизировать количество и упростить написание вспомогательного кода, не относящегося к задаче.

Конкретика
========
Предлагается развитие идеи, описанной в #197. В файле создаётся дескриптор с уникальным именем, содержащий ссылки на все сущности программы (массивы идентификаторов, функций и т.д.). При компоновке создаётся временный файл `.c`, содержащий массив указателей на все дескрипторы исходных файлов. Рантайм проходит по этому списку и сканирует дескрипторы единиц трансляции, создавая идентификаторы, нативные функции и прочее.

Обвязка в данном случае — это объект дескриптора и всё с этим связанное — весь код, кроме тел функций на Си. Обвязка минимизируется путём написания макросов по определённым соглашениям.

Исходный файл на Си выглядит таким образом:
```C++
#define RL_UNIT_ENTRY_POINT XyZZy

#include "refalrts.h"

#include "rl-table-decl.h"
#include "my-unit-name.table"

RL_IDENT_DEF(True, "True");
RL_SWAP_DEF(PrevValue, "PrevValue", LOCAL);

RL_EXTERNAL(Prout, "Prout");

struct MyStruct {
int field;
};

RL_VAR_DEF(counter, int);
RL_VAR_DEF(my_struct, struct MyStruct);

RL_FUNC_DEF(MyFuncName, "MyFuncName", ENTRY) {
… RL_FUNC(Prout) …
… RL_FUNC(MyFuncName) … /* рекурсивный вызов */
… RL_IDENT(True) …


++RL_VAR(counter);

RL_VAR(my_struct).field = RL_VAR(counter);

return RL_RETURN_SUCCESS;
}

#include "rl-table-def.h"
#include "my-unit-name.table"
```
Содержимое `my-unit-name.table`:
```
RL_TABLE_BEGIN
RL_TABLE_IDENT(True)
RL_TABLE_FUNC(PrevValue)
RL_TABLE_FUNC(Prout)
RL_TABLE_VAR(counter)
RL_TABLE_VAR(my_struct)
RL_TABLE_FUNC(MyFuncName)
RL_TABLE_END
```

Первое вхождение `my-unit-table.table` превращается в enum:
```С++
enum rlgen_offsets {
rleid_True,
rlefn_PrevValue,
rlefn_Prout,
rlevar_counter,
rlevar_my_struct,
rlefn_MyFuncName,
}
```
`rl` означает Рефал-5λ, `e` — enum, `id`, `fn`, `var` — идентификатор, функция, переменная.

Далее, дескрипторы объектов, создаваемые макросами `RL_…_DEF`, описываются структурами вида
```C++
struct rl_ident_descr {
const char *name;
unsigned int flags;
};

struct rl_func_descr {
struct rl_ident_descr base;
enum rl_return (*func)(〈параметры〉);
};

struct rl_var_descr {
struct rl_ident_descr base;
size_t varsize;
};
```
Для идентификаторов инициализируется первая структура, для функций и переменных — вторая и третья. Флаги определяют тип сущности и область видимости функций. Идентификаторы компилируются примерно так:
```
/* RL_IDENT_DEF(True, "True"); */
static struct rl_ident_descr rldid_True = {
rleid_True << 4 | RL_FLAGS_IDENT,
"True"
};
```
Функции — так:
```C++
/* RL_FUNC_DEF(MyFuncName, "MyFuncName", ENTRY) { */
static enum rl_return rlcfn_MyFuncName(〈…〉);

static rl_func_descr rldfn_MyFuncName = {
{
rlefn_MyFuncName << 4 | RL_FLAGS_FUNC | RL_FLAGS_ENTRY,
"MyFuncName"
},
rlcfn_MyFuncName
};

static enum rl_return rlcfn_MyFuncName(〈…〉) {
```
Добавление сдвинутого идентификатора ко флагам технически не обязательно, но нужно для статического контроля. Если пользователь забудет подключить `rl-table-defs.h` или забудет в ней одну строчку, то выскочит ошибка компиляции. Также рантайм может контролировать правильность идентификатора.

Макросы объявления переменных раскрываются аналогично, разве что для типа переменной вводится синоним при помощи `typedef`.

Обращения к функциям, переменным и идентификаторам раскрываются примерно также, как сейчас раскрывается `USE_IDENT` в `Library` с той разницей, что макрос этот будет в API:

https://github.com/bmstu-iu9/refal-5-lambda/blob/3cd08edd727764af9632b3b2c067678966890c98/src/lib/Library.ref#L847

Макрос для переменной раскрывается с участием typedef-синонима, поэтому дополнительное приведение типов не нужно.

Второе вхождение таблицы раскрывается так:
```С++
struct rl_ident_descr RL_UNIT_ENTRY_POINT[] {
&rldid_True,
&rldfn_PrevValue.base,
&rldfn_Prout.base,
&rldvar_counter.base,
&rldvar_my_struct.base,
&rldfn_MyFuncName.base,
NULL
}
```
Поэтому, если пользователь забудет определить объект из таблицы, то тоже получим ошибку компиляции.

Можно также придумать фокус, чтобы мы получали ошибку компиляции, если второго вхождения таблицы нет. Можно в `rl-table-decl.h` объявить, но не определить статический объект, и использовать его, а в `rl-table-def.h` его определить.

Строка
```C++
#define RL_UNIT_ENTRY_POINT XyZZy
```
всегда должна располагаться в начале файла — она распознаётся компилятором Рефала и из неё берётся идентификатор для списка инициализации.

Для генерации имени точки входа можно написать утилиту, которая вычисляет хеш от содержимого файла (начиная со второй строчки) и файла `.table` (оба имени задаются в командной строке) и на основе этого хеша генерировала случайное имя. Например, хеш в системе счисления по основанию 52 (буквы обоих регистров) или 62 (буквы обоих регистров + цифры), но при этом нужно гарантировать, что имя не начинается на цифру.

Очевидно, что когда компилятор генерирует код на Си, он его компилирует по той же схеме, разве что файлы `.table` не создаёт — текст таблицы дважды помещает в исходник.

Если файл не начинается на определение макроса `RL_UNIT_ENTRY_POINT`, то это файл рантайма, он просто передаётся компилятору Си. Можно сканировать файл целиком и выдавать предупреждение, если макрос объявлен, но не располагается на первой строчке.

В целях отладки для единицы трансляции нужно хранить имя файла единицы трансляции. Это несложно сделать, добавив ещё одну сущность, заносимую в таблицу, но пример я решил не перегружать. Также данный подход очевидно расширяется на поддержку метафункций.

Сквозная нумерация функций, переменных и идентификаторов сделана для упрощения работы программиста — нужно объявлять не несколько отдельных таблиц, а только одну. Различные внутренние имена для идентификаторов, функций и переменных (`rleid_`, `rlefn_`, `rlevar_`, соответственно) не дадут пользователю перепутать разные сущности, например, указав имя функции в `RL_IDENT` или `RL_VAR`.

**Вывод.** Предложенный способ работоспособен и не сильно напрягает с написанием связующего кода. А тот код, который написать требуется (таблица + определения используемых сущностей), может быть статически проверен компилятором языка Си на согласованность.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.