Добавление модулей расширения (плагинов) к программе.

0. Введение
Прошли те времена, когда программы создавались как нечто законченное, не имеющее возможности для расширения. Сегодня от программ требуется большая универсальность и возможность расширения. Самый простой способ увеличения гибкости и расширяемости программы заключается в добавлении поддержки дополнительных модулей — плагинов (от англ. plugin, прим. перев.). В качестве примеров программ с поддержкой дополнительных модулей (плагинов) можно назвать WEB-браузеры и медиапроигрыватели. В браузерах плагины обеспечивают поддержку Java, Flash и QuickTime, внедренных в WEB-страницы. В медиапроигрывателях, таких как XMMS, с помощью плагинов выполняется поддержка воспроизведения файлов различных форматов, визуальных эффектов и т.д.. Цель этой статьи — расказать о том, как организовать поддержку сменных модулей — плагинов в ваших программах. Маленькое замечание: в пределах этой статьи я использую слова «модуль» и «плагин» как взаимозаменяемые понятия.

1. Работа с плагинами
В распоряжении разработчика имеется библиотека dl (Dynamic Loader — Динамический Загрузчик), которая предоставляет всего четыре функции. Здесь я дам лишь краткое описание этих функций. За более подробной информацией обращайтесь к справочному руководству — man.

dlopen
Производит загрузку модуля в память.
dlclose
Выгружает модуль из памяти.
dlsym
Возвращает адрес искомой функции в модуле.
dlerror
Возвращает сообщение об ошибке, которая могла возникнуть при вызове dlopen и dlsym.
2. Пример простой программы с поддержкой плагинов.
Ниже показан код программы loader, которая принимает название плагина как аргумент командной строки.

main.c
та же программа в виде отдельного файла

#include <unistd.h>

#include <string.h>

#include <errno.h>

#include <dlfcn.h>

#define PATH_LENGTH 256

int main(int argc, char * argv[])

{

char path[PATH_LENGTH], * msg = NULL;

int (*my_entry)();

void * module;

/* сборка имени модуля и полного пути к нему в одну строку */

getcwd(path, PATH_LENGTH);

strcat(path, «/»);

strcat(path, argv[1]);

/* загрузка модуля и разрешение имен перед возвратом из dlopen */

module = dlopen(path, RTLD_NOW);

if(!module) {

msg = dlerror();

if(msg != NULL) {

dlclose(module);

exit(1);

}

}

/* попытка получить адрес функции «entry» */

my_entry = dlsym(module, «entry»);

msg = dlerror();

if(msg != NULL) {

perror(msg);

dlclose(module);

exit(1);

}

/* вызов функции «entry» в модуле */

my_entry();

/* close module */

if(dlclose(module)) {

perror(«error»);

exit(1);

}

return 0;

}

Этот пример достаточно прост. После загрузки модуля, функция dlsym, по таблице имен модуля, отыскивает адрес функции «entry» в модуле. Адрес функции запоминается в локальной переменной, после чего эта функция вызвается на исполнение. Затем модуль выгружается из памяти. Объявление указателя на функцию, возможно нуждается в дополнительном пояснении.
int (*my_entry)()
объявляет указатель на функцию, не имеющую входных параметров и возвращающую результат типа int. В данном примере в указателе запоминается адрес функции «entry» в модуле:
int entry()

Сборка программы выполняется командой:

$ gcc -o loader main.c -ldl

3. Два простых модуля расширения (плагина)
Теперь, когда у нас уже есть программа, поддерживающая модули расширения, можно создать несколько плагинов. Нет никаких ограничений, накладываемых на функции в модуле. В своем примере я объявляю функции, не имеющие входных параметров, и возвращающие результат типа int. Вы можете объявлять свои функции со своим набором входных параметров и возвращаемым значением, требуемого вам типа. Совсем не обязательно давать функциям имена «entry». Я использую это имя лишь для простоты восприятия. Кроме того, в модуль может быть включено значительно большее число функций. Ниже приведен пример исходных текстов двух простых модулей, в каждом из которых определена функция с именем «entry»:

