Динамические библиотеки, фреймворки плагинов и кастинг указателей функций в c++

Я пытаюсь создать очень открытую структуру плагинов на c++, и мне кажется, что я придумал способ сделать это, но ноющая мысль продолжает говорить мне, что есть что-то очень, очень неправильное в том, что я делаю, и это либо не сработает, либо вызовет проблемы.

дизайн, который у меня есть для моей структуры, состоит из ядра, которое вызывает . Затем функция init разворачивается и использует registerPlugin и registerFunction получить уникальный идентификатор, а затем зарегистрировать каждую функцию плагин хочет быть доступным с помощью этого идентификатора, соответственно.

функция registerPlugin возвращает уникальный идентификатор. Функция registerFunction принимает этот идентификатор, имя функции и общий указатель функции, например:

bool registerFunction(int plugin_id, string function_name, plugin_function func){}

где plugin_function

typedef void (*plugin_function)();

ядро затем берет указатель функции и помещает его в карту с function_name и plugin_id. Все плагины регистрируют свои функции должен касты функции типа plugin_function.

чтобы получить функцию, другой плагин вызывает ядро

plugin_function getFunction(string plugin_name, string function_name);

тогда этот плагин должен бросить plugin_function к своему первоначальному типу поэтому его можно использовать. Он знает (теоретически), что такое правильный тип, имея доступ к изложив все функции плагина делает доступными. Плагины, кстати, реализуются как динамические библиотеки.

это умный способ выполнения задача разрешить различным плагинам соединяться друг с другом? Или это безумная и действительно ужасная техника программирования? Если это так, пожалуйста, укажите мне правильный путь для этого.

EDIT: если требуется какое-либо разъяснение, спросите, и оно будет предоставлено.

7 ответов


указатели функций-странные существа. Они не обязательно имеют тот же размер, что и указатели данных, и поэтому не могут быть безопасно приведены к void* и обратно. Но спецификации C++ (и C) позволяют любой указатель функции, который будет безопасно приведен к другому указателю функции type (хотя позже вам придется вернуть его к более раннему типу, прежде чем вызывать его, если вы хотите определить поведение). Это сродни возможности безопасного приведения любого указателя данных к void* и спина.

указатели на методы-это то, где он становится действительно волосатым: указатель метода может быть больше, чем обычный указатель функции, в зависимости от компилятора, является ли приложение 32 - или 64-разрядным и т. д. Но еще интереснее то, что даже на одном компиляторе / платформе не все указатели метода имеют одинаковый размер: указатели метода на виртуальные функции могут быть больше, чем обычные указатели метода; если множественное наследование (например, виртуальное наследование в Алмазном шаблоне) вовлеченные, указатели метода могут быть еще больше. Это зависит от компилятора и платформы. Это также является причиной того, что трудно создавать объекты функций (которые обертывают произвольные методы, а также свободные функции), особенно без выделения памяти в куче (это просто можно использовать шаблон колдовство).

таким образом, используя указатели функций в вашем интерфейсе, становится непрактичным для авторов плагинов передавать указатели методов ваш фреймворк, даже если они используют один и тот же компилятор. Это может быть приемлемым ограничением; подробнее об этом позже.

поскольку нет никакой гарантии, что указатели функций будут одинакового размера от одного компилятора к другому, регистрируя указатели функций, вы ограничиваете авторов плагинов компиляторами, которые реализуют указатели функций, имеющие тот же размер, что и ваш компилятор. Это не обязательно будет так плохо на практике, так как размеры указателей функций, как правило, стабильны в разных версиях компилятора (и даже может быть одинаковым для нескольких компиляторов).

реальные проблемы начинают возникать, когда вы хотите вызвать функции, на которые указывают указатели функций; вы не можете безопасно вызывать функцию вообще, если вы не знаете ее истинную подпись (вы будет получить плохие результаты, начиная от "не работает" до ошибок сегментации). Таким образом, авторы плагинов будут дополнительно ограничены регистрацией только void функции, которые не принимают параметры.

