Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
Mazdaywik opened this issue Oct 22, 2020 · 2 comments
Open

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

Mazdaywik opened this issue Oct 22, 2020 · 2 comments
Assignees
Labels
Milestone

Comments

@Mazdaywik
Copy link
Member

Mazdaywik commented Oct 22, 2020

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

Цель

Нужно разработать API и механизм компоновки единиц трансляции на Си. При этом механизм должен быть

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

Текущая ситуация

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

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

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

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

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

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

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

Цель (развёрнуто)

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

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

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

Конкретика

Предлагается развитие идеи, описанной в #197. В файле создаётся дескриптор с уникальным именем, содержащий ссылки на все сущности программы (массивы идентификаторов, функций и т.д.). При компоновке создаётся временный файл .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, описываются структурами вида

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"
};

Функции — так:

/* 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:

#define USE_IDENT(ident_name) (identifiers[ident_ ## ident_name])

Макрос для переменной раскрывается с участием 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 его определить.

Строка

#define RL_UNIT_ENTRY_POINT XyZZy

всегда должна располагаться в начале файла — она распознаётся компилятором Рефала и из неё берётся идентификатор для списка инициализации.

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

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

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

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

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

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

@Mazdaywik
Copy link
Member Author

Два соображения

Ошибка компоновки для повторяющихся entry-функций

Компилятор должен выдавать ошибку, если в различных единицах трансляции находятся одноимённые entry-функции (#255, #265).

В текущем варианте с нативными функциями это работает — синхронно создаётся .rasl, в котором entry-функции перечислены и согласованы с содержимым .cpp-файла.

Предлагаемый выше подход подразумевает написание одного сишного файла, у которого компилятор Рефала интерпретирует только первую строку — извлекает из неё имя таблицы определений. Т.е. почти полностью сишный файл для компоновщика является чёрным ящиком. Поэтому компилятор не знает, какие entry-функции в этом файле определены.

Если сишный файл создаётся при использовании оптимизации прямой кодогенерации, компилятор мог бы генерировать синхронный с ним .rasl-файл с перечислением entry-функций. Но это не решает проблему с самописными файлами на Си.

Вариант решения проблемы — добавлять в исходник комментарий вида

/*
$ENTRY Foo
$ENTRY Bar
$ENTRY Baz
*/

содержимое которого будет анализироваться на стадии компоновки. Недостаток подхода: комментарий пишет сам программист и вынужден согласовывать его содержимое вручную.

Альтернативный вариант — парсить сам файл, ища в нём макросы RL_…_DEF более сложен технически: код может быть закомментированным, могут присутствовать директивы условной компиляции и т.д. Да и распарсить такие макросы будет сложнее, чем комментарий со строчками $ENTRY.

Поэтому скорее всего придётся остановиться на подходе с комментарием.

#include МАКРОС

Язык Си позволяет следующий фокус:

#define FILENAME "header.h"
#include FILENAME

Т.е. в директивах #include допустимо использовать макросы, раскрывающиеся в имена файлов.

Поэтому генерацию таблицы можно упростить:

#define TABLE_NAME "my-unit-name.table"
#include "rl-table-generator.h"

Файл rl-table-generator.h определит необходимые макросы и вставит таблицу несколько раз.

Можно поступить даже так:

#define TABLE_CONTENTS \
  RL_TABLE_IDENT(True, "True") \
  RL_TABLE_ENTRY_FUNC(SomeFunction, "SomeFunction") \
  RL_TABLE_EXTERNAL(Prout, "Prout") \
  …
#include "rl-table-generator.h"

Подход с TABLE_NAME применим для рукописных исходников, с TABLE_CONTENTS — для целевых файлов режима прямой кодогенерации.

@Mazdaywik
Copy link
Member Author

Рабочий пример

#include <stdio.h>


#define TABLE \
  UNIT(foo, 10) \
  UNIT(bar, 20) \
  UNIT(baz, 5)

/* START */
#define UNIT(name, val) \
  name_ ## name,

enum {
  TABLE
};

#undef UNIT


#define UNIT(name, val) \
  val,

int values [] = {
  TABLE
};

#undef UNIT
/* END */

int main() {
  printf("foo %d\n", values[name_foo]);
  printf("bar %d\n", values[name_bar]);
  printf("baz %d\n", values[name_baz]);
  return 0;
}

Текст между /* START */ и /* END */ помещается в библиотечный заголовочный файл.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant