PDL 简介
可移植动态加载器
什么是 PDL?
PDL
(Portable Dynamic Loader,便携式动态加载器)是一个轻量级、简洁且便携的库,专门用于创建和使用动态加载的类对象。
为什么我们需要动态加载类?
动态加载类技术的主要目的是创建插件,以扩展主程序的功能。在许多平台上,动态加载模块的主要问题是它们只支持过程式编程范式。当您尝试加载类时,会出现许多问题。PDL
库解决了其中大部分问题(但可惜并非全部)。
在 Win32 平台上,PDL
是 COM 技术的一个非常简单的替代方案,它没有引用计数、全局类注册以及许多其他功能。在 Unix/Linux 平台上,有几个类似的库,例如 C++ Dynamic Class Loader。大型跨平台库 WxWidgets
也提供了动态类加载支持功能。
PDL
开发的主要目标是创建一个跨平台库,能够为 Win32 和 Unix/Linux 平台提供单一的动态类加载机制(与 COM 和 C++ Dynamic Class Loader 不同)。该库还应该是轻量级的且独立的(与庞大的 WxWidgets
不同)。
创建动态加载类
让我们详细介绍使用 PDL
库创建动态加载类的过程。首先,我们需要声明一个接口,该接口将用于处理可加载类的实例。
一个不可或缺的条件是,该接口必须继承自 PDL::DynamicClass
。让我们看看 DynamicClass
的声明。
class DynamicClass
{
public:
/**
* @brief Get class name
* return class name
*/
virtual const char * GetClassName() const throw() = 0;
/**
* @brief Destroy class instance
*/
void Destroy() throw() { delete this; }
protected:
/**
* @brief Destructor
*/
virtual ~DynamicClass() throw() { ;; }
};
纯虚函数 GetClassName()
返回类的名称。您不必担心它的定义,稍后我将解释原因。
非虚函数 Destroy()
用于销毁类的实例。类析构函数被声明为 protected,以防止直接调用。稍后我将描述为什么需要这种技巧。
根据 PDL
的理念,我们必须继承我们的接口并定义 protected 虚析构函数。
我们还需要在类声明内部插入 DECLARE_DYNAMIC_CLASS
宏,并将类名作为参数。如果我们查看此宏的定义,会发现它只是定义了虚方法 GetClassName()
。
#define DECLARE_DYNAMIC_CLASS( className ) \
public: \
virtual const char * GetClassName() const throw()
{
return #className;
}
最后,让我们为接口添加纯虚方法,以实现动态加载类的有用功能。例如,我们添加 DoSomething()
方法。
因此,我们得到了以下接口。
#include <DynamicClass.hpp>
class MyTestInterface : public PDL::DynamicClass
{
public:
/**
* @brief Test method
*/
virtual void DoSomething() throw() = 0;
/**
* @brief Declare this class dynamically loadable
*/
DECLARE_DYNAMIC_CLASS( MyTestInterface )
};
我们应该将此声明放在一个单独的头文件中,在本例中是 MyTestInterface.hpp。为了兼容性,构建动态加载类及其直接使用时都必须包含此文件。
然后,我们应该声明一个继承自抽象接口 MyTestInterface
的类,并定义实现其有用功能的类方法。我们还需要使用 EXPORT_DYNAMIC_CLASS
宏来导出该类。请注意,此宏应放置在类声明之外。
#include <MyTestInterface.hpp>
#include <stdio.h>
class MyTestClass1 : public MyTestInterface
{
public:
/**
* @brief Test method
*/
void DoSomething() throw()
{
fprintf( stderr, "MyTestClass1::DoSomething()\n" );
}
};
EXPORT_DYNAMIC_CLASS( MyTestClass1 )
让我们看看 EXPORT_DYNAMIC_CLASS
宏的定义(DynamicClass.hpp 文件)。
#define EXPORT_DYNAMIC_CLASS( className ) \
extern "C" PDL_DECL_EXPORT PDL::DynamicClass * Create##className() \
{ \
try { return new className(); } \
catch( ... ) { ;; } \
return NULL; \
}
此宏定义并导出了一个名为 Create<class_name>
的函数(一个构建函数),该函数会创建一个作为参数传递的类的实例。extern "C"
修饰符是必需的,以防止函数名被修饰。PDL_DECL_EXPORT
宏将此函数声明为可导出。它在 platform.h 中的定义针对不同平台是特定的。
有几个重要问题。构建函数(Create<class_name>
)会捕获类构造函数抛出的所有异常,并在发生异常时返回 NULL
。这解决了主程序中处理插件抛出的异常的所有问题。在实例创建时,我们的构建函数返回一个指向 PDL::DynamicClass
的指针,因此,如果您忘记继承此类的接口,编译器会通过产生一个类型转换错误来提醒您。
现在我们可以构建我们的插件了。一个插件可以包含多个不同的类,但它们的名称在模块级别上应该是唯一的。每个类都必须使用 EXPORT_DYNAMIC_CLASS
宏导出。
使用动态加载类
此时,我们有了一个包含动态加载类的插件。让我们尝试使用它。
首先,我们需要获取动态类加载器 PDL::DynamicLoader
的实例。这是一个单例。要获取实例的引用,我们应该使用 static
方法 DynamicLoader::Instance()
。
PDL::DynamicLoader & dynamicLoader = PDL::DynamicLoader::Instance();
然后,我们需要加载类实例并获取其指针。
MyTestInterface * instance =
dynamicLoader.GetClassInstance< MyTestInterface >
( myLibName, "MyTestClass1" );
这里 myLibName
是插件库的文件名,例如 "MyTestClass1.dll" 或 "MyTestClass.so"。别忘了包含带有接口声明的头文件——在本例中是 MyTestInterface.hpp,如前所述。
最后,我们调用加载类的有用方法。
instance -> DoSomething();
由于动态类加载器在失败时会抛出 PDL::LoaderException
,因此捕获它是正确的。这是我们示例的完整代码。
#include <MyTestInterface.hpp>
#include <stdio.h>
try
{
PDL::DynamicLoader & dynamicLoader = PDL::DynamicLoader::Instance();
MyTestInterface * instance =
dynamicLoader.GetClassInstance< MyTestInterface >
( myLibName, "MyTestClass1" );
instance -> DoSomething();
}
catch( PDL::LoaderException & ex )
{
fprintf( stderr, "Loader exception: %s\n", ex.what() );
}
有一个重要的特性:所有动态加载的类都是单例。这意味着对同一个库名和类名调用 DynamicLoader::GetInstance()
的重复调用将返回指向同一类实例的指针。这简化了对加载实例的控制并防止了内存泄漏。如果您需要创建多个类实例,可以实现动态加载类工厂。
让我们检查一下加载的类实例是如何被销毁的。这是 DynamicClass::Destroy()
方法的职责。您不需要直接调用它——DynamicLoader
会在其析构函数或 DynamicLoader::Reset()
中执行此操作。为什么我们不使用通用的析构函数?这是因为内存分配/释放机制的问题,在不同的编译器中略有不同。让我们设想一下:我们用编译器 A 构建了一个插件,用编译器 B 构建了一个主程序。当我们通过动态加载器加载一个类时,它的实例是由编译器 A 生成的代码创建的。但是,如果我们调用析构函数,我们就会调用编译器 B 生成的代码。这可能导致意外的问题。
为了防止这些问题,析构函数 ~DynamicClass()
被声明为 protected
,您需要调用 DynamicClass::Destroy()
方法来代替。此方法确保析构函数的代码与构造函数的代码由相同的编译器编译。
美中不足
库名称存在一个问题。如果库文件名发生更改,PDL
会认为它是一个不同的库。然而,不同的名称可能指向同一个库,例如:C:\MyProg\libs\mylib.dll 和 MyLIB.DLL。
请注意,PDL
无法解决不同编译器使用的名称修饰问题。这个问题在目前尚未解决。
PDL
库已在以下平台进行测试:
- FreeBSD 6.2
- Debian 4.0 Linux 2.6.18-4
- openSUSE 10.2
- Windows XP
我将非常感谢有关 PDL
在其他平台使用情况的任何信息。
谢谢
特别感谢 Vladimir 和 Asya Storozhevykh、Alexander Ledenev 和 Valery Artukhin,他们帮助我(希望)使这篇文章变得更好。