dll的def文件与__declspec(dllexpo…

本文探讨了使用__declspec(dllexport)和def文件两种方式导出DLL函数的方法。通过对比,解析了不同编译方式(C/C++)、调用约定(__cdecl、__stdcall、__fastcall)对函数名称的影响及在跨平台调用时的注意事项。

dll的def文件与__declspec(dllexport)导出函数方式比较

 

 

【__declspec(dllexport) 方式】
首先对C和C++编译(extern "C")与调用约定(__cdecl、__stdcall、__fastcall)进行组合测试:
【C++编译】
__declspec(dllexport) int add(int, int);

dll的def文件与__declspec(dllexport)导出函数方式比较

__declspec(dllexport) int __cdecl add(int, int);

dll的def文件与__declspec(dllexport)导出函数方式比较

__declspec(dllexport) int __stdcall add(int, int);

dll的def文件与__declspec(dllexport)导出函数方式比较

__declspec(dllexport) int __fastcall add(int, int);

dll的def文件与__declspec(dllexport)导出函数方式比较

对于C++编译器的函数名修饰规则:不管__cdecl, __fastcall还是__stdcall调用方式,函数修饰名都是以"?"开始,后面是函数在名字,再后面是函数返回类型和参数类型按照代号拼出的参数表。对于__stdcall方式,参数表的开始标示是"@@YG”,对于__cdecl方式则是"@@YA”,对于__fastcall方式则是"@@YI”. 
参数表后以"@Z”标示整个名字的结束,如果该函数无参数,则以"Z”标识结束。 
更详细的dll基础知识请参考:
http://hi.baidu.com/luosiyong/blog/item/92bbdcfe860435375c600812.html
更深入的C++函数名修饰编码规则请参考:
http://hi.baidu.com/wanggang2008/blog/item/cd43e60756021bc07a89470a.html


【C编译】
extern "C" __declspec(dllexport) int add(int, int);

dll的def文件与__declspec(dllexport)导出函数方式比较

extern "C" __declspec(dllexport) int __cdecl add(int, int);

dll的def文件与__declspec(dllexport)导出函数方式比较

extern "C" __declspec(dllexport) int __stdcall add(int, int);

dll的def文件与__declspec(dllexport)导出函数方式比较

extern "C" __declspec(dllexport) int __fastcall add(int, int);

dll的def文件与__declspec(dllexport)导出函数方式比较

如果创建dll和可执行文件都是使用的VC,那用__declspec(dllexport)足够解决问题。但是如果创建出来的dll要和别的厂商的工具包构建的可执行文件链接,那就有一些额外的问题来了。
在开发dll的时候,一般不让编译器改变函数名,所以通常是以C方式编译,即加入了extern "C"说明。但是看上面的组合测试结果,__stdcall和__fastcall编译出来的函数名还是和原始的函数名不同。就拿__stdcall来说,它以C编译导出的时候,会在函数前面加入下划线,并在函数后面加入@和参数总大小的字节数。
或许现在你就想,__cdecl不就是没有改变名称的方式吗,并且默认也是__cdecl调用约定,的确,我们自己写的dll几乎都可以使用__cdecl方式。但是,Windows中最普遍的调用方式都是__stdcall(比如CALLBACK、 WINAPI),一些常用的定义如下:
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
现在假如用VC的__stdcall方式开发的一个dll,里面包含了上面那样的add函数,如果要在VB中使用,VB的程序员需要如下声明:
Declare Function add Lib "win.dll" Alias"_add@8"() As Integer
注意他需要写的名称是 "_add@8",而不是简单的"add",否则就会出现函数未定义的链接错误。
【备注】
__declspec(dllexport)的位置:
To export functions, the __declspec(dllexport) keyword must appear to the left of the calling-convention keyword, if a keyword is specified.

For example:
__declspec(dllexport) void __cdecl Function1(void);


To export all of the public data members and member functions in a class, the keyword must appear to the left of the class name as follows:
class __declspec(dllexport) CExampleExport : public CObject

class definition  };
Reference:

1. http://msdn.microsoft.com/en-us/library/d91k01sh(VS.80).aspx

2. http://msdn.microsoft.com/en-us/library/a90k134d(VS.80).aspx

【def文件导出方式】
首先了解一下 使用def文件从dll导出:
http://msdn.microsoft.com/zh-cn/library/d91k01sh(v=VS.80).aspx

具体到测试实例,我们的def文件内容如下:
LIBRARY "win"

EXPORTS
 add @1

其中LIBRARY指定dll的模块名称,即dll名字,EXPORTS后的每一行指定一个导出函数名字,这个名字和头文件中的声明一致,后面可以跟@序号指定该函数的序号(这个是可选的,后面按序号导入函数的时候再详细说)。

然后再测试一下__stdcall和__fastcall是否会对导出函数改名,测试结果如下,均未改名:

extern "C" int __stdcall add();

dll的def文件与__declspec(dllexport)导出函数方式比较

extern "C" int __fastcall add();

dll的def文件与__declspec(dllexport)导出函数方式比较

另外一种方案是在代码中给链接器指定导出函数名字:
extern "C" __declspec(dllexport) int __fastcall add(int a, int b);
#pragma comment(linker, "/export:add=@add@8")
这里告诉链接器,导出一个函数名为add的函数,函数入口点和@add@8相同

dll的def文件与__declspec(dllexport)导出函数方式比较

这样,我们既可以使用add,也可以使用@add@8了。
__stdcall方式和这类似,为add=_add@8。

【按序号而不是按名称从dll导出函数】
http://msdn.microsoft.com/zh-cn/library/e7tsx612(VS.80).aspx
def文件定义如下:
LIBRARY "win"

EXPORTS
 add @1 NONAME

dll的def文件与__declspec(dllexport)导出函数方式比较

函数名称和Hint都不见了。
这样也可以用来隐藏dll中一些重要函数。

隐藏了函数名称,在应用程序中使用序号来导入函数:
#include
#include

int main()
{
 typedef int (* AddFunc)(int, int);
 HMODULE hModule = LoadLibrary("dll.dll");
 AddFunc add = (AddFunc)GetProcAddress(hModule, MAKEINTRESOURCE(1)); //注意这里序号的指定方式
 printf("%d\n", add(1, 2));
 return 0;
}

【备注】
def文件和__declspec(dllexport)方式优缺点对比:
http://blog.vckbase.com/zaboli/archive/2006/10/29/22911.html

