动态库,插件框架和c ++中的函数指针转换

Mir*_*ate 8 c++ plugins frameworks function-pointers dynamic-library

我试图在c ++中创建一个非常开放的插件框架,在我看来,我已经想出了一个方法,但是一个唠叨的想法一直在告诉我,我正在做的事情非常非常错,它要么不起作用,要么会引起问题.

我对我的框架的设计包括一个调用每个插件init函数的内核.然后,init函数转向并使用内核registerPluginregisterFunction获取唯一的id,然后分别使用该id注册插件想要访问的每个函数.

函数registerPlugin返回唯一的id.函数registerFunction接受id,函数名和泛型函数指针,如下所示:

bool registerFunction(int plugin_id, string function_name, plugin_function func){}
Run Code Online (Sandbox Code Playgroud)

其中plugin_function是

typedef void (*plugin_function)();
Run Code Online (Sandbox Code Playgroud)

然后内核获取函数指针并将其放在带有function_name和的映射中plugin_id.注册其功能的所有插件必须使函数类型化plugin_function.

为了检索函数,另一个插件调用内核

plugin_function getFunction(string plugin_name, string function_name);
Run Code Online (Sandbox Code Playgroud)

然后该插件必须将其plugin_function转换为其原始类型,以便可以使用它.它通过访问.h文件概述插件提供的所有功能,知道(理论上)正确的类型.插件由by实现为动态库.

这是完成允许不同插件相互连接的任务的聪明方法吗?或者这是一个疯狂而且非常可怕的编程技巧?如果是,请指出我正确的方向来实现这一目标.

编辑:如果需要澄清,请询问并提供.

Cam*_*ron 19

函数指针是奇怪的生物.它们不一定与数据指针的大小相同,因此无法安全地转换void*回来.但是,C++(和C)规范允许任何函数指针安全地转换为另一个函数指针类型(尽管如果你想要定义的行为,你必须在调用它之前将其强制转换回更早的类型).这类似于安全地转换任何数据指针void*和返回的能力.

指向方法的指针非常多毛:方法指针可能比普通函数指针大,具体取决于编译器,应用程序是32位还是64位等等.但更有趣的是,即使在相同的编译器/平台,并非所有方法指针的大小都相同:虚函数的方法指针可能比普通的方法指针大; 如果涉及多重继承(例如菱形模式中的虚拟继承),则方法指针可以更大.这也因编译器和平台而异.这也是创建函数对象(包装任意方法以及自由函数)很困难的原因,尤其是在没有在堆上分配内存的情况下(这可能只是使用模板法术).

因此,通过在接口中使用函数指针,插件作者将方法指针传递回框架变得不切实际,即使它们使用相同的编译器也是如此.这可能是一个可接受的约束; 稍后会详细介绍.

由于无法保证函数指针从一个编译器到下一个编译器的大小相同,因此通过注册函数指针,您可以将插件作者限制为实现与编译器具有相同大小的函数指针的编译器.这在实践中不一定是那么糟糕,因为函数指针大小在编译器版本中往往是稳定的(对于多个编译器甚至可能是相同的).

当你想调用函数指针指向的函数时,真正的问题就开始出现了.如果你不知道它的真实签名,你根本无法安全地调用该函数(从"不工作"到分段错误,你得到不好的结果).因此,插件作者将进一步限制为仅注册void不带参数的函数.

