Closed Mazdaywik closed 5 years ago
Задача состоит в том, чтобы для используемых компиляторов (Windows: BCC 5.5.1, Visual C++, GCC, Watcom; Linux: GCC) найти способ (а) получать библиотеку, из которой можно извлечь refalrts::NativeModule g_module
, и (б) загрузить эту библиотеку. При этом способ должен ложиться в инфраструктуру компилятора.
При этом желательно минимизировать использование платформенно-зависимых средств компилятора.
На 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-файл в своей командной строке.
Командные строки получились следующие:
bcc32 -tWD -edlltest-bcc.dll dlltest.cpp
cl /EHcs /LD dlltest.def /Fedlltest-vc.dll dlltest.cpp
gcc -shared -Wl,--enable-stdcall-fixup dlltest.def -odlltest-gcc.dll dlltest.cpp
cl -W3 -passwopts:-wcd=13 -LD dlltest.def -Fedlltest-watcom.dll dlltest.cpp
Все вышеперечисленные командные строки можно записать в виде:
〈префикс〉 〈имя-новой-dll〉 〈исходники〉
причём имя .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;
}
Под Линуксом возникла проблема.
На первый взгляд, проблем нет. В импортируемом файле просто объявляем нестатическую глобальную переменную, в импортирующем используем 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λ сейчас поддерживает командную строку только такого вида:
〈префикс〉〈имя-целевого-файла〉〈дополнительные-флаги〉〈исходники〉
где
〈префикс〉
задаётся в --cpp-command-exe
или --cpp-command-lib
,〈имя-целевого-файла〉
формируется компилятором на основе флага -o
или имени первого исходника,〈дополнительные-флаги〉
передаются опциями -F
и -f
,〈исходники〉
— исходники C++.Места для передачи -ldl
в конце просто нет.
Варианты:
--cpp-command-???
до строки вида g++ -o@EXE@ @FLAGS@ @SOURCES@ -ldl
. С одной стороны, это идеально решит проблему с передачей опций, которые могут быть только в конце. С другой — затруднится раскрутка стабильной версии. Сейчас к префиксу, заданному в c-plus-plus.conf.*
, можно в скрипте просто приписать имя исполнимого файла и исходники. Потом придётся чем-то парсить шаблон.-F
/-f
(или --cpp-command-???
), которые будут добавляться в конец командной строки. Нужно будет добавлять новую опцию, которая не будет до следующей версии пониматься стабильной версией.Все решения сомнительны.
Перенести флаги в конец нельзя.
#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
.
Ещё одна проблема с .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-???=…
, чтобы он мог различаться для исходников и библиотек.
Библиотечный рантайм должен быть минималистичным и «мобильным» — не зависеть от основного рантайма. Как минимум (это необходимое условие), библиотечный рантайм не должен компоноваться статически с функциями основного рантайма.
Эта цель была достигнута. Функции на Си++, входящие в библиотеку, получают указатель на виртуальную машину (#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 прогон, медиана и квартили.
(Примечание: полное время = чистое время Рефала + нативные функции + оверхед рантайма. Последний нас не интересует.)
(Total refal time)
): 25,864 с (25,557…25,849).
Нативные функции (Native time
): 1,972 с (1,871…2,140).
Полное время (Total refal time
): 29,750 с (29,688…29,859).-Od
:
Чистое время Рефала: 17,598 с (17,411…17,820).
Нативные функции: 1,926 с (1,772…1,967).
Полное время: 21,968 с (21,938…21,984).-Od
:
Чистое время Рефала: 18,570 с (18,512…18,834).
Нативные функции: 1,989 с (1,925…2,124).
Полное время: 23,094 с (23,047…23,203).Парадоксально чистое время Рефала без оптимизации -Od
ускорилось (25,864 → 25,218, 2,5 %), причём достоверно, ибо доверительные интервалы не пересекаются! Я не нахожу объяснения этому факту. Возможно, методика наблюдений неточна (хотя я ей верил). Либо замер неточный. Но перемерять лень, бенчмарк с 21 прогоном требует ≈10 минут. Полное время, однако, демонстрирует только условно достоверное ускорение.
Оценить изменение производительности нативных функций не представляется возможным, поскольку доверительные интервалы существенно пересекаются (во всех четырёх измерениях) — результат измерений недостоверен.
Наиболее интересно замедление с -Od
. Оно достоверно и для чистого времени Рефала, и для полного времени выполнения программы. Для чистого деградация составляет 5,5 %, для полного — 5,1 %.
Ранее оптимизация -Od
давала прирост (первый-второй замеры, чистое время) 32,0 %, теперь даёт лишь 26,3 %. Иначе говоря, ранее ускоряло на треть, теперь только на четверть.
Следующий пункт — сделать 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
, оставив эту функцию в slim-префиксе. Это нелогично.Hash
в текущей задаче — вынести её куда-то вон. Но в других задачах не упоминается Hash
, поэтому логично создать для этого новую задачу.Hash
в динамический модуль, а всё остальное — вынести вон. Половинчатое решение, может быть проще сделать целиком, чем дробить пополам.А ведь надо будет ещё модифицировать сборку стабильной версии, чтобы она создавала динамические модули (возможно, дело не ограничится одним Hash
).
Возможно, дело не ограничится одним Hash
. Может быть интересно разделить Library
на две части: одна содержит только встроенные функции Рефала-5, вторая — расширения. В префикс slim
будет входить только первая. А вторая — сохранит название Library
. Но это как-нибудь потом.
Время выполнения тестов (в корне папки autotests
) действительно ускорилось. Делался замер 2019-08-12, тесты выполнялись примерно 35 минут 10 секунд, и сегодня (2019-09-03), примерно 21 минуту 10 секунд. (Замер выполнялся путём создания скриншотов, каждые 10 секунд, соответственно, было сделано 211 и 127 скриншотов.)
Оказалось, что сделать Hash
полностью (и как динамический модуль, и удалить из slim-префикса) оказалось сравнительно несложно, поэтому сделано всё полностью.
Кроме того, были ускорены автотесты (коммит 235f2def1e03678f2fd3cf3f144d0f19ddba67d4), задача эта нигде явно не описывалась, но я давно хотел её сделать.
По этой задаче выполнено всё. Её можно закрывать.
Коммит выше (41277c2bf6fdc7f8479d528c1d84e72a5770fdba) относится к #167.
Эта задача — подзадача для #76, частично относится к #87.
Цель
Необходимо реализовать генерацию и загрузку модулей типа N — библиотек .dll/.so.
Что уже есть
srefc-core
может генерировать N-модули, поскольку они генерируются почти также, как и I-модули.srmake-core
тоже поддерживает генерацию N-модулей. Вроде как.srmake-core
умеет находить исходники для исполнимых и библиотечных модулей в разных папках.Этого всего уже немало и это неплохо.
Что надо сделать
.def
-файлов компиляторов на Windows, вариантов декорирования имён и т.д.new
/malloc
в одном модуле, должен освобождатьсяdelete
/free
в том же самом модуле — должна поддерживаться возможность безболезненной работы.dll
-ок и модуля типа I, созданных разными компиляторами.Hash
динамическим модулем.(список может пополняться)