становится хуже: способ вызова функции на уровне ассемблера зависит не только от размера указателя подписи и функции. Существует также соглашение о вызове, способ обработки исключений (стек должен быть правильно размотан при возникновении исключения) и фактическая интерпретация байтов указателя функции (если он больше указателя данных, что означают дополнительные байты? В каком порядке?). На данный момент автором плагина является в значительной степени ограничивается использованием того же компилятора (и версии!), что вы и должны быть осторожны, чтобы соответствовать соглашению о вызове и параметрам обработки исключений (например, с компилятором MSVC++ обработка исключений явно включена только с /EHsc option), а также использовать только обычные указатели функций с точной сигнатурой, которую вы определяете.

все ограничения до сих пор can считается разумным, если немного ограничивает. Но мы еще не закончили.

если вы бросаете в std::string (или почти любая часть STL), все становится еще хуже, потому что даже с тем же компилятором (и версией) существует несколько разных флагов/макросов, которые управляют STL; эти флаги могут влиять на размер и значение байтов, представляющих строковые объекты. В сущности, это все равно, что иметь два!--32-->разные объявления структуры в отдельных файлах, каждый с тем же именем, и надеясь, что они будут взаимозаменяемыми; очевидно, это не работает. Пример флага -_HAS_ITERATOR_DEBUGGING. Обратите внимание, что эти параметры могут даже меняться между режимом отладки и выпуска! Эти типы ошибок не всегда проявляются немедленно / последовательно и могут быть очень трудными для отслеживания.

вы также должны быть очень осторожны с динамическим управлением памятью между модулями, так как new в одном проекте может быть определено иначе, чем new в другом проекте (например, он может быть перегружен). При удалении, вы можете имейте указатель на интерфейс с виртуальным деструктором, что означает vtable необходимо правильно delete объект и различные компиляторы все реализуют vtable все по-другому. В общем, вы хотите, чтобы модуль, который выделяет объект, был тем, чтобы освободить его; более конкретно, вы хотите код это освобождает объект, который был скомпилирован в тех же условиях, что и код, который его выделил. Это одна из причин std::shared_ptr можно взять аргумент "deleter" при его построении-потому что даже с тем же компилятором и флагами (единственный гарантированный безопасный способ поделиться shared_ptrs между модулями), new и delete не может быть одинаковым везде shared_ptr может быть уничтожен. С помощью deleter код, который создает общий указатель, управляет тем, как он в конечном итоге уничтожается. (Я просто бросил этот абзац для хорошей меры; вы, похоже, не делитесь объектами через границы модуля.)

все это следствие того, что C++ не имеет стандартного двоичного интерфейса ( ABI); это бесплатно-для-все, где очень легко выстрелить себе в ногу (иногда не осознавая этого).

Итак, есть ли надежда? Еще бы! Вместо этого вы можете предоставить API C своим плагинам, и ваши плагины также предоставляют API C. Это довольно удобно, потому что в C API можно interoperated с практически любого языка. Вам не нужно беспокоиться об исключениях, кроме того, чтобы убедиться, что они не может всплывать над функциями плагина (это забота авторов), и он стабилен независимо от компилятора/параметров (при условии, что вы не передаете контейнеры STL и тому подобное). Существует только одно стандартное соглашение о вызовах (cdecl), который является значением по умолчанию для функций, объявленных extern "C". void*, на практике, будет одинаковым для всех компиляторов на одной платформе (например, 8 байт на x64).

вы (и авторы плагинов) все еще можете написать свой код на C++, если все внешняя связь между ними использует API C (т. е. притворяется модулем C для целей взаимодействия).

указатели функций C также, вероятно, совместимы между компиляторами на практике, хотя, если вы не хотите зависеть от этого, вы можете зарегистрировать функцию плагина имя (const char*) вместо адреса, а затем вы можете извлечь адрес самостоятельно, используя, например,LoadLibrary С GetProcAddress для Windows (аналогично, Linux и Mac OS X имеют dlopen и dlsym). Это работает, потому что имя-коверкая отключено для функций, объявленных с extern "C".

обратите внимание, что нет прямого способа ограничить зарегистрированные функции одним типом прототипа (в противном случае, как я уже сказал, Вы не можете назвать их должным образом). Если вам нужно дать определенный параметр функции плагина (или вернуть значение), вам нужно зарегистрироваться и вызвать различные функции с помощью различные прототипы отдельно (хотя вы можете свернуть все указатели функций до общего типа указателя функции внутри и только отбросить в последнюю минуту).

