首页
社区
课程
招聘
[原创]Dll导出C++类的3种方式(多干货)
发表于: 2020-9-16 15:31 15215

[原创]Dll导出C++类的3种方式(多干货)

2020-9-16 15:31
15215

前段时间在制作动态链接库的时候发现C++直接导出类会产生各种各样的问题(例如:Dll地狱等),查阅相关资料后发现了一种基本上可以完美解决的方法,分享给大家。我会列出3种方式的对比供大家参考。

 

用到的宏定义:

1
2
3
4
5
#ifdef DLL_EXPORTS
#define DLLAPI __declspec(dllexport)
#else
#define DLLAPI __declspec(dllimport)
#endif

1.Using pure C (纯C语言方式)

include.h头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//需要导出的类
class IDll
{
public:
    int Sum(int n) { return n + n; }
    void Release(){ delete this; }
};
 
//导出获取对象的函数
extern "C" DLLAPI IDll* __stdcall GetObj();
//类中需要导出的函数
extern "C" DLLAPI int __stdcall DllSum(IDll *pDll, int n);
//释放对象的函数,类似析构函数,但是使用者必须手动调用
extern "C" DLLAPI void __stdcall DllRelease(IDll *pDll);

Dll.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DLLAPI IDll* __stdcall GetObj()
{
    return new IDll;
}
 
DLLAPI int __stdcall DllSum(IDll *pDll, int n)
{
    int nResult = -1;
 
    if (pDll)
    {
        nResult = pDll->Sum(n);
    }
 
    return nResult;
}
 
DLLAPI void __stdcall DllRelease(IDll *pDll)
{
    if (pDll)
    {
        pDll->Release();
    }
}

使用Dll:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "../Dll/include.h"
#pragma comment(lib,"../Debug/Dll.lib")
 
int _tmain(int argc, _TCHAR* argv[])
{
    IDll *pObj = GetObj();
 
    if (pObj)
    {
        printf("sum: %d\r\n", DllSum(pObj, 42));
        DllRelease(pObj);
        pObj = NULL;
    }
    return 0;
}

这种方式类似与Win32的窗口句柄,用户将句柄作为参数传递给函数,并对对象执行各种操作。

缺点:

1.调用创建对象函数的时候编译器无法判断类型是否匹配,例如:

1
2
3
// void* GetSomeOtherObject(void) 已定义,并且是其他类的创建函数
IDll pDll = GetSomeOtherObject();
//这种方式编译也会通过

2.需要手动调用Release函数,一旦忘记则会造成内存泄露
3.如果导出的函数的参数支持除基本数据类型以外的其他类型的参数(例如:class),则也得为这些类型提供接口。

2.Using a regular C++ class (C++直接导出类)

include.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CBase
{
public:
    CBase();
    int Sub(int n);
private:
    int m_n;
};
//需要导出的类
class DLLAPI CDll: public CBase
{
public:
    CDll();
    int Sum(int n);
public:
    int m_n;
};

include.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "include.h"
 
CBase::CBase()
{
    m_n = 999;
}
 
int CBase::Sub(int n)
{
    return --n;
}
 
CDll::CDll()
{
    m_n = 123;
}
 
int CDll::Sum(int n)
{
    return CBase::Sub(n);
}

使用Dll:

1
2
3
4
5
6
7
8
9
10
#include "../Dll/include.h"
#pragma comment(lib,"../Debug/Dll.lib")
 
int _tmain(int argc, _TCHAR* argv[])
{
    CDll dll;
    std::cout << dll.m_n << std::endl;
    std::cout << dll.Sum(100) << std::endl;
    return 0;
}

输出:

缺点:

1.这种方式虽然简单易用,但是局限性很大,而且后期维护会很麻烦,除了导出的东西太多、使用者对类的实现依赖太多之外,还有其它问题:必须保证使用同一种编译器。导出类的本质是导出类里的函数,因为语法上直接导出了类,没有对函数的调用方式、重命名进行设置,导致了产生的dll并不通用。
2.Dll地狱问题:
假设DLL需要升级,对CDll进行了修改,增加了一个成员变量m_n2,其他的都不改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class CBase
{
public:
    CBase(){ m_n = 999; }
    ~CBase(){}
    int Sum(int n){ return n + n; }
public:
    int m_n;
};
//需要导出的类
class DLLAPI CDll: public CBase
{
public:
    CDll(){ m_n = 0; m_n2 = 10; }
    ~CDll(){}
    int Sum(int n) { return CBase::Sum(n); }
public:
    //新成员
    int m_n2;
    int m_n;
};

注意,在同一个解决方案下的两个项目,重新编译Dll时Lib文件会改变,使用Dll的项目不能重新编译,输出:

原因:
首先,程序语句“CDll dll;”为这个类申请一块内存。这块内存保存该类的所有成员变量,以及虚函数表。内存的大小由类的声明决定,在应用程序编译时就已经确定。当使用“dll.m_n”时,因为在m_n之前定义了一个m_n2,m_n的实现偏移地址实际已经靠后了。所以dll.m_n访问的将是原来m_n后面的那个位置,而这个位置已经超出原来那块内存的后部范围了。
很显然,在更换了DLL后,应用程序还按原来的大小申请了一块内存,而它调用的方法却访问了比这块内存更大的区域,出错再在所难免。

 

同样的情形还会发生在以下这些种情况中:

1) 应用程序直接访问类的公有变量,而该公有变量在新DLL中定义的位置发生了变化;
2) 应用程序调用类的一个虚函数,而新的类中,该虚函数的前面又增加了一个虚函数;
3) 新类的后面增加了成员变量,并且新类的成员函数将访问、修改这些变量;
4) 修改了新类的基类,基类的大小发生了变化;
等等,总言而之,一不小心,你的程序就会掉进地狱。

 

通过对这些引起出错的情况进行分析,会发现其实只有三点变化会引起出错,因为这三点是使用这个DLL的应用程序在编译时就需要确定的内容,它们分别是:

1) 类的大小;
2) 类成员的偏移地址;
3) 虚函数的顺序。

 

要想做一个可升级的DLL,必需避免以上三个问题。所以以下三点用来使DLL远离地狱。

1,不直接生成类的实例。对于类的大小,当我们定义一个类的实例,或使用new语句生成一个实例时,内存的大小是在编译时决定的。要使应用程序不依赖于类的大小,只有一个办法:应用程序不生成类的实例,使用DLL中的函数来生成。把导出类的构造函数定义为私有的(privated),在导出类中提供静态(static)成员函数(如NewInstance())用来生成类的实例。因为 NewInstance()函数在新的DLL中会被重新编译,所以总能返回大小正确的实例内存。
2,不直接访问成员变量。应用程序直接访问类的成员变量时会用到该变量的偏移地址。所以避免偏移地址依赖的办法就是不要直接访问成员变量。把所有的成员变量的访问控制都定义为保护型(protected)以上的级别,并为需要访问的成员变量定义Get或Set方法。Get或Set方法在编译新DLL时会被重新编译,所以总能访问到正确的变量位置。
3,忘了虚函数吧,就算有也不要让应用程序直接访问它。因为类的构造函数已经是私有 (privated)的了,所以应用程序也不会去继承这个类,也不会实现自己的多态。如果导出类的父类中有虚函数,或设计需要(如类工场之类的框架),一定要把这些函数声明为保护的(protected)以上的级别,并为应用程序重新设计调用该虑函数的成员函数。这一点也类似于对成员变量的处理。

 

如果导出的类能遵循以上三点,那么以后对DLL的升级将可以认为是安全的。

 

如果对一个已经存在的导出类的DLL进行维护,同样也要注意:

不要改动所有的成员变量,包括导出类的父类,无论定义的顺序还是数量;不要动所有的虚函数,无论顺序还是数量。

 

总结起来,其实是一句话:导出类的DLL不要导出除了函数以外的任何内容。
DLL地狱问是归根结底是因为DLL当初是作为函数级共享库设计的,并不能真正提供一个类所必需的信息。类层上的程序复用只有Java和C#生成的类文件才能做到。

3.Using an abstract C++ interface (使用抽象接口方式)

C++抽象接口(仅包含纯虚函数且不包含数据成员的C++类)同时兼顾以下两个方面:与对象无关的纯净接口,以及方便的的面向对象的调用方式。所需要做的就是为头文件提供接口声明并实现工厂函数,该函数将返回新创建的对象实例。仅工厂函数必须与说明__declspec(dllexport/dllimport)符一起声明。该接口不需要任何其他说明符。

 

include.h

1
2
3
4
5
6
7
8
//定义接口类纯虚函数
struct IDll
{
    virtual int Sum(int n) = 0;
    virtual void Release() = 0;
};
//工厂函数,用于创建对象
extern "C" DLLAPI IDll* __stdcall GetObj();

在上述代码段中,工厂函数GetObj声明为extern "C"。这是必需的,以防止对函数名称的修改。因此,此函数作为常规C函数公开,并且可以被任何C兼容编译器轻松识别.

 

类的实现cpp文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "include.h"
 
class CDll: public IDll
{
    int Sum(int n);
    void Release();
};
 
int CDll::Sum(int n)
{
    return n + n;
}
 
void CDll::Release()
{
    delete this;
}
 
DLLAPI IDll* __stdcall GetObj()
{
    return new CDll;
}

使用Dll:

1
2
3
4
5
6
7
8
9
10
#include "../Dll/include.h"
#pragma comment(lib,"../Debug/Dll.lib")
 
int _tmain(int argc, _TCHAR* argv[])
{
    IDll *pObj = GetObj();
    std::cout << pObj->Sum(123) << std::endl
    pObj->Release();
    return 0;
}

输出:

C++不像其他编程语言(例如C#或Java)那样为接口提供特殊的概念。但这并不意味着C++无法声明和实现接口。创建C++接口的常用方法是声明一个没有任何数据成员的抽象类。然后,另一个单独的类从接口继承并实现接口方法,以实现对客户端的隐藏。客户端既不知道也不在乎接口的实现方式。它所知道的只是哪些方法可用以及它们做什么。

实现方式

这种方法背后的想法非常简单。仅由纯虚拟方法组成的无成员C++类不过是一个虚表,即一个函数指针数组。函数指针数组由DLL作者填充需要导出的函数在DLL中。然后,在DLL外部使用此指针数组来调用实际的实现。

优点

导出的C++类可以通过抽象接口与任何C++编译器一起使用。
DLL的C运行时库和客户端彼此独立。因为资源获取和释放完全发生在DLL模块内部,客户端不受DLL内部改变的影响。
实现了真正的模块分离。可以重新设计和重建生成的DLL模块,而不会影响项目的其余部分。
如果需要,可以将DLL模块轻松转换为成熟的COM模块。

缺点

创建新对象实例并将其删除需要显式函数调用。但是,智能指针可以解决。
抽象接口方法不能返回或接受常规C++对象作为参数。它是A内置类型(如int,double,char*等)或另一抽象接口。它与C接口的限制相同。

使用智能指针创建对象:

......(后期我会补上)

附:本人经过测试后一种检测DLL加载和卸载的靠谱方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
    )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        Beep(532, 1000);
        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        Beep(532, 1000);
        break;
    }
    return TRUE;
}

在DLL内尽量不要使用MessageBox函数用来检测,可以使用Win API的Beep函数,Beep可以通过控制主板扬声器的发声频率和节拍来演奏美妙的旋律,有大神用该函数完成了天空之城这首歌,非常有意思.

1
https://blog.csdn.net/v1t1p9hvbd/article/details/71523218

最后:附上"奔跑的小河"总结的一些动态链接库的基本知识

1
https://www.cnblogs.com/huzongzhe/p/6735189.html

参考文献
https://www.codeproject.com/Articles/28969/HowTo-Export-C-classes-from-a-DLL
http://club.topsage.com/thread-497586-1-1.html
https://www.cnblogs.com/lidabo/p/7121745.html


[课程]FART 脱壳王!加量不加价!FART作者讲授!

最后于 2020-9-16 15:52 被Jmsrwt编辑 ,原因:
收藏
免费 7
支持
分享
最新回复 (11)
雪    币: 3167
活跃值: (882)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2

关于__stdcall

这是一种函数的调用方式。默认情况下VC使用的是__cdecl的函数调用方式,如果产生的dll只会给C/C++程序使用,那么就没必要定义为__stdcall调用方式,如果要给其他语言程序使用则需定义__stdcall。这个可能不是很重要,因为可以自己在调用函数的时候设置函数调用的规则。像VC就可以设置函数的调用方式。不过__stdcall这调用约定会Name-Mangling,所以我觉得用VC默认的调用约定简便些。但是,如果既要__stdcall调用约定,又要函数名不给修饰,那可以使用*.def文件,或者在代码里#pragma的方式给函数提供别名(这种方式需要知道修饰后的函数名是什么)。


举例:

·extern "C" __declspec(dllexport) int  __stdcall Sum(int n);
 
·#pragma comment(linker, "/export:Sum=_Sum@4")




 



最后于 2020-9-16 16:17 被Jmsrwt编辑 ,原因:
2020-9-16 16:16
0
雪    币: 49
活跃值: (817)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
厉害,好好学习一下!
2020-9-16 16:24
1
雪    币: 23080
活跃值: (3432)
能力值: (RANK:648 )
在线值:
发帖
回帖
粉丝
4
感谢分享
2020-9-16 16:27
1
雪    币: 5633
活跃值: (2497)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
谢谢分享。
2020-9-16 16:28
1
雪    币: 1048
活跃值: (65)
能力值: ( LV1,RANK:10 )
在线值:
发帖
回帖
粉丝
6

谢谢分享~ 支持支持~

最后于 2020-9-16 16:46 被Love Lenka编辑 ,原因:
2020-9-16 16:46
1
雪    币: 1270
活跃值: (109)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
7
学习了,谢谢分享。
2020-9-16 17:07
1
雪    币: 83
活跃值: (1082)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
8
谢谢
2020-9-16 18:07
1
雪    币: 22
活跃值: (423)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
马克
2020-9-16 19:41
0
雪    币: 198
活跃值: (2533)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
10
感谢
2020-9-17 09:36
0
雪    币: 33
活跃值: (113)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
有大神用该函数完成了天空之城这首歌,非常有意思.
————————
总有一些无聊的人会做一些无聊的事情,然后另外一些无聊的人会去验证这件事
2020-10-17 12:13
0
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
请问如何制作出C接口的dll,主要是VS版本不同的情况,还能实现h、lib、dll方式的调用?比如用VS2019用C++编写dll,给VS2010的C++使用
2022-9-30 11:45
0
游客
登录 | 注册 方可回帖
返回
//