Многие программные продукты предоставляют возможность расширить функционал путём установки плагинов, а соответствующая документация и/или статьи сообщества подробно рассказывают, как создать свой первый плагин к %PRODUCT_NAME%. Но не в этой статье. Здесь будут созданы свои велосипеды с плагинами в образовательных целях.
Предисловие
Первая мысль о собственной плагинной системе возникла ещё в четвёртом семестре, на курсе Языков Системного Программирования. Лабораторные работы принимали такие же студенты, как и мы, только на год старше и прошедшие с отличием курс ЯСПа. Ключевым во время сдачи лаб изучить что-то новое, а особо умные студенты, желающие получить оценку выше, сначала получали дополнительное практическое задание. Суть задания сильно зависела от фантазии и жестокости принимающего. На пятой лабораторной (из семи) нужно было принести консольный bmp-редактор, который поворачивал картинку на произвольный угол. Задание на оценку повыше — добавить фильтр «размытие по Гауссу». Тогда я уже прекрасно был знакомы со своим принимающим, и знал, что прописанного в задании к ЛР ему будет мало, и я решил нападать первым. Так зародилась идея вынести все фильтры в плагины и подхватывать их при загрузке редактора.
Требования к окружению
Для комфортной компиляции всех примеров требуются:
- *NIX-подобная система (WSL тоже подходит)
- Компилятор C, в моём случае gcc 6.3.0
- Компилятор С++, в моём случае g++ 6.3.0
- Система сборки, в моём случае GNU Make 4.1
Уровень 1. Учимся подхватывать библиотеки
Нашей первой целью будет незамысловатая программа, которая будет получать список *.so файлов через аргументы командной строки, искать и (в случае успеха) выполнять в них функцию такого вида:
void do_magic(void);
Для этого нам потребуется всего ничего:
dlopen(3)
,
dlsym(3)
,
dlclose(3)
,
dlerror(3)
. Помимо этого, стоит уметь работать с указателями на функции в С.
Начнём с самого простого, с кода плагина. Для первого уровня не нужно заморачиваться: выведем своеобразный «Hello World!».
hello.c
#include <stdio.h>
void do_magic(void) {
printf("Hello from shared object!\n");
}
Само «ядро» состоит лишь из вызовов функций
dl*
в правильном порядке.
core.c
#include <stdio.h>
#include <dlfcn.h>
void plugin_execute(const char* path) {
/* Подгружаем динамическую библиотеку */
void* obj = dlopen(path, RTLD_NOW);
if(obj == NULL) {
fprintf(stderr, "%s\n", dlerror());
return;
}
/* Объявляем указатель на функцию */
void (*f)(void);
/* Получаем адрес на фунцию do_magic() из динамической библиотеки */
f = (void(*)(void))dlsym(obj, "do_magic");
if(f == NULL) {
/* Такой функции в библиотеке нет */
fprintf(stderr, "%s\n", dlerror());
} else {
/* Функция нашлась, выполняем */
f();
}
/* Закрываем открытое */
dlclose(obj);
}
int main(int argc, char* argv[]) {
for(int i = 1; i < argc; i++) {
plugin_execute(argv[i]);
}
return 0;
}
Сборка требует линковки с libdl для core и ключа -shared для hello.so
Команды сборки
gcc -o core core.c -ldl
gcc -o hello.so hello.c -shared
Попробуем запустить:
$ ./core ./hello.c ./hello.so /lib64/ld-linux-x86-64.so.2
./hello.c: invalid ELF header
Hello from shared object!
/lib64/ld-linux-x86-64.so.2: undefined symbol: do_magic
Отлично! Наш плагин был признан системой и функция была выполнена. В кач-ве практического задания, развивающего умения работать со строками в С и курить маны, предлагаю парсить текущий каталог и искать *.so файлы.
Дополнительный вопрос:
Что произойдёт, если в *.so файле не будет
функции do_magic
, а будет
переменная с таким именем?
Уровень 2. Загружаем объект класса.
Сишный код — это просто. Усложним задачу. Теперь мы хотим получать не просто указатель на функцию, а целый экземпляр какого-то класса. Но так как ядро у нас не экстрасенсорное, то нам следует сделать «базу» для всех совместимых с нашей системой плагинов.
base.h
class Base {
public:
virtual void do_magic(void) {};
};
Теперь мы можем наследоваться от этого класса, делать что угодно, зная, что по крайней мере метод
do_magic
будет доступен ядру.
hello.cpp
#include <stdio.h>
#include "base.h"
class Hello : Base {
public:
void do_magic(void) {
printf("Hello from shared object!\n");
}
};
Дело за малым: как-то получить экземпляр класса. Самый простой для нас способ: создать обёртку из сишной функции, которая будет создавать нам объект класса. Для этого стоит воспользоваться заклинанием
extern "C"
hello.cpp
...
extern "C" {
void* setup_plugin(void) {
return new Hello();
}
}
Суть заклинанияБез этого заклинания имена функций декорируются и setup_plugin
выглядит как _Z12setup_pluginv
. Заклинание же убеждает компилятор использовать оригинальное название, как это и происходит в обычном С.
Несложно доказать это, собрав с extern «С» и без него и проверив командой objdump -t file
Теперь адаптируем ядро для получения классов.
core.cpp
void plugin_execute(const char* path) {
...
/* Изменяем указатель на функцию, которая возвращает (void*) */
void* (*f)(void);
/* Здесь так же изменяем приведение типа и имя функции для поиска */
f = (void*(*)(void))dlsym(obj, "setup_plugin");
if(f == NULL) {
fprintf(stderr, "%s\n", dlerror());
} else {
/* Функция нашлась, забираем у неё экземпляр нужного класса */
Base* b = reinterpret_cast<Base*>(f());
b->do_magic();
}
...
}
...
Теперь, когда мы используем в загрузке плагинов классы С++, мы можем позволить себе не сильно заморачиваясь накрутить функционал в виде имени читаемого имени плагина, версии и всего, чего душа пожелает.
Уровень X. Скриптовый язык
При желании можно разработать собственный скриптовый язык, на котором и разрешить писать плагины. Интересный вариант, но разбираться в этой статье он явно не будет.
Заключение
Вот так не очень сложно оказалось делать плагинную систему на языках С и С++. Исходный вариант, который я сдавал на дисциплине ЯСП, имеет более сложную структуру, но так как было ограничение на используемый язык в виде С89 и делалось в ночь перед сдачей, то получилось не очень аккуратно. Тем не менее, исходный код доступен для изучения здесь:
github/Firemoon777/bmp-editor.
Исходники примеров
комментарии (0)