bmstu-iu9 / refal-5-lambda

Компилятор Рефала-5λ
https://bmstu-iu9.github.io/refal-5-lambda
Other
79 stars 35 forks source link

Реализация модулей типа N #170

Closed Mazdaywik closed 5 years ago

Mazdaywik commented 6 years ago

Эта задача — подзадача для #76, частично относится к #87.

Цель

Необходимо реализовать генерацию и загрузку модулей типа N — библиотек .dll/.so.

Что уже есть

Этого всего уже немало и это неплохо.

Что надо сделать

(список может пополняться)

Mazdaywik commented 5 years ago

Детали создания динамических библиотек

Задача состоит в том, чтобы для используемых компиляторов (Windows: BCC 5.5.1, Visual C++, GCC, Watcom; Linux: GCC) найти способ (а) получать библиотеку, из которой можно извлечь refalrts::NativeModule g_module, и (б) загрузить эту библиотеку. При этом способ должен ложиться в инфраструктуру компилятора.

При этом желательно минимизировать использование платформенно-зависимых средств компилятора.

Windows

На Windows библиотеки могут экспортировать только функции, переменные экспорту не подлежат. По крайней мере, я таких упоминаний не встречал. Поэтому для экспорта значения g_module нужно экспортировать функцию, возвращающую &g_module.

В качестве эксперимента требовалось экспортировать функцию вида:

int the_func(int x) {
  return x * 100;
}

При этом её имя должно быть доступно как "the_func" в GetProcAddress(). Путём экспериментов выяснилось, что разные компиляторы по разному декорируют имя для разных соглашений по вызову (__cdecl, __stdcall…), для экспорта функции используются разные директивы (__declspec(dllexport), __attribute__((dllexport))…), разные директивы используются для задания этих соглашений по вызову.

Альтернативный способ экспорта функций — использование .def-файлов. Синтаксис у него тоже различается в разных компиляторах, но можно выбрать минимальное общее подмножество, достаточное для экспорта одной функции.

В общем, опытным путём удалось экспортировать функцию в следующей форме:

#include <windows.h>

extern "C"
#ifdef __BORLANDC__
__declspec(dllexport)
#endif
int WINAPI the_func(int x) {
  return x * 100;
}

и .def-файл:

EXPORTS
    the_func                       @1   ; the_func

Оказалось, что передать .def-файл через командную строку bcc32 (версии BCC 5.5.1) нельзя. Нужно сначала компилировать в объектники, а потом явно вызывать линковщик. Но использование соглашения stdcall (задаваемое макросом WINAPI) обеспечивает необходимое декорирование имени (вернее, его отсутствие). Заметим, на других компиляторах использование __declspec(dllexport) + WINAPI приводит к другому декорированию (не помню, какому, давно ковырял).

Остальные компиляторы для Windows правильно понимают .def-файл в своей командной строке.

Командные строки получились следующие:

Импортирующий файл довольно очевиден:

#include <windows.h>
#include <stdio.h>

typedef int (WINAPI *callback_t)(int);

int main(int argc, char *argv[]) {
  const char *dllname = ".\\dlltest";
  HMODULE lib;
  callback_t func;
  int arg;

  if (argc > 1) {
    dllname = argv[1];
  }

  lib = LoadLibraryEx(dllname, NULL, 0);
  if (! lib) {
    printf("Can't load library\n");
    return 1;
  }

  func = (callback_t) GetProcAddress(lib, "the_func");

  if (func) {
    arg = 42;
    printf("call func(%d) = %d\n", arg, func(arg));
  } else {
    printf("Can't find symbol\n");
    FreeLibrary(lib);
    return 1;
  }

  FreeLibrary(lib);
  printf("Success\n");
  return 0;
}

Linux

Под Линуксом возникла проблема.

На первый взгляд, проблем нет. В импортируемом файле просто объявляем нестатическую глобальную переменную, в импортирующем используем dlopen() + dlsym() для доступа:

int the_value = 42;
#include <dlfcn.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
  const char *dllname = "./dlltest.so";
  void *lib;
  int *pvalue;

  if (argc > 1) {
    dllname = argv[1];
  }

  lib = dlopen(dllname, RTLD_NOW);
  if (! lib) {
    printf("Can't load library: %s\n", dlerror());
    return 1;
  }

  pvalue = (int*) dlsym(lib, "the_value");

  if (pvalue) {
    printf("*pvalue = %d\n", *pvalue);
  } else {
    printf("Can't find symbol\n");
    dlclose(lib);
    return 1;
  }

  dlclose(lib);
  printf("Success\n");
  return 0;
}