module1.c
текст модуля в виде отдельного файла

int entry()

{

printf(«Я — первый модуль!\n»);

return 0;

}

module2.c
текст модуля в виде отдельного файла

int entry()

{

printf(«Я — второй модуль!\n»);

return 0;

}

Компиляция модулей:

$ gcc -fPIC -c module1.c

$ gcc -shared -o module1.so module1.o

$ gcc -fPIC -c module2.c

$ gcc -shared -o module2.so module2.o

Несколько замечаний по компиляции. Во-первых, флаг -fPIC’ («Position Independent Code») сообщает компилятору о необходимости относительной (от англ. relative) адресации. Это означает, что скомпилированный код может быть размещен в любой области памяти, а загрузчик сам «побеспокоится» об адресах во время загрузки модуля. Во-вторых, флаг -shared’ (общедоступный, разделяемый) говорит компилятору о том, что этот код должен быть собран таким образом, чтобы было возможно связать его с любым другим исполняемым кодом. Другими словами .so — файлы (shared object) ведут себя подобно библиотекам, только не могут быть связаны с программой с помощью ключа компиляции -l’ (да простит меня читатель за подобное сравнение, но *.so файлы очень напомнают мне динамически загружаемые библиотеки *.dll в операционной системе MS Windows. прим. перев.).

4. Запуск программы Loader
Ниже показан пример запуска нашей программы loader и результат ее выполнения:

$ ./loader module1.so

Я — первый модуль!

$ ./loader module2.so

Я — второй модуль!

5. Функции инициализации и финализации плагина
Содержание этого раздела предполагает использование специфических особенностей компилятора gcc. Если вы используете другой компилятор, то вам следует обратиться к документации за разрешением проблем совместимости.

Ключевое слово __attribute__’ позволяет определить массу полезных атрибутов для функций, однако я остановлюсь только на двух из них — constructor’ и destructor’. За дополнительной информацией по атрибутам обращайтесь к info gcc. Формат ELF (Executable and Linkable Format — формат исполняемых и связываемых модулей) предполагает наличие двух секций — .init и .fini, в которых может содержаться код, исполняемый до и после загрузки модуля (для обычных программ это означает — «до и после исполнения функции main()»). Код, размещаемый в этих секциях может выполнять действия по инициализации переменных модуля, выделению/освобождению ресурсов и пр.. Например, модуль может иметь ряд переменных, определяющих правила взаимодействия с программой, значения которых считываются из главной программы сразу после загрузки модуля. Эти переменные могут содержать точки входа (команды), которые поддерживаются плагином. В моем примере модули имеют лишь по одной точке входа — функции «entry», вы можете определить большее количество функций. Ниже приведен пример использования атрибутов:

__attribute__ ((constructor)) void init()

{

/* этот код вызывается сразу после загрузки модуля функцией dlopen() */

}

__attribute__ ((destructor)) void fini()

{

/* этот код вызывает непосредственно перед выгрузкой модуля функцией dlclose() */

}

Имена init() и fini() не являются обязательными, я использую их лишь для большей ясности понимания назначения этих функций. Однако имеется ряд имен, зарезервированных gcc. Вот некоторые из них — _init, _fini, _start и _end. Полный список имен функций и переменных в модуле можно посмотреть с помощью утилиты nm. Атрибуты constructor’ и destructor’ сообщают компилятору о том, что этот код должен располагаться в секциях .init и .fini соответственно.

6. Заключение
Библиотека dl делает поддержку сменных модулей — плагинов в программе достаточно простой задачей. Приведенный здесь пример демонстрирует возможность импорта единственной функции из плагина, но он может быть легко распространен на случай значительно большего количества функций и обращения к ним так, как будто они являются частью превоначальной программы.

Источник:  http://www.pcclub.pp.ua