vak: (Default)
[personal profile] vak
Представьте, что вы разрабатываете некую софтину, и хотите разделить её на части. Одна часть по жизни должна оставаться постоянной, а другая часть (или много частей) могут заменяться или подгружаться по потребности. Типичный пример: ядро операционной системы и программы юзера, или графический редактор и его плагины.

Между частями софтины имеется определённый программный интерфейс: обычно набор процедур/функций, заданный на некоем языке, скажем Си. Постоянная часть реализует эти функции и "экспортирует" их для потребления переменными частями. То есть имеем некоторый include-файл, скажем "api.h".

Переменные части, или плагины, удобно компилировать в виде разделяемых библиотек в позиционно независимом коде, чтобы можно было подгрузить и выполнить в любом удобном месте.

Внимание, вопрос: как скомпилировать такой плагин? Ведь его не с чем линковать. У нас нет никакой библиотеки типа "api.so", которую можно было бы подсунуть линкеру.

Разберём на примере. Пусть основной код предоставляет плагину всего одну функцию: tell_user(string). Вот такой файл api.h:
void tell_user(const char *);
Простейший плагин, выдающий юзеру сообщение:
#include "api.h"
void _start()
{
tell_user("Hi!");
}
Пытаемся компилировать, естественно, получаем ошибку:
$ cc -shared -fPIC plugin.c -o plugin.elf
plugin.c: undefined reference to `tell_user'
Линкеру нужно подсунуть некую разделяемую библиотеку api.so, чтобы он увидел там внешний символ tell_user и настроил связи на него. Как сделать такую библиотеку?

Оказывается, есть способ. Компилятор clang умеет извлекать из Си-шных исходников описание интерфейса в формате YAML. А отдельная утилита llvm-ifs может делать из этого описания бинарник разделяемой библиотеки для линкера.

Делаем раз, получаем описание интерфейса:
$ clang -c -emit-interface-stubs api.h
$ cat api.ifs
--- !ifs-v1
IfsVersion: 3.0
Target: x86_64-apple-macosx13.0.0
Symbols:
- { Name: "tell_user", Type: Func }
...
Делаем два, компилируем интерфейс в бинарник api.so:
$ llvm-ifs api.ifs --output-elf=api.so
$ file api.so
api.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), no program header, stripped
Теперь можем скомпилировать наш плагин:
$ cc -shared -fPIC plugin.c api.so -o plugin.elf
$ size plugin.elf
text data bss dec hex filename
677 344 80 1101 44d plugin.elf
Дело сделано, бинарник плагина готов. Теперь можно заняться реализацией собственно основной софтины, загружающей и выполняющей этот бинарник. 

Подробная статья про внутреннее устройство ELF-файла с динамическим связыванием: https://mcuoneclipse.com/2021/06/05/position-independent-code-with-gcc-for-arm-cortex-m/

Date: 2023-04-20 05:22 (UTC)
ircicq: (Default)
From: [personal profile] ircicq
Чем это удобнее dlopen()/dlsym() ?

Date: 2023-04-20 05:57 (UTC)
ircicq: (Default)
From: [personal profile] ircicq
Понятно

Обычно это реализуется через инициализацию плагина Callback-ом:
void InitPlugin(void (*tell_user)(char*));

На Си++ передав указатель на абстрактный класс со всем нужным API.
Edited Date: 2023-04-20 05:59 (UTC)

Date: 2023-04-20 06:37 (UTC)
From: [personal profile] ex0_planet
Оч напоминает виндовый подход где линкер без подобной библиотеки никогда не вдуплял как сгенерировать импорты. Зачем это на elf-системах где линкеру вполне по умолчанию понимает, что в случае -shared unresolved externals являются вполне штатной ситуацией и панику поднимать не нужно...

Date: 2023-04-20 08:11 (UTC)
From: [personal profile] ex0_planet
Ну так они ж не из воздуха берутся, есть какой-то include в котором они описаны и этот include видят и плагин и ядро, прототипы у них одинаковые. Семейство dlopen в этой схеме нужно только чтобы найти точку входа в загружаемый плагин, дальше всю работу сделает динамический линкер. Опечататься в одном имени, конечно, можно, но сложно, и ошибка будет быстро выловлена.

Date: 2023-04-20 07:10 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi

Ничего себе автоматизировали. Я бы в этом плагине требовал передачи инстанса, где это tell_user имплементировано.

Date: 2023-04-20 14:22 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi

Я имею в виду, что какой-то инстанс нужен же. Но в си какой-то другой менталитет. Не лямбда.

Date: 2023-04-20 20:31 (UTC)
juan_gandhi: (Default)
From: [personal profile] juan_gandhi

С точки зрения S-комбинатора это звучит очень необычно. Может быть, оттуда и все эти странные идеи насчет DI идут.

Date: 2023-04-20 17:31 (UTC)
From: [personal profile] caztd
Microsoft называет это "import library" (суффикс .lib).
Но их вроде как нельзя из интерфейса сгенерировать.

Date: 2023-04-20 21:09 (UTC)
ircicq: (Default)
From: [personal profile] ircicq
.lib генерируется из экспорта .EXE/.DLL примерно так:

dumpbin.exe /exports foo.dll >foo.def
lib.exe /def:foo.def /out:foo.lib

То есть достаточно экспортировать нужный API в главном приложении.




Date: 2023-04-20 21:11 (UTC)
From: [personal profile] caztd
Да, но нужно приложение. Интерфейса (.h файла) недостаточно.

Date: 2023-04-20 22:04 (UTC)
ircicq: (Default)
From: [personal profile] ircicq
несложно скриптом преобразовать .h -> .def
особенно для Си где нет mangled function names

Date: 2023-04-20 22:07 (UTC)
From: [personal profile] caztd
Я подозреваю это как раз примерно то же, что "clang -c -emit-interface-stubs" делает.

Date: 2023-04-22 17:02 (UTC)
From: [personal profile] h1uke
vak, а оно умеет только _функции_ в yaml экспортировать, или также структуры данных? Мне просто приходится по работе иметь дело с множеством С-шных структур, спроектированных безо всякого представления о выравнивании, мобильности данных и т.п. Было бы неплохо получить возможность автоматически готовить подпрограммы распечатки содержимого эттх структур, и, возможно, какие-то заготовки для "бланкового ввода" в них. Другими словами, "poor man" reflection. Для gcc уже создан вполе толковый инструмент - pahole, но вокруг него еще слесарить надо ...

Date: 2023-04-23 07:44 (UTC)
x86128: (Default)
From: [personal profile] x86128
DLL-hell начало. :)