C++:从dll动态加载类

30

在我的当前项目中,我希望能够从一个dll中加载一些类(这个dll不总是相同的,而且即使在编译我的应用程序时可能不存在)。对于给定的类可能会有几个替代的dll(例如用于Direct3D9和OpenGL的实现),但在任何时候只会加载/使用其中一个dll。

我有一组基础类,定义了我想要加载的类的接口以及一些基本方法/成员(即用于引用计数的方法/成员),然后dll项目会从这些基础类派生出他们自己的类。

//in namespace base
class Sprite : public RefCounted//void AddRef(), void Release() and unsigned refCnt
{
public:
    virtual base::Texture *GetTexture()=0;
    virtual unsigned GetWidth()=0;
    virtual unsigned GetHeight()=0;
    virtual float GetCentreX()=0;
    virtual float GetCentreY()=0;
    virtual void SetCentre(float x, float y)=0;

    virtual void Draw(float x, float y)=0;
    virtual void Draw(float x, float y, float angle)=0;
    virtual void Draw(float x, float y, float scaleX, flota scaleY)=0;
    virtual void Draw(float x, float y, float scaleX, flota scaleY, float angle)=0;
};

问题是我不确定如何使可执行文件和其他dll加载和使用这些类,因为我之前只使用过只有一个dll的情况,我可以通过使用生成dll时得到的.lib文件让Visual Studio链接器来解决所有问题。

我不介意为实例化这些类使用工厂方法,其中很多类已经通过设计使用此方式(例如,精灵类由主图形类创建,即Graphics->CreateSpriteFromTexture(base::Texture*))。

编辑: 当我需要编写供Python使用的C++ dll时,我使用了一个名为pyCxx的库。结果得到的dll基本上只导出了一个方法,该方法创建了“Module”类的一个实例,该实例然后可以包含用于创建其他类等的工厂方法。

最终的dll可以在Python中仅使用“import [dllname]”导入。

//dll compiled as cpputill.pyd
extern "C" void initcpputill()//only exported method
{
    static CppUtill* cpputill = new CppUtill;
}

class CppUtill : public Py::ExtensionModule<CppUtill>
{
public:
    CppUtill()
    : Py::ExtensionModule<CppUtill>("cpputill")
    {
        ExampleClass::init_type();

        add_varargs_method("ExampleClass",&CppUtill::ExampleClassFactory, "ExampleClass(), create instance of ExampleClass");
        add_varargs_method("HelloWorld",  &CppUtill::HelloWorld,  "HelloWorld(), print Hello World to console");

        initialize("C Plus Plus module");
    }
...
class ExampleClass
...
    static void init_type()
    {
        behaviors().name("ExampleClass");
        behaviors().doc ("example class");
        behaviors().supportGetattr();
        add_varargs_method("Random", &ExampleClass::Random, "Random(), get float in range 0<=x<1");
    }

它到底是如何工作的,我能在纯C++环境中使用它来解决我的问题吗?

4个回答

28

在我看来,最简单的方法是编写一个简单的C函数,该函数返回指向其他地方描述的接口的指针。然后你的应用程序可以调用该接口的所有函数,而不必知道它使用的类是什么。

编辑:下面是一个简单的示例。

在您的主应用程序代码中,您创建了一个接口的头文件:

class IModule
{
public:
    virtual ~IModule(); // <= important!
    virtual void doStuff() = 0;
};

主应用程序编码使用上述接口,但没有提供有关接口实际实现的任何详细信息。

class ActualModule: public IModule
{
/* implementation */
};

现在,这些模块 - DLL的实际实现了接口,而那些类甚至不需要被导出 - __declspec (dllexport)不是必要的。模块的唯一要求是导出一个单一函数,用于创建和返回接口的实现:

__declspec (dllexport) IModule* CreateModule()
{
    // call the constructor of the actual implementation
    IModule * module = new ActualModule();
    // return the created function
    return module;
}

注意:错误检查被省略了 - 通常情况下,您需要检查新返回的指针是否正确,并保护自己免受ActualModule类构造函数中可能引发的异常。

然后,在您的主应用程序中,您只需要简单地加载模块(LoadLibrary函数)并找到函数CreateModuleGetProcAddr函数)。然后,您通过接口使用该类。

编辑2:您的Refcount(接口的基类)可以在主应用程序中实现(并导出)。然后,所有模块都需要链接到主应用程序的lib文件(是的!exe文件可以像dll文件一样拥有lib文件!),就足够了。


