函数指针是奇怪的生物。它们不一定与数据指针的大小相同,因此不能安全地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*
) 而不是地址,然后您可以自己提取地址,例如LoadLibrary
使用GetProcAddress
对于 Windows(类似地,Linux 和 Mac OS X 有dlopen
和dlsym
)。这是有效的,因为使用 .声明的函数禁用了名称修饰extern "C"
。
请注意,没有直接的方法可以将注册函数限制为单一原型类型(否则,正如我所说,您无法正确调用它们)。如果您需要为插件函数提供特定参数(或返回值),则需要分别注册和调用具有不同原型的不同函数(尽管您可以将所有函数指针折叠为一个通用函数指针在内部输入,并且只在最后一分钟回退)。
最后,虽然您不能直接支持方法指针(在 C API 中甚至不存在,但即使使用 C++ API 也具有可变大小,因此不容易存储),您可以允许插件提供“用户- data" 注册函数时的不透明指针,每当它被调用时都会传递给函数;这为插件作者提供了一种简单的方法来编写方法周围的函数包装器并存储对象以将方法应用到 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 代替,因为它至少具有事实上的标准。”
注意:有时在插件将在完全相同的情况下编译的假设下,设计一个普通的 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();
}
这具有使用 C API 以实现兼容性的好处,但也为用 C++ 编写的插件(以及主要项目代码,即 C++)提供了更高级别的抽象。请注意,它允许在单个 DLL 中定义多个插件。您还可以通过使用宏来消除一些重复的函数名称,但对于这个简单的示例,我选择不这样做。
顺便说一下,所有这些都假设插件没有相互依赖关系——如果插件 A 影响插件 B(或被插件 B 需要),您需要设计一种安全的方法来根据需要注入/构造依赖关系,因为无法保证插件将按什么顺序加载(或初始化)。在这种情况下,两步过程会很有效:加载并注册所有插件;在注册每个插件期间,让他们注册他们提供的任何服务。在初始化过程中,通过查看注册的服务表,根据需要构造请求的服务。这确保了所有插件提供的所有服务在尝试使用它们之前都已注册,无论插件以何种顺序注册或初始化。