它变得更糟:函数调用实际上在汇编程序级别工作的方式不仅取决于签名和函数指针大小.还有调用约定,处理异常的方式(抛出异常时堆栈需要正确展开),以及函数指针字节的实际解释(如果它大于数据指针,那么额外的字节是多少)表示?以什么顺序?).此时,插件作者几乎只限于使用相同的编译器(和版本!),并且需要小心匹配调用约定和异常处理选项(使用MSVC++编译器,例如,异常处理仅使用/EHsc选项显式启用,以及仅使用具有您定义的确切签名的普通函数指针.

如果有点限制,到目前为止的所有限制都可以被认为是合理的.但我们还没有完成.

如果你投入std::string(或几乎是STL的任何部分),事情会变得更糟,因为即使使用相同的编译器(和版本),也有几个不同的标志/宏来控制STL; 这些标志可以影响表示字符串对象的字节的大小和含义.实际上,它就像在单独的文件中有两个不同的结构声明,每个文件具有相同的名称,并希望它们可以互换; 显然,这不起作用.示例标志是_HAS_ITERATOR_DEBUGGING.请注意,这些选项甚至可以在调试和发布模式之间切换!这些类型的错误并不总是立即/一致地表现出来,并且很难追踪.

您还必须非常小心跨模块的动态内存管理,因为new在一个项目中可能与new另一个项目中的定义不同(例如,它可能会过载).删除时,您可能有一个指向具有虚拟析构函数的接口的指针,这意味着vtable需要正确delete的对象,并且不同的编译器都以vtable不同的方式实现这些内容.通常,您希望分配对象的模块是解除分配对象的模块; 更具体地说,你想要的代码,这将释放已经完全相同的条件分配它的代码编译下一个对象.这是std::shared_ptr构造它时可以采用"删除"参数的一个原因- 因为即使使用相同的编译器和标志(shared_ptr在模块之间共享s 的唯一保证的安全方式),new并且delete可能不会在任何地方shared_ptr被破坏.使用删除器,创建共享指针的代码也控制它最终如何被销毁.(我只是把这一段放进去了;你似乎没有跨模块边界共享对象.)

所有这些都是C++没有标准二进制接口(ABI)的结果; 它是一个免费的所有人,很容易在脚下拍摄自己(有时没有意识到).

那么,有什么希望吗?完全正确!您可以将C API公开给您的插件,并让您的插件也公开C API.这非常好,因为C API几乎可以与任何语言进行互操作.你不必担心异常,除了确保它们不能在插件函数之上冒泡(这是作者关注的),并且无论编译器/选项如何(假设你没有传递STL容器)它都是稳定的等等).只有一个标准调用约定(cdecl),它是声明的函数的默认值extern "C".void*实际上,在同一平台上的所有编译器中都是相同的(例如x64上的8个字节).

您(以及插件作者)仍然可以用C++编写代码,只要两者之间的所有外部通信都使用C API(即为了互操作而伪装成C模块).

C函数指针也在实践编译器之间可能兼容,但如果你宁愿不依赖于这个你可以有插件注册一个函数(const char*),而不是地址,然后你可以提取自己使用的地址,例如,LoadLibraryGetProcAddress对于Windows(类似地,Linux和Mac OS X有dlopendlsym).这是有效的,因为对于声明的函数禁用了名称修改extern "C".

请注意,没有直接的方法将注册的函数限制为单个原型类型(否则,正如我所说,你无法正确调用它们).如果你需要给一个插件函数一个特定的参数(或者获取一个值),你需要分别注册和调用不同的函数(尽管你可以将所有函数指针折叠到一个公共函数指针)在内部键入,并且仅在最后一刻回送.

最后,虽然你不能直接支持方法指针(它们甚至不存在于C API中,但即使使用C++ API也具有可变大小,因此无法轻松存储),您可以允许插件提供"用户 - data"注册函数时的opaque指针,无论何时调用它都会传递给函数; 这为插件作者提供了一种简单的方法来编写围绕方法的函数包装器并存储对象以将该方法应用于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();
}
Run Code Online (Sandbox Code Playgroud)

对不起这个回复的长度; 其中大部分可以概括为"C++标准不扩展到互操作;使用C代替,因为它至少具有事实上的标准."


注意:有时最简单的方法是设计一个普通的C++ API(带有函数指针或接口或者你最喜欢的任何东西),假设插件将在完全相同的情况下编译; 如果您希望自己开发所有插件(即DLL是项目核心的一部分),这是合理的.如果您的项目是开源的,那么这也可以工作,在这种情况下,每个人都可以独立地选择编译项目和插件的内聚环境 - 但是这使得除了源代码之外很难分发插件.


更新:正如评论中的ern0所指出的,可以抽象模块互操作的细节(通过C API),以便主项目和插件都处理更简单的C++ API.以下是这种实施的概述:

// 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();
}
Run Code Online (Sandbox Code Playgroud)

这有利于使用C API实现兼容性,但也为使用C++编写的插件(以及主项目代码,即C++)提供更高级别的抽象.请注意,它允许在单个DLL中定义多个插件.你也可以通过使用宏来消除一些函数名的重复,但我选择不使用这个简单的例子.


顺便说一下,所有这些都假定没有相互依赖性的插件 - 如果插件A影响(或需要)插件B,你需要设计一个安全的方法来根据需要注入/构建依赖项,因为没有办法保证插件将被加载(或初始化)的顺序.在这种情况下,两步过程可以很好地工作:加载并注册所有插件; 在注册每个插件时,让他们注册他们提供的任何服务.在初始化期间,通过查看已注册的服务表,根据需要构造所请求的服务.这可确保所有插件提供的所有服务尝试使用它们之前进行注册,无论插件注册或初始化的顺序如何.