火枪手:尽管C++的二进制兼容性是一个大问题,但是C函数具有标准的命名系统和调用约定,所以只要你调用的函数按名称在C中存在,你就没问题了。请检查C++函数的调用约定是否一致(使用编译开关)。 - j_random_hacker
1
但请注意brone提到的关于在DLL返回的对象上使用"delete"的问题 - 如果要这样做,DLL和主程序必须使用相同的运行时库。 这也适用于其他共享资源,例如stdio / iostreams缓冲输出。 - j_random_hacker
j_random_hacker: 我认为他不会使用delete - 因为有RefCounted类 - 我会假设,他的RefCounted类会在引用计数为0时删除对象。个人而言,我还会在RefCounted类中拥有一个静态的alloc函数,并使用placement new来构造对象。 - Paulius
好的,我已经在VC9下让它工作了。然后我使用g++编译了第三个dll(c.dll)。但是当我试图从我的VC9 exe中使用该dll时,程序崩溃了。使用C方法创建类可以,但调用任何成员都会导致访问权限违规,例如:“写入位置0x00000004的访问权限违规”。 - Fire Lancer
1
这是一个我过去自己使用过的好解决方案。然而,由于所有vtable相关的东西在编译器之间并不标准化,你必须使用与主应用程序相同的编译器来编译dlls。如果你不能保证这一点,就不应该使用这个解决方案。 - markh44
显示剩余6条评论

16

你正在重新发明COM。Refcounted类相当于IUnknown接口,而抽象类则相当于接口。在DLL中作为COM服务器的入口点名为DllGetClassObject(),它是一个类工厂。Microsoft有很多关于COM的文档可供参考,请查看一下他们是如何实现的。


我的想法完全一致。这里有一篇介绍性文章的链接:http://www.codeproject.com/KB/COM/comintro2.aspx - ChrisN
这是一个很好的观点,但令人沮丧的是,所有问题都归结于使用类似COM的构造,与.NET相比非常麻烦! - Andy

4
“在我撰写所有这些内容的同时,Paulius Maruška编辑了他的评论,基本上是说相同的话。因此,对于任何重复表示歉意,虽然我想现在你有多一个备用 :)

从我的记忆中,假设使用Visual C++...

您需要使用LoadLibrary来动态加载DLL,然后使用GetProcAddress从中检索出一个函数的地址,该函数将创建DLL代码实现的实际派生类。您如何决定精确执行此操作取决于您(需要查找DLL,需要指定它们公开功能的方式等),因此现在让我们假设插件仅提供新的Sprite实现。

为此,请确定主程序将调用以创建这些新Sprite之一的DLL中的函数的签名。这个看起来很合适:


typedef Sprite *(*CreateSpriteFn)();

然后,从主程序中,您需要加载一个 DLL(如何找到此 DLL 取决于您),并从中获取精灵创建函数。我已经决定将精灵创建函数命名为“CreateSprite”:
HMODULE hDLL=LoadLibrary(pDLLName);
CreateSpriteFn pfnCreateSprite=CreateSpriteFn(GetProcAddress(hDLL,"CreateSprite"));

然后要真正创建其中之一,只需调用该函数:
Sprite *pSprite=(*pfnCreateSprite)();

一旦您完成了 DLL 并且没有剩余被它创建的对象,那么您可以使用 FreeLibrary 将其卸载:
FreeLibrary(hDLL);

创建一个支持这个接口的DLL,需要编写派生类的代码等等,然后在DLL代码中提供CreateSprite函数,使用适当的签名,以便调用程序需要。
__declspec(dllexport)
extern "C"
Sprite *CreateSprite()
{
    return new MyNewSprite;
}

“dllexport”是指您可以使用GetProcAddress按名称选择此函数,并且extern "C"确保名称未被改变,不会出现“CreateSprite@4”之类的问题。
另外两个注意事项:
1. 如果GetProcAddress无法找到函数,则返回0,因此您可以使用它来扫描DLL列表(例如从FindFirstFile和朋友那里返回)查找支持接口的DLL,或尝试查找多个入口点并支持每个插件的多种类型。
2. 如果主程序要使用delete运算符删除精灵,则需要使用相同的运行时库构建DLL和主程序,以便主程序的delete和DLL的new都使用相同的堆。您可以通过让DLL提供一个DeleteSprite函数来解决这个问题,该函数使用DLL的运行时库而不是主程序的运行时库来删除精灵,但是仍然必须注意程序的其余部分,因此您可能只想强制DLL编写者使用与您的程序相同的运行时库。(例如,3D Studio MAX就需要这样做。)

2

当主应用程序构建时,如果我没有dll(它需要使用与dll创建的.lib),则无法工作,并且即使在主应用程序构建时所有dll都存在,我也看不到加载多个可能的dll的方法。 - Fire Lancer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接