Closed Mazdaywik closed 4 years ago
Если модуль уже загружен, то повторно загружать его не надо. Возникает вопрос: а как определять повторную загрузку? По имени файла нельзя: один и тот же файл может быть доступен по разным путям: в путь можно добавить несколько раз ./././
, можно добавлять пути «вверх» типа otherdir/../
, на платформе Windows можно использовать разный регистр символов и чередовать знаки /
и \
.
Предлагается идентифицировать файлы по идентификатору устройства и идентификатору файла на устройстве. На POSIX эти параметры можно получить при помощи системного вызова stat
и полей st_dev
и st_ino
:
http://www.opennet.ru/man.shtml?topic=stat&category=2&russian=2 http://www.opennet.ru/man.shtml?topic=stat&category=3&russian=5
На Windows аналогичную информацию можно получить при помощи GetInformationByHandle
(поля с более длинными именами dwVolumeSerialNumber
, nFileIndexHigh
, nFileIndexLow
):
https://technet.microsoft.com/ru-ru/office/aa364952(v=vs.110) https://technet.microsoft.com/ru-ru/office/aa363788(v=vs.110)
Вариант платформенно-независимого интерфейса для этой цели:
namespace refalrts::api {
struct stat;
const stat *stat_create(const char *filename);
signed stat_compare(const stat *left, const stat *right);
void stat_destroy(const stat *stat);
}
Соответственно, в структуре refalrts::api::stat
будет находиться BY_HANDLE_FILE_INFORMATION
для Windows и struct stat
для POSIX. Функция stat_compare
будет производить сравнение двух структур, возвращая -1
, 0
или +1
. Если файла не существует или произошла ошибка, stat_create
будет возвращать NULL
. Поэтому эту функцию можно использовать для более точной реализации ExistFile
.
Если указан полный путь, то модуль загружается по полному пути. Ну, почти (см. про LOAD_WITH_ALTERED_SEARCH_PATH
далее).
Если указано только имя без пути и расширения, то по умолчанию добавляется расширение .dll
. Чтобы предотвратить автоматическое добавление .dll
, имя файла должно заканчиваться на точку.
Если задано только имя модуля (возможно, с расширением), то сначала производится поиск среди загруженных ранее модулей — ищется модуль с совпадающим именем и расширением.
Если путь не указан или указан относительный путь, то выполняется процедура поиска. Процедура поиска весьма витиевата, зависит от кучи флагов и настроек среды. При загрузке функцией LoadLibraryEx
можно указать LOAD_WITH_ALTERED_SEARCH_PATH
. Можно в реестре установить SafeDllSearchMode
(по умолчанию установлено, начиная с Windows XP SP2). Также может быть установлен дополнительный путь поиска при помощи SetDllDirectory
. Рассмотрим разные варианты.
! LOAD_WITH_ALTERED_SEARCH_PATH && SafeDllSearchMode
%WINDIR%\system32
).%WINDIR%
).%PATH%
.! LOAD_WITH_ALTERED_SEARCH_PATH && ! SafeDllSearchMode
%PATH%
.LOAD_WITH_ALTERED_SEARCH_PATH && SafeDllSearchMode
LOAD_WITH_ALTERED_SEARCH_PATH
активизируется, только имя модуля задано полным путём.
%PATH%
.LOAD_WITH_ALTERED_SEARCH_PATH && ! SafeDllSearchMode
%PATH%
.SetDllDirectory()
SetDllDirectory()
.%PATH%
.LOAD_LIBRARY_SEARCH_…
Мне их лень расписывать.
Как говорится, понять это невозможно, можно только запомнить.
Согласно POSIX, если dlopen()
видит /
в строке имени файла, то он имя файла интерпретирует как путь, абсолютный или относительный. Иначе запускается платформенно-специфичная процедура поиска.
Процедура поиска на Linux включает следующие шаги:
DT_RPATH
и не содержит тега DT_RUNPATH
, то осуществляется поиск библиотеки в каталогах, перечисленных в DT_RPATH
.LD_LIBRARY_PATH
(данный пункт не применим к программам с битами SETUID и SETGID).DT_RUNPATH
, то осуществляется поиск в папках, перечисленных в этом теге./lib
, потом /usr/lib
.Параметр DT_RPATH
считается deprecated, видимо, потому что считается кошернее сначала смотреть в LD_LIBRARY_PATH
.
В отличие от Windows поиск в явном виде не включает ни текущей папки, ни папки исполнимого файла. Потому что если конкретному приложению нужен поиск в текущей папке, оно может положить .
в LD_LIBRARY_PATH
(перед запуском) или в DT_RPATH
/DT_RUNPATH
(во время компиляции). А для поиска относительно каталога исполнимого файла есть более гибкий вариант — строки, заданные в DT_RPATH
/DT_RUNPATH
, могут содержать подстроку $ORIGIN
или ${ORIGIN}
, которая загрузчиком заменяется на путь к исполнимому файлу. Пример из man:
Thus, an application located in
somedir/app
could be compiled withgcc -Wl,-rpath,'$ORIGIN/../lib'
so that it finds an associated shared object in
somedir/lib
no matter where somedir is located in the directory hierarchy.
Предлагаемый подход частично комбинирует подходы Windows и Linux. Переменную $ORIGIN
мы не берём, поскольку она сравнительно сложна в реализации. А значит, в путях поиска потребуется в явном виде каталог приложения.
Да и вообще, переменных, аналогичных DT_RPATH
или DT_RUNPATH
, предусматривать пока не будем. Они потребуют изменения формата RASL’а.
Стандартных путей для библиотек (типа %WINDIR%\System32
или /usr/lib
предусматривать также не будем — компилятор может быть установлен куда угодно. Поэтому остаются только путь до исполнимого файла, переменная окружения и текущая папка.
Примечание. Возможно, в будущих версиях появятся стандартные пути для библиотек, а также аналоги DT_RPATH
или DT_RUNPATH
.
Очень удобным видится подход POSIX, когда поиск осуществляется только для имён без пути. Если имя файла содержит /
, то оно считается путём.
Поэтому предлагается следующий алгоритм.
Имя модуля может заканчиваться на суффикс .rasl-module
(файл без нативного кода) или платформенно-зависимый суффикс библиотеки (.dll
или .so
).
/
, на Windows содержит /
или \
или начинается с буквы диска (^[A-Za-z]:
).RL_LIB
. Папки на платформе Windows разделяются знаком ;
, на POSIX — знаком :
..rasl-module
, затем с платформенно-зависимым расширением библиотеки.lib
папки установки, а затем из точки (текущей папки). На Windows:
set RL_MODULE_PATH=C:\path\to\refal-5-lambda\lib;.
на POSIX:
export RL_MODULE_PATH=/path/to/refal-5-lambda/lib:.
Дополнение к предыдущему комментарию.
На Windows есть функция GetModuleHandle
, которая принимает имя модуля и возвращает дескриптор ранее загруженного модуля с тем же именем. Если имя задано как NULL
, возвращается дескриптор исполнимого файла. Функции LoadLibrary
и LoadLibraryEx
вместо имени файла NULL
принимать не могут.
На Linux dlopen
может принимать NULL
вместо имени файла с той же семантикой — возвращает дескриптор исполнимого файла.
Разумно сохранить подобную семантику для загружаемых модулей Рефала.
Мы имеем некоторый файл без стандартного расширения. Как определить, нативный ли он. Нужно ли использовать функции операционной системы (dlopen
, LoadLibrary
) для попытки загрузки файла?
Можно пытаться это делать всегда. Если загрузка прошла успешно, то значит, что модуль нативный. Если нет — только с RASL. Эвристика: если код RASL начинается с нулевого смещения, значит нативного кода в файле заведомо нет.
Но предлагается другой вариант. Если в модуле есть как минимум одна нативная функция, то делается попытка загрузить модуль как нативный. Если нет — попытки не делается, даже если он нативный на самом деле.
«Возможно, стоит обеспечить возможность запуска штатным интерператором (типа refgo
) и таких модулей. Например, для запуска из-под Linux модулей с префиксом для Windows.»
К этой задаче не относится. Относится к задаче #48.
«Бинарное API библиотек ОС должно быть задокументировано и позволять создавать модули нативных функций, пользуясь другими языками (например, Delphi).»
Пока не надо. В связи со #197 API будет меняться, а значит, документировать пока не своевременно.
«Значение переменной среды по умолчанию (должно быть задано при установке) состоит из пути к подпапке lib
папки установки, а затем из точки (текущей папки). На Windows:
set RL_MODULE_PATH=C:\path\to\refal-5-lambda\lib;.
на POSIX:
export RL_MODULE_PATH=/path/to/refal-5-lambda/lib:.
»
Реализовано как путь к папке не /lib
, а /bin
, точки в конце нет. Необходимость в точке (текущей папке) не очевидна, а библиотеки действительно разумно перенести в /lib
«На Windows есть функция GetModuleHandle
, которая принимает имя модуля и возвращает дескриптор ранее загруженного модуля с тем же именем. Если имя задано как NULL
, возвращается дескриптор исполнимого файла. Функции LoadLibrary
и LoadLibraryEx
вместо имени файла NULL
принимать не могут.
На Linux dlopen
может принимать NULL
вместо имени файла с той же семантикой — возвращает дескриптор исполнимого файла.
Разумно сохранить подобную семантику для загружаемых модулей Рефала.»
Не поддерживается. Функция Module-Load
при пустом имени файла выдаёт ошибку. Частично это нивелируется возможностью указать GLOBAL
, CURRENT
или CURRENT-AND-GLOBAL
вместо дескриптора в функциях Module-Mu
, Module-LookupFunction
и Module-FunctionPtr
. Если потребуется именно получать реальный дескриптор текущего модуля — тогда и реализую означенную семантику (например, с пустым именем для Module-Load
).
«Ленивая загрузка нативных модулей»
Неожиданно, реализовано:
/bin
в /lib
.RL_MODULE_PATH
, но не сообщает об этом при установке — изменить комментарий архива.curl … | bash
) устанавливает только PATH
, не устанавливая RL_MODULE_PATH
. Надо уточнить.Все дочерние задачи для текущей сделаны, все ссылки выделены красным. Остались только эти три пункта.
RL_MODULE_PATH
должна использоваться папка /lib
дерева исходников, а не глобальная RL_MODULE_PATH
. Либо вообще не требоваться.RL_MODULE_PATH
в пустоту, то раскрутить библиотеки не удаётся — компилятор почему-то пытается подгрузить Hash
и не находит. Причём, непонятно почему — он должен использовать префикс rich
, в котором Hash
уже есть.Проблема перестала воспроизводиться.
Эта задача — родительская для нескольких подзадач. В ней будет описана общая концепция динамической загрузки, детали реализации будут в подзадачах.
Задача динамической загрузки
Программа на Рефале есть набор функций. Следует реализовать механизм, позволяющий создавать готовую для выполнения программу, функции которой могут находиться в нескольких отдельных файлах. Один из файлов (содержащий функцию
Go
) должен быть либо исполнимым файлом операционной системы, либо файлом интерпретируемого кода (см. #48) — будем называть его исполнимым модулем. Остальные файлы должны быть либо файлами интерпретируемого кода, либо библиотеками динамической загрузки операционной системы (но они также могут содержать интерпретируемый код) — будем называть их подключаемыми модулями. Исполнимые и подключаемые модули будем называть общим словом скомпилированные модули.Скомпилированные модули могут зависеть от других подключаемых модулей — загрузка таких модулей должна вызывать загрузку зависимых модулей. Следствие: формат модулей должен содержать ссылки на зависимые модули.
Компилятор должен уметь порождать модули четырёх типов:
Go
в экспорте. Для запуска таких исполнимых модулей нужен самостоятельный интерпретатор (наподобиеrefgo
). Исходные тексты для этого модуля не могут содержать нативных функций. Для создания этих файлов компилятор C++ не требуется. Будем их называть модулями типа R.refgo
) и таких модулей. Например, для запуска из-под Linux модулей с префиксом для Windows. Исходные тексты для этого модуля не могут содержать нативных функций. Для создания этих файлов компилятор C++ не требуется. Будем называть их модулями типа I0 («и-ноль»).Требования:
Подходы
Динамические библиотеки ОС (
.dll
или.so
) экспортируют только сущности одного вида — функции в Windows, символы в Linux. При этом на уровне средств ОС типы функций и символов не определены — их соответствие должен контролировать сам программист. (Да, компиляторы C++ поддерживают декорирование имён, но базовые механизмы — ОС Windows или загрузчикиld-linux.so
иlibdl
— о них ничего не знают).Аналогично, подключаемые модули Рефала-5λ тоже будут экспортировать только сущности одного вида — функции Рефала. Поскольку контроля типов функций в языке нет, то и экспортируемые функции также будут иметь одинаковый вид.
Далее будут рассмотрены сходства и отличия подходов Windows и Linux при реализации динамических библиотек. Реализация динамических библиотек в других вычислительных средах намеренно не рассматривалась (т.к. у меня (@Mazdaywik’а) опыта работы с ними нет).
Общее подходов Linux и Windows
Обе реализации динамических библиотек позволяют исполнимому модулю иметь экспортируемые функции, которые могут импортироваться из библиотек.
Оба подхода позволяют иметь в библиотеке перехватчики загрузки и выгрузки (
DllMain()
и_init()
/_fini()
).Загружаемые библиотеки имеют счётчик связей — при повторной загрузке библиотеки с тем же именем счётчик инкрементируется, библиотека выгружается, когда счётчик дошёл до нуля.
Подход Windows
В Windows динамические библиотеки являются пространствами имён. Если в библиотеках
foo.dll
иbar.dll
одновременно присутствует функцияHello
, то конфликта не будет — в таблицах импорта для каждой библиотеки указываются перечисляются функции, которые из неё импортируются. Хотя, написать приложение, которое одновременно использует две одноимённые функции из разных библиотек, подключаемых статически, скорее всего, будет нетривиальной задачей.Чтобы функцию экспортировать из библиотеки, её нужно добавить в
.def
-файл или пометить нестандартным атрибутом__dllexport
(или__declspec(dllexport)
, или__attribute__((dllexport))
— точный вид зависит от используемого компилятора Си. Функции, импортируемые из библиотек, помечаются нестандартным атрибутом__dllimport
(синтаксис тоже может быть разным), который, вроде как, необязателен.Библиотека может содержать функцию
DllMain()
, которая будет вызываться при (а) загрузке библиотеки, (б) при выгрузке библиотеки, (в) при создании или уничтожении потока в процессе. Важной особенностью, о которой нужно помнить, является то, что внутри этой библиотеки нельзя использовать сишный рантайм и немалую часть WinAPI — это ведёт к неопределённому поведению. ИзDllMain()
вызываются конструкторы и деструкторы глобальных объектов — при проектировании библиотеки это нужно учитывать.Подход Linux
В Linux библиотеки и функции независимы: если исполнимый файл имеет неразрешённые (unresolved) ссылки на функции
Hello
иWorld
, а также требует загрузки библиотекlibfoo.so
иlibbar.so
, то на этапе запуска приложения совершенно не важно, из какой из библиотек будет импортирована какая функция. Если две библиотеки экспортируют функции с одинаковыми именами, то скомпилированные модули будут связаны с той из них, которая загрузилась раньше.Из библиотек экспортируются все символы, не помеченные как
static
в языке Си, импортируются все, которые линковщик не увидел в текущем модуле. Однако, если компилируется исполнимый файл, линковщик обязательно проверит, что в библиотеках, указанных в списке зависимостей, присутствуют неразрешённые в исполнимом файле символы. Если создаётся разделяемый объектный файл, такой проверки не делается.Однако, загрузка позволяет более тонко управлять видимостью имён. В глобальную область видимости помещаются функции, загруженные из разделяемых объектных файлов при запуске, а также экспортируемые функции из exe-файла, если тот слинкован с флагом
-rdynamic
или--export-dynamic
. Функция загрузки модуляdlopen()
поддерживает следующие флаги, управляющие видимостью:RTLD_LOCAL
(по умолчанию) иRTLD_GLOBAL
— первый предписывает не добавлять в глобальную область видимости имена, экспортируемые из данной библиотеки и зависимых от неё библиотек. Второй предписывает добавлять.RTLD_DEEPBIND
— поместить символы из этой библиотеки перед глобальными символами с теми же именами, позволяет переопределить функции из ранее загруженных библиотек.Поиск внешних ссылок при загрузке библиотеки осуществляется в списке зависимостей библиотеки и в ранее загруженных библиотеках с флагом
RTLD_GLOBAL
. Если исполнимый файл собран с ключом-rdynamic
(или с--export-dynamic
), то глобальные символы исполнимого файла также помещаются в глобальное пространство имён.Библиотека может иметь функции
_init()
и_fini()
, которые, соответственно, вызываются внутриdlopen()
иdlclose()
, однако, они считаются устаревшими. Вместо них следует использовать функции, помеченные атрибутами__attribute__((constructor))
и__attribute__((destructor))
.Выбранный подход
Выбирается подход, аналогичный подходу Linux, потому что он проще.
static
в Си. Импортируемые — те, которые были объявлены и не разрешены при компоновке (хотя для исполнимых файлов линковщик проверяет их наличие в зависимых библиотеках).Этапы
srefc
иsrmake
, а также скрипт выполнения автотестов не заметят разницы). Но уже будет выбран формат RASL, будут реализованы функции его разбора и загрузки в память. Подзадача #77.srefc
,srmake
, скрипт выполнения тестов, возможно, утилитуsrmake
(та, что-core
). Подзадача #89.srmake
, скрипт автотестов) — но код должен будет остаться кроссплатформенным (по большей части). Подзадача #90.Первым этапом является поддержка модулей типа I+, последним — N, второй и третий могут выполняться в любом порядке.