Для початку коротко підсумую висновки зроблені у попередньому пості.
- C++ не надає ніяких механізмів динамічного завантаження коду;
- Динамічне завантаження коду можливе у мові C;
- Для створення екземплярів плаганів використовуються функції-фабрики, а для уравління - абстрактний інтерфейс;
- Щоб уникнути name mangling функція-фабрика помічається як export "C", при цьому цілком і повністю втрачається інформація про тип функції;
- Доступ до функції можливий тільки через reinterpret_cast із void *.
Усе це в купі означає що за такого підходу ні про яку type safety і мови бути не може. Виходу немає?
Вихід є! У C++ можливо реалізувати систему плагінів на базі динамічно завантажуваних бібліотек і при цьому настільки типобезпечну, на скільки це взагалі можливо у C++! Як же цього добитись?
Для початку я вирішив пошукати матеріали в Internet, бо проблема існує уже давно і не може такого бути щоб ніхто нею досі не переймався. І я не помилився, рішення знайшлось майже одразу (правда, потім я його загубив і знайти знову було уже не так просто). Але давайте не будемо відкривати карти одразу, давайте поміркуємо.
Функція dlsym, що надає доступ до символів завантаженної у runtime бібліотеки повертає лише void *, а значить із неї каші не звариш. Задача у нас стоїть проста: викликати щось що сконструює для нас об’єкт класу про який ми нічого не знаємо. Знання про конкретний тип такого об’єкту існує тільки у завантажуваному модулі, а значить це "щось" живе саме там. А "достукатись" до нього ми можемо тільки через void *... Замкнене коло?
Але чому саме ми маємо лізти у завантажуваний модуль за необхідними функціями? Чому б їм самим не прийти до нас? "Якщо гора не йде до Магомета..." - якось так. Але динамічно завантажуваня бібліотека - об’єкт пасивний. Сама вона нічого не може виконувати. Чи все таки може? Да, може. Згадаємо про ініціалізацію глобальних змінних. Вона відбувається автоматично одразу після завантаження бібліотеки. А значить нам треба виконати певний код у конструкторі певної глобальної змінної який щось зробить. Наша задача - отримати доступ до фабрики із основної програми. Значить треба під час ініціалізації передати у основну програму посилання, вказівник чи значення фабрики! Це називається "автоматична реєстрація".
Давайте ще раз розглянемо процес з точки зору типів. Фабрика, що надається плагіном, може сконструювати його екземпляр бо вона володіє повною інформацією про тип. Інформація про API ядра під час розробки плагіна теж відома, а значить він може взаємодіяти з ядром, в тому числі і зареєструвати сам себе у ньому. Виходить логічно і красиво. Та годі балакати, давайте дивитись код!
plugin.h:
#ifndef __PLUGIN_H__
#define __PLUGIN_H__
#include <string>
class Plugin {
public:
virtual ~Plugin() {}
virtual void hello(const std::string & name) = 0;
};
#endif
|
| _Winnie C++ Colorizer |
Код цього файлу не змінився, ми взаємодіємо із плагінами через абстрактний інтерфейс. Ну насправді він не зовсім абстрактний, код деструктора тут наведено, але це тільки для того щоб автоматично створені деструктори його нащадків були віртуальними і знищення об’єктів було коректне.
Тепер нам потрібен певний інтерфейс ядра, що дозволяє реєстрацію фабрик.
manager.h:
#ifndef __MANAGER_H__
#define __MANAGER_H__
#include <string>
#include <map>
class Plugin;
typedef Plugin * (* Creator) ();
typedef std::map<std::string, Creator> Factories;
class PluginManager {
public:
static Plugin * get(const std::string & name);
static void registerCreator(const std::string & name, Creator creator);
private:
static Factories & m_factories();
};
#endif
|
| _Winnie C++ Colorizer |
manager.cpp:
#include "manager.h"
Plugin * PluginManager::get(const std::string & name)
{
const Factories::const_iterator it(m_factories().find(name));
if (it == m_factories().end())
return NULL;
return it->second();
}
void PluginManager::registerCreator(const std::string & name, Creator creator)
{
m_factories()[name] = creator;
}
Factories & PluginManager::m_factories()
{
static Factories factories;
return factories;
}
|
| _Winnie C++ Colorizer |
Клас PluginManager характерний тим що він має тільки статичні методи. Справа тут у тому що у нас немає можливості передати у плагін об’єкт. Можна використовувати лише функції і явні або неявні глобальні змінні. Глобальні змінні це зло, тому краще використовувати їх неявно, повністю контролюючи доступ (наприклад для thread safety). Контейнер з фабриками заховано у спеціальний приватний метод щоб не займатися явною його ініціалізацією. PluginManager вміє реєструвати нові фабрики плагінів (замінюючи старі при колізії імен) і створювати екземпляри плагінів використовуючи ці фабрики. Ідентифікація плагіну відбувається за допомогою рядка, але якщо ця функціональність не потрібна то можна використовувати інший контейнер (наприклад, std::vector) і генерувати екземпляри "оптом", після реєстрації усіх плагінів.
myplugin.cpp:
#include <string>
#include <iostream>
#include "plugin.h"
#include "manager.h"
class MyPlugin : public Plugin {
public:
void hello(const std::string & name);
};
void MyPlugin::hello(const std::string & name)
{
std::cout << "Hello, " << name << "!" << std::endl;
}
Plugin * create()
{
return new MyPlugin;
}
class Registrator {
public:
Registrator()
{
PluginManager::registerCreator("MyPlugin", create);
}
} registrator;
|
| _Winnie C++ Colorizer |
Реалізація плагіну майже не змінилась. Але тепер крім функції-фабрики додано клас-реєстратор і об’єкт цього класу. Із ними зв’язаний один нюанс, якого я торкнусь пізніше. Зверніть увагу, функція-фабрика більше не вимагає extern "C"!
main.cpp:
#include <dlfcn.h> // dl*, RTLD_*
#include <cstdlib>
#include <iostream>
#include "plugin.h"
#include "manager.h"
int main(int, char **)
{
void * handle = dlopen("./myplugin.so", RTLD_NOW);
if (!handle) {
std::cerr << "Error loading myplugin.so: " << dlerror() << std::endl;
return EXIT_FAILURE;
}
Plugin * myPlugin = PluginManager::get("MyPlugin");
if (!myPlugin) {
std::cerr << "Failed to instantiate MyPlugin" << std::endl;
return EXIT_FAILURE;
}
myPlugin->hello("World");
return EXIT_SUCCESS;
}
|
| _Winnie C++ Colorizer |
Головна програма теж майже не змінилась. Зникло отримання доступу до функції-фабрики, замість якої ми тепер використовуємо PluginManager.
А тепер якщо взяти весь цей код, скомпілювати і запустити то...
$ g++ -W -Wall -Wextra -pedantic -fPIC -shared myplugin.cpp -o myplugin.so $ g++ -W -Wall -Wextra -pedantic -ldl main.cpp manager.cpp -o test $ ./test Error loading myplugin.so: ./myplugin.so: undefined symbol: _ZN13PluginManager15registerCreatorERKSsPFP6PluginvE $ c++filt _ZN13PluginManager15registerCreatorERKSsPFP6PluginvE PluginManager::registerCreator(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, Plugin* (*)())
Чому так? А все просто насправді. Це зв’язано із тим як працює компоновщик коли об’єднує окремі об’єктні файли у готовий бінарник. Коли використовується динамічна компоновка то за замовчуванням символи із одного модуля залишаються невидимими для іншого. Про що нам, власне, і пишуть. Це легко перевірити переглянувши таблицю динамічних символів:
$ g++ -W -Wall -Wextra -pedantic -ldl main.cpp manager.cpp -o test $ nm -D test | c++filt | grep register
Щоб змінити поведінку компоновщика треба явно передати йому ключ --export-dynamic (-Wl,--export-dynamic) або використати ключ -rdynamic gcc:
$ g++ -W -Wall -Wextra -pedantic -rdynamic -ldl main.cpp manager.cpp -o test faust@hammer ~/temp/ar $ ./test Hello, World! faust@hammer ~/temp/ar $ nm -D test | c++filt | grep register 0000000000404276 T PluginManager::registerCreator(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, Plugin* (*)())
До речі, саме проблема з експортом символів (хоча і дещо іншого характеру) років п’ять тому заставила мене відмовитись від використання статичних бібліотек у Stargazer. Але з часом я знайшов правильне її вирішення і у версії 2.408 повернув їх назад.
А тепер щодо тонкощів із реєстрацією. У dlopen є другий параметр, через який можна керувати поведінкою динамічного компоновщика. Для цього у dlfcn.h визначено набір констант RTLD_*. Одна із них, RTLD_GLOBAL, дозволяє поміщати щойно завантажені символи у глобальну область видимості, тим самим надаючи можливість наступним бібліотекам при завантаження використовувати їх. Але якщо вказати цей прапор то завантажити два плагіна вже не вдасться. Реєстрацію пройде тільки перший, всі наступні реєструватися не будуть. Це відбувається через колізію імен у модулях: registrator уже проініціалізовано, і це видно у всіх динамічних бібліотеках що завантажуються. Навіть більше того - код функції-фабрики не буде завантажено, бо він і так уже є. Щоб уникнути цього треба або придумувати унікальні імена для фабрики і об’єкта-реєстратора, або... помістити їх у анонімний простір імен! Перевіримо це:
myplugin2.cpp:
#include <string>
#include <iostream>
#include "plugin.h"
#include "manager.h"
class MyPlugin2 : public Plugin {
public:
void hello(const std::string & name);
};
void MyPlugin2::hello(const std::string & name)
{
std::cout << "Hello, " << name << "! (2)" << std::endl;
}
Plugin * create()
{
return new MyPlugin2;
}
class Registrator {
public:
Registrator()
{
PluginManager::registerCreator("MyPlugin2", create);
}
} registrator;
|
| _Winnie C++ Colorizer |
#include <dlfcn.h> // dl*, RTLD_*
#include <cstdlib>
#include <iostream>
#include "plugin.h"
#include "manager.h"
int main(int, char **)
{
void * handle = dlopen("./myplugin.so", RTLD_NOW | RTLD_GLOBAL);
if (!handle) {
std::cerr << "Error loading myplugin.so: " << dlerror() << std::endl;
return EXIT_FAILURE;
}
handle = dlopen("./myplugin2.so", RTLD_NOW | RTLD_GLOBAL);
if (!handle) {
std::cerr << "Error loading myplugin2.so: " << dlerror() << std::endl;
return EXIT_FAILURE;
}
Plugin * myPlugin = PluginManager::get("MyPlugin");
if (!myPlugin) {
std::cerr << "Failed to instantiate MyPlugin" << std::endl;
return EXIT_FAILURE;
}
myPlugin->hello("World");
Plugin * myPlugin2 = PluginManager::get("MyPlugin2");
if (!myPlugin2) {
std::cerr << "Failed to instantiate MyPlugin2" << std::endl;
return EXIT_FAILURE;
}
myPlugin2->hello("World");
return EXIT_SUCCESS;
}
|
| _Winnie C++ Colorizer |
Зверніть увагу на RTLD_GLOBAL!
$ g++ -W -Wall -Wextra -pedantic -rdynamic -ldl main.cpp manager.cpp -o test $ g++ -W -Wall -Wextra -pedantic -fPIC -shared myplugin.cpp -o myplugin.so $ g++ -W -Wall -Wextra -pedantic -fPIC -shared myplugin2.cpp -o myplugin2.so $ ./test Hello, World! Failed to instantiate MyPlugin2
Якщо RTLD_GLOBAL прибрати то все буде працювати без проблем:
$ g++ -W -Wall -Wextra -pedantic -rdynamic -ldl main.cpp manager.cpp -o test $ ./test Hello, World! Hello, World! (2)
Але більш корректне рішення - використання анонімних просторів імен (а вони як раз і придумані щоб спасати від колізій):
#include <string>
#include <iostream>
#include "plugin.h"
#include "manager.h"
class MyPlugin : public Plugin {
public:
void hello(const std::string & name);
};
void MyPlugin::hello(const std::string & name)
{
std::cout << "Hello, " << name << "!" << std::endl;
}
namespace {
Plugin * create()
{
return new MyPlugin;
}
class Registrator {
public:
Registrator()
{
PluginManager::registerCreator("MyPlugin", create);
}
} registrator;
}
|
| _Winnie C++ Colorizer |
(у myplugin2.cpp змінюємо аналогічно, а у main.cpp повертаємо RTLD_GLOBAL).
$ g++ -W -Wall -Wextra -pedantic -fPIC -shared myplugin.cpp -o myplugin.so $ g++ -W -Wall -Wextra -pedantic -fPIC -shared myplugin2.cpp -o myplugin2.so $ g++ -W -Wall -Wextra -pedantic -rdynamic -ldl main.cpp manager.cpp -o test $ ./test Hello, World! Hello, World! (2)
Цю прекрасну ідею придумав не я, а Stephan Beal ("Classloading in C++: Bringing classloading into the 21st century" - дуже рекомендую ознайомитись, там повноцінно розкрита сама ідея і її реалізація) і використав у своїй бібліотеці серіалізації S11N. До речі, цей-же Stephan Beal помічений у проекті V8-Juice.
Про ще одне дуже схоже рішення проблеми можна почитати у статті Gigi Sayfan "A cross-platform plugin framework for C/C++". Підхід відрізняється "фундаментальністю" і універсальністю (дозволено реалізувати плагіни на C), але реєстрація відбувається по команді із ядра, викликом спеціальної функції, і про type safety мова не йде (базовий інтерфейс орієнтовано на мову C).
Ще більш "фундаментальний" підхід демонструє бібліотека DynObj. Реалізація ґрунтується на препроцесорі і ідентифікаторах типів. На скільки я зрозумів, у них робота з об’єктами ведеться вручну, з реалізацією власних таблиць віртуальних методів. Одним словом - монстрячество.
Коротенька стаття про розробку системи плагінів: "Building a Better Plugin Architecture". До фабрик вони дійшли, але про автореєстрацію мова не йде. Плагін має експортувати функцію типу void registerPlugin(PluginManager & pm) яку буде визвано із ядра, і у самій цій функції займатись реєстрацією.
Цікавим також буде почитати пропозицію від Daveed Vandervoorde про включення підтримки плагінів у стандарт C++: n2015.
І на останок: Stephan Beal не рекомендує займатися вивантаженням плагінів, тільки якщо це не абсолютно необхідно. Можна отримати адських проблем з викликом неіснуючих функцій і сегфолтами на рівному місці. Зазвичай плагіни живуть до самої зупинки програми, тож краще залишити все як є і нехай ОС сама вивільняє ресурси. Я із ним частково згоден після "Про динамічні бібліотеки і C++ " і "Ну я так просто здатися не міг". До речі, зараз Stargazer теж іноді падає при зупинці в момент вивантаження плагінів. Покищо не розібрався у чому проблема, проявляється дуже рідко.