наконец, хотя вы не можете напрямую поддерживать указатели методов (которые даже не существуют в API C, но имеют переменный размер даже с API C++ и, следовательно, не могут быть легко сохранены), вы можете позволить плагинам предоставлять непрозрачный указатель "пользовательские данные" при регистрации их функции, которая передается в API c++. функция всякий раз, когда она вызывается; это дает авторам плагина простой способ написать обертки функций вокруг методов и сохранить объект для применения метода в параметре user-data. Параметр user-data также может использоваться для всего, что хочет автор плагина, что упрощает интерфейс и расширение вашей системы плагинов. Другой пример использования-адаптация между различными прототипами функций с помощью оболочки и дополнительных аргументов, хранящихся в пользовательские данные.

эти предложения приводят к коду что-то вроде этого (для Windows -- код очень похож на другие платформы):

// Shared header
extern "C" {
    typedef void (*plugin_function)(void*);

    bool registerFunction(int plugin_id, const char* function_name, void* user_data);
}

// Your plugin registration code
hModule = LoadLibrary(pluginDLLPath);

// Your plugin function registration code
auto pluginFunc = (plugin_function)GetProcAddress(hModule, function_name);
// Store pluginFunc and user_data in a map keyed to function_name

// Calling a plugin function
pluginFunc(user_data);

// Declaring a plugin function
extern "C" void aPluginFunction(void*);
class Foo { void doSomething() { } };

// Defining a plugin function
void aPluginFunction(void* user_data)
{
    static_cast<Foo*>(user_data)->doSomething();
}

извините за длину этого ответа; большинство из них можно суммировать с "стандарт C++ не распространяется на взаимодействие; используйте C вместо этого, так как он, по крайней мере, имеет де-факто стандартов."


Примечание: иногда проще всего создать обычный API C++ (с указателями функций или интерфейсами или что вам больше нравится) в предположении, что плагины будут скомпилированы при точно таких же обстоятельствах; это разумно, если вы ожидаете, что все плагины будут разработаны самостоятельно (т. е. DLL являются частью ядра проекта). Это также может работать, если ваш проект с открытым исходным кодом, и в этом случае каждый может самостоятельно выбрать согласованную среду, в которой проект и плагины компилируются - но тогда это затрудняет распространение плагинов, кроме как в качестве источника код.


обновление: как указал ern0 в комментариях, можно абстрагировать детали взаимодействия модулей (через API C), чтобы как основной проект, так и плагины имели дело с более простым API c++. Ниже приводится краткое описание такой реализации:

// iplugin.h -- shared between the project and all the plugins
class IPlugin {
public:
    virtual void register() { }
    virtual void initialize() = 0;

    // Your application-specific functionality here:
    virtual void onCheeseburgerEatenEvent() { }
};

// C API:
extern "C" {
    // Returns the number of plugins in this module
    int getPluginCount();

    // Called to register the nth plugin of this module.
    // A user-data pointer is expected in return (may be null).
    void* registerPlugin(int pluginIndex);

    // Called to initialize the nth plugin of this module
    void initializePlugin(int pluginIndex, void* userData);

    void onCheeseBurgerEatenEvent(int pluginIndex, void* userData);
}


// pluginimplementation.h -- plugin authors inherit from this abstract base class
#include "iplugin.h"
class PluginImplementation {
public:
    PluginImplementation();
};


// pluginimplementation.cpp -- implements C API of plugin too
#include <vector>

struct LocalPluginRegistry {
    static std::vector<PluginImplementation*> plugins;
};

PluginImplementation::PluginImplementation() {
    LocalPluginRegistry::plugins.push_back(this);
}

extern "C" {
    int getPluginCount() {
        return static_cast<int>(LocalPluginRegistry::plugins.size());
    }

    void* registerPlugin(int pluginIndex) {
        auto plugin = LocalPluginRegistry::plugins[pluginIndex];
        plugin->register();
        return (void*)plugin;
    }

    void initializePlugin(int pluginIndex, void* userData) {
        auto plugin = static_cast<PluginImplementation*>(userData);
        plugin->initialize();
    }

    void onCheeseBurgerEatenEvent(int pluginIndex, void* userData) {
        auto plugin = static_cast<PluginImplementation*>(userData);
        plugin->onCheeseBurgerEatenEvent();
    }
}