Но засада возникает при сборке:

all: run dlltest.so
    ./run ./dlltest.so

run: run.cpp
    g++ -o $@ $+ -ldl

dlltest.so: dlltest.cpp
    g++ -shared -fpic -o $@ $+

Ключ -ldl, который подключает библиотеку с функциями dlopen(), dlsym()…, в командной строке должен быть последним. Т.е.

g++ -o run run.cpp -ldl

работает, а

g++ -ldl -o run run.cpp

не работает.

Однако, Рефал-5λ сейчас поддерживает командную строку только такого вида:

〈префикс〉〈имя-целевого-файла〉〈дополнительные-флаги〉〈исходники〉

где

Места для передачи -ldl в конце просто нет.

Варианты:

Все решения сомнительны.

Mazdaywik commented 5 years ago

Перенести флаги в конец нельзя.

#include <stdio.h>

int main() {
#ifdef HELLO
  printf("Hello, %d\n", HELLO);
#else
  printf("Hello, World!\n");
#endif
  return 0;
}

Как минимум BCC учитывает порядок флагов:

D:\…>bcc32 -DHELLO=1 test.cpp
Borland C++ 5.5.1 for Win32 Copyright (c) 1993, 2000 Borland
test.cpp:
Turbo Incremental Link 5.00 Copyright (c) 1997, 2000 Borland

D:\…>test.exe
Hello, 1

D:\…>bcc32 test.cpp -DHELLO=1
Borland C++ 5.5.1 for Win32 Copyright (c) 1993, 2000 Borland
test.cpp:
Turbo Incremental Link 5.00 Copyright (c) 1997, 2000 Borland

D:\…>test.exe
Hello, World!

Что же делать?

Расширять синтаксис --cpp-command-??? пока не будем, поскольку расширение пока сложно объединить со стабильной версией. Перенос флагов в конец отваливается.

Поэтому возможный вариант — второй. Если добавить аналог --cpp-command-???=…, например, --cpp-command-???-suf=…, то можно выборочно добавлять суффикс к исполнимым файлам и библиотекам. Но, впрочем, -ldl для .so-шек мешать особо не будет, можно закрыть глаза.

Поэтому проще добавить аналог -F/-f. Опция -F (синоним --cppflags=…) не закавычивает свой аргумент при передаче компилятору C++. Опция -f (синоним --cppflag=…) закавычивает. Т.е. можно написать -F"-O3 -g -DX=1" или можно написать -f-O3 -f-g -f-DX=1. Или можно написать -f"-IC:\Program Files\…\Include", поскольку аргумент будет передан закавыченным. Аналог какой опции должен быть для добавления в конец — -F, -f или обеих — надо думать.

Ещё один вариант — запоминать относительный порядок флагов и исходников и их восстанавливать при компиляции. Вроде

srefc -F-DX=1 file1.ref -F-DY=2 file2.ref -F-ldl

даст

cc -DX=1 file1.cpp -DY=2 file2 -ldl

Но он сложнее, много придётся переписывать в srefc.ref.

Mazdaywik commented 5 years ago

Ещё одна проблема с .def-файлами для Windows.

Нужно будет подставлять путь к .def-файлу в CPPLINEL. А переменная CPPLINEL может содержать пробелы, поэтому должна заключаться в кавычки:

https://github.com/bmstu-iu9/refal-5-lambda/blob/1f7f1ba89cd6e4aab71385a1a6deb1c1708bf38f/src/scripts/srefc.bat#L61 https://github.com/bmstu-iu9/refal-5-lambda/blob/1f7f1ba89cd6e4aab71385a1a6deb1c1708bf38f/src/srmake/SRMake.ref#L149-L156

Но если компилятор сам поставлен в папку, путь к которой содержит пробелы? Получится фигня. Значит, нельзя просто так включать путь к .def-файлу внутрь CPPLINEL.

Возможное решение: воспользоваться флагом из предыдущего комментария: на Linux (unix-like) туда будет кидаться -ldl, на Windows — путь к .def-файлу. И, наверное, этот флаг должен быть аналогом --cpp-command-???=…, чтобы он мог различаться для исходников и библиотек.

Mazdaywik commented 5 years ago

Библиотечный рантайм должен быть минималистичным и «мобильным» — не зависеть от основного рантайма. Как минимум (это необходимое условие), библиотечный рантайм не должен компоноваться статически с функциями основного рантайма.

