Windows基础-动态连接库的导出与发布

本文章将以1个机器人底盘通讯程序为例,给大家展现动态链接库的1种简单暴力但灵活安全的用法:


将你的最外层类修改成适于封装成DLL的模样

1般的程序会包括多层的类封装,这里我们将最外层类撤除,也就是说这个类拆掉后,类里面的函数都成了全局函数。
如果你有1大把public变量的话,聪明的你可以将变量改成查询函数,将其内联并直接return便可:

// 1种查询底盘运行状态的函数,返回数据包的首地址并报告包长度
unsigned char* QueryState(int queryFlag, unsigned short* pkgLength); // 常见的查询函数
// 1个人物跟踪器状态报告函数(直接返回变量的查询函数)
unsigned int QueryPerson(int queryFlag); // 里面1个大switch然后各种return

如果有构造和析构函数,在public里另写1个Init和Release函数。

导出DLL(共3步)

第1步:将最外层的类拆开

// 假定我们有这样1个类
class MecanumController
{
public:
enum ChassisState
{
CHASSIS_BASE,
CHASSIS_VOLTAGE,
CHASSIS_CURRENT,
CHASSIS_ERRCODE,
// ...
};
public:
bool Open(const char* port_name);
unsigned char SendByte(unsigned char data);
unsigned char Move(unsigned short TanslationSpeed, short Yaw, short RotationAngleSpeed);
unsigned char* QueryState(int queryFlag, unsigned int bytesLength);
private:
HANDLE usart_handle;
unsigned char ReceiveByte(unsigned char data);
void Helper(void* lp);
};

可以发现,这里有1些private变量,依照常理它应当被封装起来不可见,这里我们拆开后照旧写在外便可。
清静起见,你可以把private里面的声明放在源文件代码的上面。
enumtypedef,如果你的函数的参数直接把枚举名字写上去了,建议改成int来减少改动。
清静起见,把enumtypedef放在源文件代码的上面。
PS:你也能够再写1个头文件,如果头文件只含枚举等方便调用的定义内容,还可与DLL1起发布,便于开发者的查看,视封装的复杂程度而定,自己进行平衡。
头文件变化以下,class段完全删除,只剩下全局声明:

// 类中的public函数
bool Open(const char* port_name);
unsigned char SendByte(unsigned char data);
unsigned char Move(unsigned short TanslationSpeed, short Yaw, short RotationAngleSpeed);
void* QueryState(int queryFlag, unsigned int bytesLength);

在源文件里的变化不过是删除MecanumController::并把private里的那些声明拷贝进去。

第2步:在头文件加入关键字

此时,我们的头文件里只剩下了public函数。
现在,我们在头文件中加入:

#define XXXAPI extern "C" _declspec(dllexport)
// 叫XXXAPI只是1个习惯,XXX代表了1些名字,你也能够起1个其他的名字或直接不define

并修改这几个函数声明,修改后的头文件像这样:

//#include"xxx"
#define output extern "C" _declspec(dllexport)
output bool Open(const char* port_name);
output unsigned char SendByte(unsigned char data);
output unsigned char Move(unsigned short TanslationSpeed, short Yaw, short RotationAngleSpeed);
output void * QueryState(int queryFlag, unsigned int bytesLength);

第3步:编译并导出DLL

如果你的工程不是DLL的,可以在解决方案里新建1个这里写图片描述 并按之前所做的去配置环境,注意,你输出的DLL名称是和你工程名1样的,建议起好名字,如果不嫌麻烦的话你导出后你可以重命名1下。

按这样配置后就是1个DLL工程了。
这里写图片描述

如果你是win32工程且设置为DLL空项目,照上面修改以后在解决方案管理器中对其右键点生成便可导出。
这里写图片描述

没有手抖的话,在输出里会看到以下信息:
这里写图片描述

至此,我们导出了1个DLL。

但是DLL还不能直接供他人使用,我们需要写1个专用于开发者的DLL头文件


发布DLL(共两步)

让我们先看看动态加载DLL是如何实现的

Windows提供以下Windows API用于DLL的装载、报告函数入口和DLL的卸载
分别是:

// DLL装载
HINSTANCE LoadLibraryW(LPCWSTR lpLibFileName); // Unicode工程使用wchar_t
HINSTANCE LoadLibraryA(LPCSTR lpLibFileName); // MultiByte工程使用char
// 报告函数入口
FARPROC GetProcAddress(HINSTANCE hModule, _In_ LPCSTR lpProcName);
// DLL卸载
BOOL FreeLibrary(HINSTANCE hLibModule);

LoadLibraryWLoadLibraryA可以通过宏定义LoadLibrary自动选择正确的函数,所以我们直接叫这个函数为LoadLibrary,如果函数成功读取并载入DLL于内存,则返回1个非0的HINSTANCE变量,否则返回NULL

FARPROC是1个整形变量(int),在minwindef.h中定义。

#ifdef _WIN64
typedef INT_PTR (FAR WINAPI *FARPROC)();
...
#else
typedef int (FAR WINAPI *FARPROC)();
...

在不同解决方案下的长度视工程的目标平台而定,x64对应64bit整形,x86对应32bit整形,其它未定义的目标平台同32bit整形。GetProcAddress本身会返回1个存储函数入口地址的整形变量。
FreeLibrary正常使用便可,传入HINSTANCE,如果DLL被成功载入则通过DLL载入时给出的HINSTANCE值来卸载DLL。

