Задача
Передо мной возникла задача написать загрузчик библиотек, имеющий возможность предоставить какие-то интерфейсные функции внешней динамической библиотеке. Решение должно быть максимально кроссплатформенно (как минимум, работать на Linux и Windows). Загружаться должны библиотеки, написанные на различных языках программирования, поддерживающих создание динамических библиотек. В качестве примера были выбраны языки C и Pascal.
Решение
Основной загрузчик библиотек написан на языке C. Для того, чтобы загружаемые библиотеки имели возможность использовать функции основной программы, основная программа разделена на 2 части: на основной и подгружаемый модули. Основной модуль нужен просто для запуска программы, подгружаемый модуль — это также динамическая библиотека, связываемая с основным модулем во время его запуска. В качестве компиляторов были выбраны gcc (MinGW для Windows) и fpc.
Здесь будет приведён упрощённый пример программы, позволяющий разобраться в данном вопросе и учить первокурсников писать модули к своей программе (в школе часто преподают именно Pascal).
Загрузчик библиотек
Главный файл загрузчика библиотек выглядит очень просто:
main.c
#include "loader.h"
#ifdef __cplusplus
extern "C" {
#endif
int main(int argc, char *argv[]) {
if (argc > 1) {
loadRun(argv[1]);
}
return 0;
}
#ifdef __cplusplus
}
#endif
А это модуль, отвечающий за загрузку динамических библиотек, который сам вынесен в динамическую библиотеку для того, чтобы подгружаемые библиотеки имели возможность использовать предоставляемые им функции:
loader.c
#include "loader.h"
#include "functions.h"
#include <stdio.h>
#ifndef WIN32
#include <dlfcn.h>
#else
#include <windows.h>
#endif
#ifdef __cplusplus
extern "C" {
#endif
void printString(const char * const s) {
printf("String from library: %s\n", s);
}
void loadRun(const char * const s) {
void * lib;
void (*fun)(void);
#ifndef WIN32
lib = dlopen(s, RTLD_LAZY);
#else
lib = LoadLibrary(s);
#endif
if (!lib) {
printf("cannot open library '%s'\n", s);
return;
}
#ifndef WIN32
fun = (void (*)(void))dlsym(lib, "run");
#else
fun = (void (*)(void))GetProcAddress((HINSTANCE)lib, "run");
#endif
if (fun == NULL) {
printf("cannot load function run\n");
} else {
fun();
}
#ifndef WIN32
dlclose(lib);
#else
FreeLibrary((HINSTANCE)lib);
#endif
}
#ifdef __cplusplus
}
#endif
Заголовочные файлы
Это всё была реализация, а теперь заголовочные файлы.
Вот интерфейс модуля, загружающего динамические библиотеки, для основного модуля:
loader.h
#ifndef LOADER_H
#define LOADER_H
#ifdef __cplusplus
extern "C" {
#endif
extern void loadRun(const char * const s);
#ifdef __cplusplus
}
#endif
#endif
Вот интерфейс загрузчика для загружаемых им динамических библиотек (перечень функций, которые динамические библиотеки могут использовать в основной программе):
functions.h
#ifndef FUNCTIONS_H
#define FUNCTIONS_H
#ifdef __cplusplus
extern "C" {
#endif
extern void printString(const char * const s);
#ifdef __cplusplus
}
#endif
#endif
Как видно, здесь всего одна функция printString для примера.
Компиляция загрузчика
Пример недистрибутивной компиляции (в случае Windows в опции компилятора просто нужно добавить -DWIN32):
$ gcc -Wall -c main.c
$ gcc -Wall -fPIC -c loader.c
$ gcc -shared -o libloader.so loader.o -ldl
$ gcc main.o -ldl -o run -L. -lloader -Wl,-rpath,.
Дистрибутивная компиляция от недистрибутивной отличается тем, что в дистрибутивном случае динамические библиотеки ищутся в /usr/lib и имеют вид lib$(NAME).so.$(VERSION), в случае недистрибутивной компиляции они называются lib$(NAME).so, а ищутся в каталоге запуска программы.
Теперь посмотрим, что у нас получилось после компиляции:
$ nm run | tail -n 2
U loadRun
08048504 T main
$ nm libloader.so| tail -n 4
000005da T loadRun
000005ac T printString
U printf@@GLIBC_2.0
U puts@@GLIBC_2.0
Тут видим, что функции, отмечаемые как U ищутся во внешних динамических библиотеках, а функции, отмечаемые как T предоставляются модулем. Это бинарный интерфейс программы (ABI).
Динамические библиотеки
Теперь приступим к описанию самих динамических библиотек.
Библиотека на языке C
Вот пример простейшей библиотеки на языке C:
lib.c
#include "functions.h"
#ifdef __cplusplus
extern "C" {
#endif
void run(void) {
printString("Hello, world!");
}
#ifdef __cplusplus
}
#endif
Здесь везде окружение extern «C» {} нужно для того, чтобы нашу программу можно было компилировать при помощи C++-компилятора, такого, как g++. Просто в C++ можно объявлять функции с одним и тем же именем, но с разной сигнатурой, соответственно в этом случае используется так называемая декорация имён функций, то есть сигнатура функций записывается в ABI. Окружение extern «C» {} нужно для того, чтобы не использовалась эта декорация (тем более, что эта декорация зависит от используемого компилятора).
Компиляция
$ gcc -Wall -fPIC -c lib.c
$ gcc -shared -o lib.so lib.o
ABI:
$ nm lib.so | tail -n 2
U printString
0000043c T run
Запуск:
$ ./run lib.so
String from library: Hello, world!
Если мы уберём в нашем модуле окружение extern «C» {} и скомпилируем при помощи g++ вместо gcc, то увидим следующее:
$ nm lib.so | grep run
0000045c T _Z3runv
То есть, как и ожидалось, ABI библиотеки изменился, теперь наш загрузчик не сможет увидеть функцию run в этой библиотеке:
$ ./run lib.so
cannot load function run
Библиотека на языке Pascal
Как мы увидели выше, для того, чтобы наш загрузчик видел функции в динамических библиотеках, созданных компилятором C++, пришлось дополнять наш код вставками extern «C» {}, это притом, что C/C++-компиляторы и языки родственные. Что уж говорить про компилятор FreePascal совершенно другого языка — Pascal? Естественно, что и тут без дополнительных телодвижений не обойтись.
Для начала нам нужно научиться использовать экспортированные в C функции для динамических библиотек. Вот пример аналогичного C/C++ заголовочного файла на языке Pascal:
func.pas
unit func;
interface
procedure printString(const s:string); stdcall; external name 'printString';
implementation
end.
Вот пример самого модуля на языке Pascal:
modul.pas
library modul;
uses
func;
procedure run; stdcall;
begin
printString('Hello from module!');
end;
exports
run;
begin
end.
Компиляция
$ fpc -Cg modul.pas
Компилятор Free Pascal версии 2.5.1 [2011/02/21] для i386
Copyright (c) 1993-2010 by Florian Klaempfl
Целевая ОС: Linux for i386
Компиляция modul.pas
Компоновка libmodul.so
/usr/bin/ld: warning: link.res contains output sections; did you forget -T?
/usr/bin/ld: warning: creating a DT_TEXTREL in a shared object.
13 строк скомпилиpовано, 6.6 сек
Смотрим ABI получившейся библиотеки:
$ nm libmodul.so
U printString
000050c0 T run
Как видим, ничего лишнего, однако настораживает предупреждение ld во время компиляции. Находим в гугле возможную причину предупреждения, это связано с компиляцией без PIC (Position Independent Code — код не привязан к физическому адресу), однако в man fpc находим, что наша опция -Cg должна генерировать PIC-код, что само по себе странно, видимо fpc не выполняет своих обещаний, либо я делаю что-то не так.
Теперь попробуем убрать в нашем заголовочном файле кусок name 'printString' и посмотрим, что выдаст компилятор теперь:
$ nm libmodul.so
U FUNC_PRINTSTRING$SHORTSTRING
000050d0 T run
Как видно, декорация в FreePascal совсем другого рода, чем в g++ и тоже присутствует.
При запуске с этим модулем получаем:
$ ./run libmodul.so
./run: symbol lookup error: ./libmodul.so: undefined symbol: FUNC_PRINTSTRING$SHORTSTRING
А с правильным модулем получаем:
$ ./run libmodul.so
String from library: Hello from module!
Вот и всё, своей задачи — использование динамических библиотек, написанных на различных языках — мы достигли, и наш код работает как под Linux, так и под Windows (у самого Mac'а нет, поэтому не смотрел, как там с этим). Кроме того, загруженные библиотеки имеют возможность использовать предоставляемые в основной программе функции для взаимодействия с ней (то есть являются плагинами основной программы).
Сам загрузчик может выполняться в отдельном процессе, чтобы эти плагины не смогли сделать ничего лишнего с основной программой. Соответственно, основная программа может быть написана на любом другом удобном языке программирования (например, на Java или на том же C++).