插件系统-选择GetProcAddress还是Interfaces(译)
原文:
Plugin System – an alternative to GetProcAddress and interfaces
代码下载
[介绍]
有很多文章描述如何实现一个简单的插件框架,比如后面的链接[1]和[2]。通常有两种方法
(1)插件实现一组标准的(并且通常是小的)函数(方法)。宿主(host)知道这些函数的名字,并且可以通过使用GetProcAddress函数获得这些函数的地址。这并不合适,随着函数数量的增长,维护变得越来越困难。你必须手动通过函数名绑定函数,这样你就不得不做很多工作。
(2)GetProcAddress所返回的函数被用来传递接口指针(Interface Pointer)给插件或者从插件里获取接口指针。剩下的宿主和插件的通信通过接口完成。下面是一个例子
我们将使用一个简单的例子。宿主程序是一个图片查看器。它实现了一个插件框架来增加对不同图片格式的支持(在这个例子中就是24-bit的BMP图象和24-bit的TGA(Targa)图象)。插件将被实现为DLLs并且将有.imp的扩展名以便和普通dll文件区分开来了.注意,尽管如此,可是这篇文章是关于插件的,而不是关于图象解解析器的。这里所提供的解析器非常基础并且只是用来说明的。
[使用接口的方法]
接口是所有函数都是公共的并且纯虚的基类,并且没有没有数据成员。比如
// IImageParser is the interface that all image parsers
// must implement
class IImageParser
{
public:
// parses the image file and reads it into a HBITMAP
virtual HBITMAP ParseFile( const char *fname )=0;
// returns true if the file type is supported
virtual bool SupportsType( const char *type ) const=0;
};
// CBMPParser implements the IImageParser interface
实际的图象解析器必须继承自接口类并且实现纯虚函数。BMP文件解析器可能是这个样子。
class CBMPParser: public IImageParser
{
public:
virtual HBITMAP ParseFile( const char *fname );
virtual bool SupportsType( const char *type ) const;
private:
HBITMAP CreateBitmap( int width, int height, void **data );
};
static CBMPParser g_BMPParser;
// The host calls this function to get access to the
// image parser
extern "C" __declspec(dllexport) IImageParser *GetParser( void )
{
return &g_BMPParser;
}
// CImageParser is the base class that all image parsers
宿主将使用LoadLibrary函数来载入BmpParser.imp,然后使用GetProcAddress("GetParser")来得到GetParser函数的地址,然后调用它得到IImageParser类的指针。
宿主将保存了注册了的解析器的邻接表(list),它把GetParser函数返回的指针附加到那个邻接表上去。
当宿主需要解析一个bmp文件的时候,它将调用每个解析器的SupportType(".BMP")。如果返回类型是true,宿主将调用那个解析器并且使用完整文件名调用待解析文件,并将绘制HBITMAP句柄指向的位图。
基类并不真的必须是纯接口。在技术上这里的限制是所有的成员必须可以通过对象指针访问。所以你可以有:
- 纯虚成员函数(它们能通过虚表被间接访问)
- 数据成员(它们可以通过对象的指针直接访问)
- 内联成员函数(技术上它们不能通过指针访问,但是它们的代码又一次在插件里实例化。
这样就剩下了非内联和静态成员函数。插件无法从宿主访问这样的函数,宿主也不能对插件进行这样的操作。不幸的是在一个大型系统之中,这样的函数要占据代码的大部分。
例如所有的图象解析器需要CreateFunction函数。有必要在基类里声明它并且在宿主端实现。否则每个插件都将有一份这个函数的拷贝。
这个方法的另一个限制是你不能由宿主端暴露任何全局成员或者全局函数给插件端。
我们怎么改进呢?
[把宿主分成Dll和Exe]
让我们看一下USER32模块,它有两个部分 - user32.dll 和 user32.lib。真正的代码和数据在dll中,lib仅仅提供调用dll函数的占位函数。最好的事情在于它是自动发生的。当你链接到user32.lib你就自动地获得访问user32.dll函数的权利。(这里翻译的不好)
MFC 实现得更进一步 - 它暴露你能直接使用和继承的整个类。它们没有我们在上面讨论的纯虚接口类的限制。
我们也能做同样的事情。任何你想提供给插件的函数(我私下觉得原文functionality这个词用得不好)都能放在一个单独的Dll里。使用/IMPLIB 链接器选项来创建相应的 LIB 文件。插件能与那个静态库链接,并且所有导出函数都能提供给它们。你能按你喜欢的方式把代码分成Dll 和 Exe。极限情况下,像在代码里演示的那样,Exe 工程里仅仅含有一行WinMain函数,它仅仅用来启动Dll。
任何你想要导出的全局数据,函数,类,或者成员函数必须被标记为 __declspec(dllexport) 在编译插件时。一个常用的技巧是使用宏
#ifdef COMPILE_HOST
// when the host is compiling
#define HOSTAPI __declspec(dllexport)
#else
// when the plugins are compiling
#define HOSTAPI __declspec(dllimport)
#endif
添加宏COMPILE_HOST的定义到Dll工程里,但是不加到插件工程里。
在宿主Dll端:
// must inherit
class CImageParser
{
public:
// adds the parser to the parsers list
HOSTAPI CImageParser( void );
// parses the image file and reads it into a HBITMAP
virtual HBITMAP ParseFile( const char *fname )=0;
// returns true if the file type is supported
virtual bool SupportsType( const char *type ) const=0;
protected:
HOSTAPI HBITMAP CreateBitmap( int width, int height,
void **data );
};
// CBMPParser inherits from CImageParser
现在基类并不仅仅限于一个接口。我们能增加更多基本功能。CreateBitmap函数将被所有解析器共享。
这次不再是宿主调用一个函数来获取解析器并且将它添加到邻接表中,这个功能被CImageParser的构造函数取代。当解析器对象被创建,它的构造函数将自动更新邻接表。宿主不必再使用GetProcAddress函数来看看什么解析器在Dll里。
在插件端:
class CBMPParser: public CImageParser
{
public:
virtual HBITMAP ParseFile( const char *fname );
virtual bool SupportsType( const char *type ) const;
};
static CBMPParser g_BMPParser;
当g_BMPParser被创建是它的构造函数 CBMPParser() 将被调用。那个构造函数(在插件端实现)将调用基类的构造函数CImageParser() (在宿主端实现)。那是可能的因为构造函数被标记为HOSTAPI。
等等,还可以变得更好
[把宿主Dll和Exe连接起来]
(这一部分暂时没翻译)
[链接]
[1] Plug-In framework using DLLs by Mohit Khanna
[2] ATL COM Based Addin / Plugin Framework With Dynamic Toolbars and Menus by thomas_tom99
PS.我想,Interface方法,是在第一种方法之上加入了一个间接层。
我现在到没有太强的感觉非内联函数和静态函数会成为这样一个大型系统的主要部分,Interface的派生类中的非内联函数应该只被纯虚接口函数调用,不然就是接口设计有问题了。
1 条评论:
very good~~有借鉴意义呢~
发表评论