1个DLL通过LoadLibraryFreeLibrary可以被屡次使用,对1个DLL文件第1次使用LoadLibrary时,Windows会检查并将DLL,如果DLL适用则载入内存,DLL占用的计数器加1,当DLL被载入后继续被其它代码中的LoadLibrary使用时,Windows会制作1个内存映照来提高空间效力,计数器继续加1。FreeLibrary是将计数器减1,计数器为0时,Windows从内存中卸载DLL,否则只删除对应HINSTANCE的映照。(个人理解,不管第几次载入DLL,DLL只有1个副本存在于内存中,且每次载入都会产生1个内存映照(镜像)以便于资源管理,对释放而言,计数器为0时除删除映照外还多了1个delete操作)

终究我们要封装成1个类供开发者使用:流程是LoadLibrary,如果成功则用GetProcAddress初始化函数入口,释放时履行FreeLibrary

第1步:编写用户使用的头文件

我们给开发者的时候还是1个类封装,头文件内容以下:

// MecanumController.h
#pragma once
#include<Windows.h>

class MecanumController
{
public:
typedef float * State_Value;
typedef unsigned short * State_Code;
typedef unsigned char * State_Package;
enum ChassisState
{
CHASSIS_BASE,
CHASSIS_VOLTAGE,
CHASSIS_CURRENT,
CHASSIS_ERRCODE
};
public:
// INIT&UINIT
MecanumController(const char* port_name); // 不建议直接使用,直接使用不能肯定实例是不是可用,且产生未知的dll计数
void Release() { // 实例可用时卸载实例
FreeLibrary(hdll);
}
__inline static MecanumController * CreateInstance(const char* chassis_port_name) {
HINSTANCE hd=LoadLibrary(L"MecanumController.dll");
if(hd == NULL) return NULL;
FreeLibrary(hd);
return new MecanumController(chassis_port_name);
} // 如果实例可工作,返回1个实例地址,否则返回NULL
// FUNCTION
unsigned char(*SendByte)(unsigned char data);
unsigned char(*Move)(unsigned short TanslationSpeed, short Yaw, short RotationAngleSpeed);
void *(*QueryState)(int queryFlag, unsigned int bytesLength);
private:
HINSTANCE hdll;
};

MecanumController::MecanumController(const char* port_name)
{
// 尝试载入DLL
hdll = LoadLibrary(L"MecanumController.dll");
if (hdll == NULL) return;
// 初始化串口,这里外部有helper来保证指定串口可用,普通场景不建议在这里写1个容易失败的流程
((bool(*)(const char* port_name))GetProcAddress(hdll, "Open"))(port_name);
// 配置函数入口
SendByte = (unsigned char(*)(unsigned char data))GetProcAddress(hdll, "SendByte");
Move = (unsigned char(*)(unsigned short TanslationSpeed, short Yaw, short RotationAngleSpeed))GetProcAddress(hdll, "Move");
QueryState = (void *(*)(int queryFlag, unsigned int bytesLength))GetProcAddress(hdll, "QueryState");
}

初始化分为两个函数,其中构造函数去履行不会出错的流程,专有1个实例化函数来履行容易出错的流程,在确保成功后返回1个实例。

这个类里面长相奇异的就是函数指针了:

// FUNCTION
unsigned char(*SendByte)(unsigned char data);
unsigned char(*Move)(unsigned short TanslationSpeed, short Yaw, short RotationAngleSpeed);
void *(*QueryState)(int queryFlag, unsigned int bytesLength);

由于运算符的优先级关系,我们写1个函数原型的指针时是这样
返回类型 (*名字)(参数),这样就给机器1个带栈模型的指针。
使用GetProcAddress时需要类型转换:

SendByte = (unsigned char(*)(unsigned char data))GetProcAddress(hdll, "SendByte");
Move = (unsigned char(*)(unsigned short TanslationSpeed, short Yaw, short RotationAngleSpeed))GetProcAddress(hdll, "Move");
QueryState = (void *(*)(int queryFlag, unsigned int bytesLength))GetProcAddress(hdll, "QueryState");

你也能够直接靠GetProcAddress履行1个函数:

((bool(*)(const char* port_name))GetProcAddress(hdll, "Open"))(port_name);

为了便于开发者使用并查询类型定义,我们还要把定义写进去。
你也能够写1个专门的xxxdef.h来存储大量的定义,但也有可能可能破坏了简单的封装,请自行决定。

public:
typedef float * State_Value;
typedef unsigned short * State_Code;
typedef unsigned char* State_Package;
enum ChassisState
{
CHASSIS_BASE,
CHASSIS_VOLTAGE,
CHASSIS_CURRENT,
CHASSIS_ERRCODE
};

第2步:测试你的DLL

我们写1个rundll的程序吧:

// App.c
#include "MecanumController.h"
#include <iostream>

using namespace std;

int main()
{
auto chassis = MecanumController::CreateInstance("COM3");
if (!chassis)
{
cerr << "找不到MecanumController.dll" << endl;
return -1;
}
for (size_t i = 0; i < 100; i++) chassis->Move(i,0,0);
system("pause");
return 0;
}

如果1气呵成,恭喜你,你的DLL可以供他人使用了,同源码发布1样,还是要注意目标平台和Windows版本等兼容性问题。

波比源码 – 精品源码模版分享 | www.bobi11.com
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需!
7. 如遇到加密压缩包,请使用WINRAR解压,如遇到无法解压的请联系管理员!

波比源码 » Windows基础-动态连接库的导出与发布

发表评论

Hi, 如果你对这款模板有疑问,可以跟我联系哦!

联系站长
赞助VIP 享更多特权,建议使用 QQ 登录
喜欢我嘛?喜欢就按“ctrl+D”收藏我吧!♡