Windows动态库
__declspec(dllexport)与__declspec(dllimport)
首先,给出一个相对标准的动态库写法,
示例可以在https://github.com/LeiChenIntel/wheels/tree/main/cmake_guide/sample3获取。
示例是一个非常简单的动态库,定义了一个变量calFlag,提供了加减乘除计算功能。
动态库头文件:
#ifndef CALCULATIONS_H
#define CALCULATIONS_H
#ifdef calculations_EXPORTS
#define CALCULATIONS_API __declspec(dllexport)
#else
#define CALCULATIONS_API __declspec(dllimport)
#endif
extern CALCULATIONS_API int calFlag;
CALCULATIONS_API float add(float value1, float value2);
CALCULATIONS_API float subtract(float value1, float value2);
CALCULATIONS_API float multiple(float value1, float value2);
CALCULATIONS_API float divide(float value1, float value2);
#endif // !CALCULATIONS_H
一个额外的说明:
#ifndef CALCULATIONS_H
#define CALCULATIONS_H
...
#endif // !CALCULATIONS_H
这样的写法确保在复杂的include环境(比如include层层互相嵌套)中,该头文件只被一个源文件include了一次,防止出现重复声明的问题。
动态库源文件:
#include "Calculations.h"
#include <iostream>
int calFlag = 1;
float add(float value1, float value2) {
return value1 + value2;
}
float subtract(float value1, float value2) {
return value1 - value2;
}
float multiple(float value1, float value2) {
return value1 * value2;
}
float divide(float value1, float value2) {
if (value2 == 0.0) {
std::cout << "Illegal input. Divide a zero." << std::endl;
return 0;
}
return value1 / value2;
}
在示例sample3.cpp中调用动态库,打印输出结果:
#include "Calculations.h"
#include <iostream>
int main() {
std::cout << "CMake Sample 3:" << std::endl;
std::cout << "Add value : " << add(1.2, 3.5) << std::endl;
std::cout << "Subtract value: " << subtract(1.2, 3.4) << std::endl;
std::cout << "Multiple value: " << multiple(1.2, 3.4) << std::endl;
std::cout << "Divide value: " << divide(1.2, 3.4) << std::endl;
std::cout << "calFlag value: " << calFlag << std::endl;
return 0;
}
对于一开始看到代码的我来说,这段代码最让人困惑的地方莫过于CALCULATIONS_API的宏定义了,因为我看过有动态库头文件里只有__declspec(dllexport),没有__declspec(dllimport)。当然,现在看来这是一种非常糟糕的写法,不仅会给使用动态库的人带来不便,而有巨大的隐患。文件是类似这样的:
#ifndef CALCULATIONS_H
#define CALCULATIONS_H
extern __declspec(dllexport) int calFlag;
__declspec(dllexport) float add(float value1, float value2);
__declspec(dllexport) float subtract(float value1, float value2);
__declspec(dllexport) float multiple(float value1, float value2);
__declspec(dllexport) float divide(float value1, float value2);
#endif // !CALCULATIONS_H
通过查找资料,__declspec(dllexport)和__declspec(dllimport)是需要根据情况使用的。
- 如果头文件声明是用于生成动态库本身,那这个接口要加__declspec(dllexport)前缀,告诉动态库这个接口是对外的。
- 如果头文件声明是用于调用动态库,那这个接口要加__declspec(dllimport)前缀,说明这个源文件要导入动态库的内容。
所以上面的头文件写法对于生成动态库是没问题的,但对于使用动态库的源文件就不能使用这份头文件了,因为接口被写死为__declspec(dllexport)。一种解决方法是重新写一份一模一样但接口是__declspec(dllimport)的头文件,或者干脆直接在源文件声明:
extern __declspec(dllimport) int calFlag;
float add(float value1, float value2);
float subtract(float value1, float value2);
__declspec(dllimport) float multiple(float value1, float value2);
__declspec(dllimport) float divide(float value1, float value2);
#include <iostream>
int main() {
std::cout << "CMake Sample 3:" << std::endl;
std::cout << "Add value : " << add(1.2, 3.5) << std::endl;
std::cout << "Subtract value: " << subtract(1.2, 3.4) << std::endl;
std::cout << "Multiple value: " << multiple(1.2, 3.4) << std::endl;
std::cout << "Divide value: " << divide(1.2, 3.4) << std::endl;
std::cout << "calFlag value: " << calFlag << std::endl;
return 0;
}
根据官方文档,对于函数来说__declspec(dllimport)前缀是可以省略的,比如add和subtract省略了也不会出问题(甚至可以使用__declspec(dllexport)也不会出错,这点比较奇怪)。但是,对于变量(公共数据符号)就一定要加前缀,比如calFlag不加前缀或者加了__declspec(dllexport)就会报unresolved external symbol "int calFlag"错误,如图。
虽然在函数声明中使用 __declspec(dllimport) 是可选的,但如果你使用此关键字,编译器会生成更高效的代码。 不过,必须对导入的可执行文件使用 __declspec(dllimport),以访问 DLL 的公共数据符号和对象。 请注意,DLL 的用户仍需要与导入库链接。
现在问题变成了能不能把这些__declspec(dllexport),__declspec(dllimport)写法整合到一份头文件里,这样可以在生成动态库时调用,同时在使用动态库时也可以调用。回到最开始的头文件,这里的解决方法是使用宏定义calculations_EXPORTS(如果用CMake构建动态库,这个宏定义是自带的),在calculations项目中加上定义,使得CALCULATIONS_API被定义为__declspec(dllexport),在其他项目中则没有这个定义,此时CALCULATIONS_API为__declspec(dllimport)。
动态库calculations的属性,可以看到有calculations_EXPORTS定义,CALCULATIONS_API为__declspec(dllexport)。
调用动态库源文件sample3的属性,没有calculations_EXPORTS定义,CALCULATIONS_API为__declspec(dllimport)。