Windows基础-动态连接库的导出与发布
本文章将以1个机器人底盘通讯程序为例,给大家展现动态链接库的1种简单暴力但灵活安全的用法:将你的最外层类修改成适于封装成DLL的模样1般的程序会包括多层的类封装,这里我们将最外层类撤除,也就是说这个类拆掉后,类里面的函数都成了全局函数。 // 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变量,依照常理它应当被封装起来不可见,这里我们拆开后照旧写在外便可。 // 类中的public函数
bool Open(const char* port_name);
unsigned char SendByte(unsigned char data);
unsigned char Move(unsigned short TanslationSpeed,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 RotationAngleSpeed);
output void * QueryState(int queryFlag,unsigned int bytesLength); 第3步:编译并导出DLL如果你的工程不是DLL的,可以在解决方案里新建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); LoadLibraryW和LoadLibraryA可以通过宏定义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个存储函数入口地址的整形变量。 1个DLL通过LoadLibrary和FreeLibrary可以被屡次使用,对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_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 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 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,unsigned int bytesLength); 由于运算符的优先级关系,我们写1个函数原型的指针时是这样 SendByte = (unsigned char(*)(unsigned char data))GetProcAddress(hdll,"SendByte");
Move = (unsigned char(*)(unsigned short TanslationSpeed,"Move");
QueryState = (void *(*)(int queryFlag,"QueryState"); 你也能够直接靠GetProcAddress履行1个函数: ((bool(*)(const char* port_name))GetProcAddress(hdll,"Open"))(port_name); 为了便于开发者使用并查询类型定义,我们还要把定义写进去。 public:
typedef float * State_Value;
typedef unsigned short * State_Code;
typedef unsigned char* State_Package;
enum ChassisState
{
CHASSIS_BASE,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版本等兼容性问题。(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |