程序员人生 网站导航

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

栏目:php教程时间:2016-11-24 08:37:38

本文章将以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版本等兼容性问题。

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