// To declare a plugin in the DLL, just make a static instance:
class SomePlugin : public PluginImplementation {
    virtual void initialize() {  }
};
SomePlugin plugin;    // Will be created when the DLL is first loaded by a process


// plugin.h -- part of the main project source only
#include "iplugin.h"
#include <string>
#include <vector>
#include <windows.h>

class PluginRegistry;

class Plugin : public IPlugin {
public:
    Plugin(PluginRegistry* registry, int index, int moduleIndex)
        : registry(registry), index(index), moduleIndex(moduleIndex)
    {
    }

    virtual void register();
    virtual void initialize();

    virtual void onCheeseBurgerEatenEvent();

private:
    PluginRegistry* registry;
    int index;
    int moduleIndex;
    void* userData;
};

class PluginRegistry {
public:
    registerPluginsInModule(std::string const& modulePath);
    ~PluginRegistry();

public:
    std::vector<Plugin*> plugins;

private:
    extern "C" {
        typedef int (*getPluginCountFunc)();
        typedef void* (*registerPluginFunc)(int);
        typedef void (*initializePluginFunc)(int, void*);
        typedef void (*onCheeseBurgerEatenEventFunc)(int, void*);
    }

    struct Module {
        getPluginCountFunc getPluginCount;
        registerPluginFunc registerPlugin;
        initializePluginFunc initializePlugin;
        onCheeseBurgerEatenEventFunc onCheeseBurgerEatenEvent;

        HMODULE handle;
    };

    friend class Plugin;
    std::vector<Module> registeredModules;
}


// plugin.cpp
void Plugin::register() {
    auto func = registry->registeredModules[moduleIndex].registerPlugin;
    userData = func(index);
}

void Plugin::initialize() {
    auto func = registry->registeredModules[moduleIndex].initializePlugin;
    func(index, userData);
}

void Plugin::onCheeseBurgerEatenEvent() {
    auto func = registry->registeredModules[moduleIndex].onCheeseBurgerEatenEvent;
    func(index, userData);
}

PluginRegistry::registerPluginsInModule(std::string const& modulePath) {
    // For Windows:
    HMODULE handle = LoadLibrary(modulePath.c_str());

    Module module;
    module.handle = handle;
    module.getPluginCount = (getPluginCountFunc)GetProcAddr(handle, "getPluginCount");
    module.registerPlugin = (registerPluginFunc)GetProcAddr(handle, "registerPlugin");
    module.initializePlugin = (initializePluginFunc)GetProcAddr(handle, "initializePlugin");
    module.onCheeseBurgerEatenEvent = (onCheeseBurgerEatenEventFunc)GetProcAddr(handle, "onCheeseBurgerEatenEvent");

    int moduleIndex = registeredModules.size();
    registeredModules.push_back(module);

    int pluginCount = module.getPluginCount();
    for (int i = 0; i < pluginCount; ++i) {
        auto plugin = new Plugin(this, i, moduleIndex);
        plugins.push_back(plugin);
    }
}

PluginRegistry::~PluginRegistry() {
    for (auto it = plugins.begin(); it != plugins.end(); ++it) {
        delete *it;
    }

    for (auto it = registeredModules.begin(); it != registeredModules.end(); ++it) {
        FreeLibrary(it->handle);
    }
}



// When discovering plugins (e.g. by loading all DLLs in a "plugins" folder):
PluginRegistry registry;
registry.registerPluginsInModule("plugins/cheeseburgerwatcher.dll");
for (auto it = registry.plugins.begin(); it != registry.plugins.end(); ++it) {
    (*it)->register();
}
for (auto it = registry.plugins.begin(); it != registry.plugins.end(); ++it) {
    (*it)->initialize();
}

// And then, when a cheeseburger is actually eaten:
for (auto it = registry.plugins.begin(); it != registry.plugins.end(); ++it) {
    auto plugin = *it;
    plugin->onCheeseBurgerEatenEvent();
}

это имеет преимущество использования C API для совместимости, но также предлагает более высокий уровень абстракции для плагинов, написанных на C++ (и для основного проекта код, который на C++). Обратите внимание, что он позволяет определять несколько плагинов в одной DLL. Вы также можете устранить дублирование имен функций с помощью макросов, но я решил не делать этого для этого простого примера.