<think>我们正在讨论C++中的`__declspec(dllexport)`和`__declspec(dllimport)`,这两个是Microsoft Visual C++特有的关键字,用于动态链接库(DLL)的创建和使用。 根据引用[1]和[2]: - `__declspec(dllexport)`:用于标记函数、类或数据,表示它们将被导出到DLL中,以便其他程序可以使用。 - `__declspec(dllimport)`:用于声明函数、类或数据是从DLL中导入的,而不是在当前模块中定义。 区别: 1. 使用场景不同: - 当我们在构建DLL时,使用`__declspec(dllexport)`来指定哪些符号(函数、类、变量)将被导出。 - 当我们在使用DLL的应用程序(或其他DLL)中,我们需要使用`__declspec(dllimport)`来声明这些符号是从DLL导入的。 2. 编译器行为不同: - 导出(dllexport)告诉编译器生成导出符号表,并将符号放入DLL的导出表中。 - 导入(dllimport)告诉编译器该符号不在当前编译单元中,而是来自DLL,因此生成相应的导入代码(通过导入表来解析地址)。 3. 效率考虑: - 使用`__declspec(dllimport)`导入函数比不使用更高效,因为编译器知道该函数来自DLL,可以生成更高效的调用代码(避免一次间接跳转)。如果不使用,调用可能通过一个跳转表(thunk)进行,会稍微慢一些。 常见的做法是使用一个宏来切换,如引用[3]所示: 在DLL项目内部,定义宏(通常是在编译器命令行中定义`BUILD_DLL`),这样头文件中的宏就会展开为`__declspec(dllexport)`。 而在使用DLL的项目中,不定义`BUILD_DLL`,则同一头文件中的宏展开为`__declspec(dllimport)`。 示例代码(头文件): ```cpp // MyLibrary.h #ifdef MYLIB_EXPORTS #define MYLIB_API __declspec(dllexport) #else #define MYLIB_API __declspec(dllimport) #endif // 导出/导入一个函数 MYLIB_API int myFunction(int a, int b); // 导出/导入一个类 class MYLIB_API MyClass { // ... }; ``` 在DLL的源代码中,我们需要定义`MYLIB_EXPORTS`(通常在项目设置中预定义),这样编译DLL时,`MYLIB_API`就是`__declspec(dllexport)`。而在客户端代码中包含这个头文件时,由于没有定义`MYLIB_EXPORTS`,`MYLIB_API`就变成`__declspec(dllimport)`。 注意: - 对于数据导出,同样适用,例如:`MYLIB_API int global_data;` - 对于C++类,导出整个类(使用`__declspec(dllexport)`)将会导出该类的所有成员函数和静态数据成员。但是,这可能会导致ABI问题,因为不同的编译器可能有不同的名称修饰规则。因此,通常建议使用C接口或使用虚接口(工厂模式)来避免这些问题。 为什么需要`__declspec(dllimport)`? - 对于函数:编译器可以生成更高效的调用代码。如果不使用,编译器会生成一个跳转到函数地址的代码(通过IAT),而使用`__declspec(dllimport)`,编译器会直接生成一个调用IAT中条目的代码,减少了一次跳转。 - 对于数据:访问导出的全局变量时,如果不使用`__declspec(dllimport)`,则无法正确链接,因为编译器不知道这个变量来自DLL。 总结: - 导出(dllexport)用于创建DLL时标记要导出的符号。 - 导入(dllimport)用于使用DLL时声明导入的符号。 - 通过宏切换可以使得同一头文件DLL项目和客户端项目中使用。 相关问题: 1. 在Linux/macOS中如何实现类似的功能? 2. 如何避免导出C++类时的ABI兼容性问题? 3. 如果不使用`__declspec(dllimport)`,调用DLL函数会有什么后果? 4. 动态库中的全局变量导出和导入需要注意什么? </think>### C++中`__declspec(dllexport)``__declspec(dllimport)`详解 #### 一、核心区别 | 特性 | `__declspec(dllexport)` | `__declspec(dllimport)` | |-----------------------|------------------------------------------|------------------------------------------| | **作用阶段** | DLL编译时(生成阶段) | 可执行文件编译时(使用阶段) | | **功能定位** | **标记导出符号** | **声明导入符号** | | **内存操作** | 将符号写入DLL导出表 | 从DLL导入表读取符号 | | **编译器行为** | 生成`.lib`导入库和导出地址表 | 生成外部符号引用和跳转指令 | | **典型位置** | DLL项目头文件/实现文件 | 使用DLL的客户端代码 | 数学表示为: $$ \begin{cases} \text{导出:} & \text{DLL} \xrightarrow{\text{dllexport}} \text{导出表} \\ \text{导入:} & \text{EXE} \xleftarrow{\text{dllimport}} \text{导入表} \end{cases} $$ #### 二、工作原理图解 ```mermaid graph LR A[DLL项目] -->|__declspec(dllexport)| B[导出符号表] C[客户端程序] -->|__declspec(dllimport)| B B --> D[操作系统加载器] ``` #### 三、使用场景对比 1. **`__declspec(dllexport)` 场景** 编译动态库时标记需公开的接口: ```cpp // MathLib.h (DLL项目中) #ifdef BUILD_DLL #define MATH_API __declspec(dllexport) #else #define MATH_API __declspec(dllimport) #endif MATH_API int Add(int a, int b); // 函数导出 MATH_API extern double PI; // 变量导出 ``` 2. **`__declspec(dllimport)` 场景** 客户端程序调用DLL接口: ```cpp // App.cpp (在使用DLL的项目中) #include "MathLib.h" // 自动展开为dllimport int main() { double circle = 2 * PI * 5; // 使用导入变量 int sum = Add(3, 4); // 调用导入函数 return 0; } ``` #### 四、性能影响分析 使用`__declspec(dllimport)`可优化调用效率: - **直接调用**(使用dllimport): $T_{\text{call}} \approx 10\text{ns}$ 生成指令:`CALL [__imp__Add]` - **间接调用**(不使用dllimport): $T_{\text{call}} \approx 50\text{ns}$ 生成指令: ```asm MOV EAX, [Add] ; 额外寻址 CALL EAX ; 间接调用 ``` #### 五、最佳实践 1. **头文件设计技巧** ```cpp #pragma once #if defined(MATHLIB_STATIC) // 静态库情况 #define MATH_API #elif defined(MATHLIB_EXPORTS) // DLL编译 #define MATH_API __declspec(dllexport) #else // 客户端使用 #define MATH_API __declspec(dllimport) #endif ``` 2. **类导出特殊处理** ```cpp class MATH_API Vector3D { // 导出整个类 public: float Dot(const Vector3D& v); static MATH_API int dimension; // 静态成员需单独导出 }; ``` 3. **C++名称修饰问题** ```cpp extern "C" { // 禁用C++名称修饰 MATH_API void* CreateObject(); MATH_API void DestroyObject(void* obj); } ``` #### 六、常见错误 1. **缺失dllimport导致链接错误** ```log error LNK2001: unresolved external symbol _Add ``` 2. **导出/导入修饰符不一致** - 解决方案:使用统一的宏定义[^3] 3. **忘记定义预处理器宏** - DLL项目需定义:`/D MATHLIB_EXPORTS` - 客户端项目不定义该宏 #### 七、跨平台兼容方案 ```cpp #if defined(_WIN32) #ifdef MATHLIB_EXPORTS #define MATH_API __declspec(dllexport) #else #define MATH_API __declspec(dllimport) #endif #else // Linux/macOS #define MATH_API __attribute__((visibility("default"))) #endif ``` > **符号可见性对比**: > Windows导出表条目数:$N_{\text{export}} \leq 2^{16}$ > Linux ELF符号表:$N_{\text{symbol}} \leq 2^{32}$[^2] --- ### 相关问题: 1. 如何在CMake项目中自动配置dllexport/dllimport宏? 2. Linux的`__attribute__((visibility))`Windows的`__declspec`有何异同? 3. 动态库中导出C++类会遇到哪些ABI兼容性问题? 4. 如何通过Dependency Walker工具分析DLL的导出表? [^1]: __declspec(dllimport) 是Microsoft Visual C++特有的修饰符 [^2]: __declspec(dllexport)__declspec(dllimport) [^3]: 条件编译实现跨项目宏切换
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值