Pull to refresh

Создание и использование динамических библиотек, написанных на различных языках (C/C++, Pascal)

Reading time5 min
Views81K

Задача


Передо мной возникла задача написать загрузчик библиотек, имеющий возможность предоставить какие-то интерфейсные функции внешней динамической библиотеке. Решение должно быть максимально кроссплатформенно (как минимум, работать на 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++).

Литература


Tags:
Hubs:
Total votes 41: ↑35 and ↓6+29
Comments9

Articles