все это, кстати, предполагает плагины, которые не имеют взаимозависимости - если плагин a влияет (или требуется) плагином B, вам нужно разработать безопасный метод для инъекции / построения зависимостей по мере необходимости, так как нет никакого способа гарантировать, какой порядок плагины будут загружены (или инициализированы). В этом случае будет хорошо работать двухэтапный процесс: загрузка и регистрация всех плагинов; во время регистрации каждого плагина пусть они регистрируют любые услуги, которые они предоставляют. Во время инициализации создайте запрошенные службы по мере необходимости, просмотрев зарегистрированную таблицу служб. Это гарантирует, что все услуги, предлагаемые всеми плагинами, зарегистрированы до любой из них пытаются использовать, независимо от того, какие плагины заказа регистрируются или инициализировано.


  • ваше ядро должно экспортировать функции C с обычным соглашением о вызовах (cdecl или, возможно, stdcall, если вы находитесь в Windows) для регистрации плагинов и функций. Если вы используете функцию C++, то вы заставляете всех авторов плагинов использовать тот же компилятор и версию компилятора, что и вы, так как многие вещи, такие как имя функции C++, реализация STL и соглашения о вызовах зависят от компилятора.

  • Плагины должны экспортировать только функции C, такие как ядро.

  • из определения getFunction кажется, каждый плагин имеет свое имя, которое другие плагины могут использовать для получения своих функций. Это не безопасная практика, два разработчика могут создавать два разных плагина с одинаковым именем, поэтому, когда плагин запрашивает какой-либо другой плагин по имени, он может получить другой плагин, чем ожидаемый. Бест решение было бы для плагинов, чтобы иметь public GUID. Этот GUID может отображаться в заголовочном файле каждого плагина, чтобы другие плагины могли ссылаться на него.

  • вы не реализовали управление версиями. В идеале вы хотите, чтобы ваше ядро было версионным, потому что вы неизбежно измените его в будущем. Когда плагин регистрируется с ядром, он передает версию API ядра, с которой он был скомпилирован. Затем ядро может решить, можно ли загрузить плагин. Для например, если kernel version 1 получает запрос на регистрацию плагина, для которого требуется версия ядра 2, у вас есть проблема, лучший способ решить эту проблему - не позволить плагину загружаться, поскольку ему могут потребоваться функции ядра, которых нет в более старой версии. Обратный случай также возможен, kernel v2 может или не может хотеть загружать плагины, которые были созданы для kernel v1, и если это позволяет, ему может потребоваться адаптироваться к более старому API.

  • Я не уверен как идея плагина, способного найти другой плагин и вызвать его функции напрямую, так как это нарушает инкапсуляцию. Мне кажется, что лучше, если Плагины рекламируют свои возможности ядру, чтобы другие плагины могли найти необходимые им службы по возможностям, а не путем обращения к другим плагинам по имени или GUID.

  • имейте в виду, что любой плагин, который выделяет память, должен предоставить функцию освобождения для этой памяти. Каждый плагин может использовать другой библиотека времени выполнения, поэтому память, выделенная плагином, может быть неизвестна другим плагинам или ядру. Наличие выделения и освобождения в одном модуле позволяет избежать проблем.


в C++ нет ABI. Итак, то, что вы хотите сделать, имеет ограничение: плагины и ваша платформа должны скомпилировать & link тем же компилятором & компоновщиком с тем же параметром в той же ОС. Это бессмысленно, если достижение-это взаимодействие в виде двоичного распределения, потому что каждый плагин, разработанный для framework, должен подготовить много версий, которые нацелены на другой компилятор на разных ОС. Поэтому исходный код distrbute будет более практичным, чем этот, и это путь GNU(скачать src, настроить и сделать)

COM-это выбор, но он слишком сложный и устаревший. Или управляемый C++ в среде выполнения .Net. Но они только на ms os. Если вы хотите универсального решения, я предлагаю вам перейти на другой язык.


как упоминает Жан, поскольку нет стандартных соглашений C++ ABI и стандартного имени, вы застряли, чтобы скомпилировать вещи с тем же компилятором и компоновщиком. Если вы хотите общую библиотеку/dll-плагины, вы должны использовать что-то C-ish.

Если все будут скомпилированы с одним компилятором и компоновщиком, вы можете также рассмотреть функцию std::.

