СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

H Разработка плагинной системы на С и С++ в черновиках Tutorial

Многие программные продукты предоставляют возможность расширить функционал путём установки плагинов, а соответствующая документация и/или статьи сообщества подробно рассказывают, как создать свой первый плагин к %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)