Эта цель была достигнута. Функции на Си++, входящие в библиотеку, получают указатель на виртуальную машину (#163), поэтому в неё была добавлена «виртуальная таблица» — структура, содержащая указатели на функции API библиотеки времени выполнения.

Из общих соображений, косвенный доступ должен снизить быстродействие функций, реализованных на Си, и при этом не повлиять на функции, скомпилированные в RASL. Соответственно, должно упасть быстродействие нативных функций и быстродействие программ, скомпилированных с ключом -Od.

Были сделаны замеры на компьютере с процессором Intel® Core™ i5-2430M 2.40 GHz, кэши 128/512/3,0, ОЗУ 8 Гбайт, ОС Windows 10 1903 x64. Использовался компилятор BCC 5.5.1. Делались замеры на коммите bcf6a92ea0acb5e59e7fb2f1b6f8dce314533e9b и на b0b1c78d8e6cecccf1d81f1e5feb418cebfe9162 в режиме по умолчанию и с SCRIPT_FLAGS=--scratch, set SRMAKE_FLAGS=-X-Od --runtime=refalrts-diagnostic-initializer. Бенчмарк дефолтовый, 21 прогон, медиана и квартили.

(Примечание: полное время = чистое время Рефала + нативные функции + оверхед рантайма. Последний нас не интересует.)

Парадоксально чистое время Рефала без оптимизации -Od ускорилось (25,864 → 25,218, 2,5 %), причём достоверно, ибо доверительные интервалы не пересекаются! Я не нахожу объяснения этому факту. Возможно, методика наблюдений неточна (хотя я ей верил). Либо замер неточный. Но перемерять лень, бенчмарк с 21 прогоном требует ≈10 минут. Полное время, однако, демонстрирует только условно достоверное ускорение.

Оценить изменение производительности нативных функций не представляется возможным, поскольку доверительные интервалы существенно пересекаются (во всех четырёх измерениях) — результат измерений недостоверен.

Наиболее интересно замедление с -Od. Оно достоверно и для чистого времени Рефала, и для полного времени выполнения программы. Для чистого деградация составляет 5,5 %, для полного — 5,1 %.

Ранее оптимизация -Od давала прирост (первый-второй замеры, чистое время) 32,0 %, теперь даёт лишь 26,3 %. Иначе говоря, ранее ускоряло на треть, теперь только на четверть.

Mazdaywik commented 5 years ago

Следующий пункт — сделать Hash динамически компонуемым модулем. Но это нельзя просто так взять и сделать.

Логично, что в этом случае библиотеку Hash нужно будет вынести из префикса slim. Но тогда, когда программа (в частности, сам компилятор) использует *$FROM Hash, в компилируемый модуль должна добавляться соответствующая ссылка. Т.е. Hash.rasl должен быть (в папке /srlib) не пустым, а содержать ссылку. Обойтись *$REFERENCE Hash в Hash.rasl.froms не получится, поскольку должна сохраняться возможность компиляции при помощи srefc.

Вопрос: куда помещать этот самый Hash.dll/Hash.so? Чтобы его мог подцепить srefc-core, достаточно его поместить в папку bin. А если пишется другая программа? Переменная RL_MODULE_PATH должна содержать папку с файлом Hash.so.

Так что есть несколько путей закрыть эту заявку:

А ведь надо будет ещё модифицировать сборку стабильной версии, чтобы она создавала динамические модули (возможно, дело не ограничится одним Hash).

Возможно, дело не ограничится одним Hash. Может быть интересно разделить Library на две части: одна содержит только встроенные функции Рефала-5, вторая — расширения. В префикс slim будет входить только первая. А вторая — сохранит название Library. Но это как-нибудь потом.

Mazdaywik commented 5 years ago

Время выполнения тестов (в корне папки autotests) действительно ускорилось. Делался замер 2019-08-12, тесты выполнялись примерно 35 минут 10 секунд, и сегодня (2019-09-03), примерно 21 минуту 10 секунд. (Замер выполнялся путём создания скриншотов, каждые 10 секунд, соответственно, было сделано 211 и 127 скриншотов.)

Mazdaywik commented 5 years ago

Оказалось, что сделать Hash полностью (и как динамический модуль, и удалить из slim-префикса) оказалось сравнительно несложно, поэтому сделано всё полностью.

Кроме того, были ускорены автотесты (коммит 235f2def1e03678f2fd3cf3f144d0f19ddba67d4), задача эта нигде явно не описывалась, но я давно хотел её сделать.

По этой задаче выполнено всё. Её можно закрывать.

Mazdaywik commented 5 years ago

Коммит выше (41277c2bf6fdc7f8479d528c1d84e72a5770fdba) относится к #167.