链接库是个很不错的想法,直接以二进制形式供用户使用,而免除用户维护代码的麻烦,或者避免用户看到实现代码.
分类
Windows系统上链接库分为2种, 静态链接.lib 和动态链接.dll ,
而动态链接库按载入方式又分2种, Dynamic Load 和 Static load
链接库类型 |
载入方式 |
代码分布 |
用户需要的二进制文件 |
static link library |
无需载入 |
.lib包含实际代码, link时合并进用户程序 |
.lib |
dynamic link libray |
static load, 启动时隐式载入 |
.lib包含接口定义, 代码独立为.dll |
.lib .dll |
dynamic link libray |
dynamic load, 使用时通过系统函数显式载入 |
.lib包含接口定义, 代码独立为.dll |
.dll |
这里主要谈 Windows平台下的 static load dynamic link library.
流程
生成DLL和使用DLL的过程分三个阶段:
1. 编译阶段 (头文件 .h)
dll工程中在头文件中对导出内容(function, class, type, object, variable)进行定义.
而用户编译时需要引入这个头文件才能在代码中使用dll导出的定义.
2. 链接阶段 (库文件 .lib)
dll工程在link阶段会生成.lib
用户link时需要 这个.lib 解决link时的代码定位.
3. 运行阶段 (.dll)
dll工程生成的.dll不能单独运行,只能提供给用户程序
而用户程序运行时需要载入.dll
以下分别对这3个方面的细节作些说明
编译阶段(头文件定义)
1. 文件组织
分清外部/内部定义,分置于不同的文件,只导出外部使用时所必须的定义.
尽量减少导出定义的依赖关系和文件数目.
导出的定义越多,头文件越多,对用户就越复杂,也越容易与用户定义发生冲突.
2. 定义冲突问题
DLL导出的定义有时会与用户自己的定义冲突,特别是一些基本的数据类型定义.解决方法如下
1. 对于#define, 可以在用户使用前#undef掉
2. 对于typedef等,可将其封装在类或者namespace内部,使用前using namespace即可.
3. 改名,将DLL导出的定义统一加上前缀!但如果这个外部定义在DLL内部也要使用,则内部使用起来会比较繁琐,但也别无他法
一般为了避免冲突,尽量将定义包含在namespace或class内部,并且避免使用#define,因为其不受namespace约束,虽然可以#undef,但增加了用户的负担,而且用户误用的话出错不容易查到.
3. 导出定义的格式
使用__decspec(dllexport)参数修饰导出声明 使用__decspec(dllimport)参数修饰导出声明
export 的class, object, variable 必须 显式地 import方可使用, 不过在VC调试窗口中,这些import地object或variable不可见,应该算作VC的bug,不过用户在本地定义一个引用指向它即可见.
但函数可以不必import, 只要普通声明即可.
可在dll中使用#define切换export和import定义使得一个头文件可同时用于DLL和用户.例如
#ifdef DLL_EXPORT
#define DLL_API __decspec(dllexport)
#else
#define DLL_API __decspec(dllimport)
#endif
dll编译时定义DLL_EXPORT,而用户编译时无此定义即切换至import,如此dll和用户可公用一个.h
4. 调用规范(call type)问题
dll导出的函数有其默认的调用规则,一般为stdcall,而函数声明时并未显示指定该规则.如果函数定义在用户使用时被包含在其他调用规则范围内或者被其他规则的函数调用,就会出现link错误,找不到对应规则的dll函数.所以dll函数安全起见最好显式指定调用规则为__stdcall或者extern "C".
5. 导出的效果
class 被导出后,所有成员(函数+变量)都可使用
object被导出后,只有其成员变量可用,成员函数不可
class 的成员函数可以被单独导出
link 阶段 (.lib)
dll工程中link时使用/dll选项即可生成.lib和.dll
用户工程中link时需要引入.lib
运行阶段 (.dll)
运行时,程序会在当前目录和系统目录搜索需要的.dll,如果找不到会直接退出.
但是有时.dll不是必须组件,这时可以在用户工程link时,添加/delayload:<optinal>.dll以延迟<optinal>.dll的load, 如此用户程序在启动时不会去搜寻.dll,只有在真正用到时才会去检查
动态载入方式
这里顺便提一下动态载入.一般先 LoadLibrary 载入dll得到dll handle,
之后通过 GetProcessAddress获取函数地址,并将其赋予同类型的函数指针即可使用该函数.
但这里仍然需要dll头文件中的函数类型定义,当然用户程序中也可以自己定义一下.
GetProcessAddress 同样也可以用于得到变量和对象的地址.
dll相关的问题的说明
DLLMain
dll的主函数是DllMain,形式参见msdn.如果不自定义DllMain,VC会提供一个默认版本的DllMain.
DllMain内部的进入有4种情况
DLL_PROCESS_ATTATCH |
DLL被载入时 |
DLL_PROCESS_DETACH |
DLL被释放时 |
DLL_THREAD_ATTATCH |
DLL中有thread启动时 |
DLL_THREAD_DETACH |
DLL中有thread停止时 |
通过传入的参数可以区分这四种情况
同一份代码,既可生成dll,也可生成exe
有时需要一份代码既可以生成exe,又可以用作dll.其实在exe程序中使用export定义也会生成.lib文件,而且这个.lib可以配合exe用在用户程序中,但是运行时会出现一些资源冲突,dll函数也不能正常使用.
既然exe不能用作dll,那就只能可以通过设置了不同link选项的configuration分别生成dll和exe版本.
dll 和 exe 如果在代码上有区别也可通过不同的#define开关切换,
exe的入口是main或者WinMain, 而dll的入口是DllMain,所以互不影响.
dll内实现界面
在DLL内部实现界面,要考虑以下问题:
1. 界面资源的切换
exe在调用dll函数时,并不切换资源空间,而dll中的界面需要自己的资源空间,所以在dll使用界面资源之前,需要切换至dll的资源空间,具体做法可以在DLLMain的DLL_PROCESS_ATTACH处理时,加入代码:
AFX_MODULE_STATE* pState = AfxGetModuleState();
pState->m_hCurrentInstanceHandle = pState->m_hCurrentResourceHandle = (HINSTANCE) hModule;
这里使用AfxWinInit函数也可,其内会作和上面代码同样的设置.
2. 界面线程
界面需要自己的消息循环,所以为了不阻塞用户程序,需要把dll界面放入线程当中.
在需要界面的时候,启动线程即可,这里需要使用cWinThread类.
至于线程的实现,如果原本就有继承了cWinApp的主线程,可以将cWinApp改为cWinThread,并去除theApp的定义, 这样主线程就变成了一个受控的普通线程.这里通过宏定义再cWinApp和cWinThread的话,可以使界面同时用于exe和dll!
如果需要额外再添加一个界面,或者没有cWinApp主线程,则可写一个新类继承于cWinThread,而把界面的消息循环,比如DoModal放入Thread内部调用.
另外,如果界面处理功能不太要求实时性或者比较简单,也可以通过SetTimer来实现,而不需要复杂的thread.
3. 界面与主程序的同步
用户程序在启动或关闭dll界面时,可能需要等待界面操作完成,可以通过EVENT来实现同步锁的功能,在开始界面操作前reset event,通知界面操作后开始等待event set, 而界面内部完成操作后会set event,而主线程中等待到event set事件后再动作.
相关工具
1. dll2lib
可将dll转换成静态链接库连入用户代码解除二进制依赖问题
2. dependency
VC自带的工具,可以查看dll的导出导入定义,依赖关系
版权声明:以上资料皆摘自网上,仅供积累参考使用!