typedef std::function<void ()> plugin_function;

std::map<std::string, plugin_function> fncMap;

void register_func(std::string name, plugin_function fnc)
{
   fncMap[name] = fnc;
}

void call(std::string name)
{
   auto it = fncMap.find(name);
   if (it != fncMap.end())
      (it->second)();   // it->second is a function object
}


///////////////

void func()
{
   std::cout << "plain" << std::endl;
}

class T
{
public:
   void method()
   {
     std::cout << "method" << std::endl;
   }

   void method2(int i)
   {
     std::cout << "method2 : " << i << std::endl;
   }
};


T t; // of course "t" needs to outlive the map, you could just as well use shared_ptr

register_func("plain", func);
register_func("method", std::bind(&T::method, &t));
register_func("method2_5", std::bind(&T::method2, &t, 5));
register_func("method2_15", std::bind(&T::method2, &t, 15));

call("plain");
call("method");
call("method2_5");
call("method2_15");

вы также можете иметь плагин функции, которые принимают argumens. Это будет использовать заполнители для std:: bind, но вскоре вы можете обнаружить, что ему несколько не хватает boost::bind. Boost bind имеет хорошую документацию и примеры.


нет причин, по которым вы не должны этого делать. В C++ использование этого стиля указателя является лучшим, так как это просто простой указатель. Я не знаю ни одного популярного компилятора, который сделал бы что-то настолько мертвое, как не создание указателя функции, как обычный указатель. Это за пределами разумного, что кто-то может сделать что-то настолько ужасное.

стандарт плагина Vst работает аналогичным образом. Он просто использует указатели функций в.dll и не имеет способов вызова непосредственно занятия. Vst-очень популярный стандарт, и в windows люди используют практически любой компилятор для плагинов Vst, включая Delphi, основанный на pascal и не имеющий ничего общего с C++.

поэтому я бы сделал именно то, что вы предлагаете лично. Для распространенных известных плагинов я бы не использовал строковое имя, а целочисленный индекс, который можно искать намного быстрее.

альтернативой является использование интерфейсов, но я не вижу причин, если ваше мышление уже основано на функции указатели.

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

с вашим предложением стиля указателя функции вы можете использовать .NET, чтобы сделать один из плагинов, например. Очевидно, вам нужно будет разместить Mono в вашей программе, чтобы загрузить его, но только для гипотетических целей он иллюстрирует простоту этого.

кроме того, когда вы используете интерфейсы, вы должны попасть в отсчет ссылок, который неприятен. Вставьте свою логику в указатели функций, как вы предлагаете, а затем оберните элемент управления в некоторые классы C++, чтобы сделать вызов и прочее для вас. Затем другие люди могут сделать плагины с другими языками, такими как Delphi Pascal, Free Pascal, C, другие компиляторы C++ и т. д...

но, как всегда, независимо от того, что вы делаете, обработка исключений между компиляторами останется проблемой, поэтому вам нужно подумать об обработке ошибок. Лучший способ-это собственные плагины метод ловит собственные исключения плагина и возвращает код ошибки ядру и т. д...


со всеми отличными ответами выше, я просто добавлю, что эта практика на самом деле довольно широко распространена. В моей практике я видел это как в коммерческих проектах, так и в бесплатных/открытых источниках.

Да - да, это хорошая и проверенная архитектура.


вам не нужно регистрировать функции вручную. Неужели? Действительно.

то, что вы можете использовать, - это реализация прокси-сервера для вашего интерфейса плагина, где каждая функция загружает свой оригинал из общей библиотеки по требованию, прозрачно и вызывает его. Тот, кто достигает прокси-объекта этого определения интерфейса, просто может вызвать функции. Они будут загружены по требованию.

Если Плагины являются синглетами, то нет необходимости в ручной привязке вообще (в противном случае правильный экземпляр должен быть выбран первым).

идея для разработчика нового плагина заключалась бы в том, чтобы сначала описать интерфейс, затем иметь генератор, который генерирует заглушку для реализации для общей библиотеки, и дополнительно класс прокси-сервера плагина с той же сигнатурой, но с автозагрузкой по требованию, который затем используется в клиентском программном обеспечении. Оба должны выполнять один и тот же интерфейс (в C++ чисто абстрактный класс).