extern "C"在DLL导出函数时有什么作用?

extern是c/c++语言中表明函数和全局变量作用范围的关键字。该关键字告诉编译器,其声明的函数和变量可以在本模块或其他模块中使用。通常,在模块的头文件中对本模块提供给其他模块引用的函数和全局变量以关键字extern声明。例如,你写了一个DLL,在导出接口中可以声明extern "C"修饰的函数。与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。
extern "C"指令中的C,表示的一种编译和连接规约,而不是一种语言。"C"表示符合C语言的编译和连接规约的任何语言。所以,extern "C"的真实目的是实现类C和C++的混合编程。
人们经常使用下面这种方式,告诉编译器,这部分代码按C语言的格式进行编译,而不是C++的:

#ifdef __cplusplus            
extern "C"{


#endif


/*…*/


#ifdef __cplusplus


}


#endif


我现在正在做的项目,所有的DLL都是下面这样声明导出函数的:
extern "c" XXXAPI int _stdcall fun();
这里首先说明一下_stdcall和_cdecl的区别:
默认情况下VC使用的是__cdecl的函数调用方式,如果产生的dll只会给C/C++程序使用,那么就没必要定义为__stdcall调用方式,如果要给Win32汇编使用(或者其他的__stdcall调用方式的程序),那么就可以使用__stdcall。这个可能不是很重要,因为可以自己在调用函数的时候设置函数调用的规则。像VC就可以设置函数的调用方式,所以可以方便的使用win32汇编产生的dll。不过__stdcall这调用约定会Name-Mangling,所以我觉得用VC默认的调用约定简便些。但是,如果既要__stdcall调用约定,又要函数名不给修饰,那可以使用*.def文件,或者在代码里#pragma的方式给函数提供别名(这种方式需要知道修饰后的函数名是什么)。


1)调用协议常用场合
__stdcall:Windows API默认的函数调用协议。
__cdecl:C/C++默认的函数调用协议。
2)函数参数入栈方式
__stdcall:函数参数由右向左入栈。
__cdecl:函数参数由右向左入栈。
3)栈内数据清除方式
__stdcall:函数调用结束后由被调用函数清除栈内数据。
__cdecl:函数调用结束后由函数调用者清除栈内数据。
4)C语言编译器函数名称修饰规则
__stdcall:编译后,函数名被修饰为“_functionname@number”。
__cdecl:编译后,函数名被修饰为“_functionname”。
5)C++语言编译器函数名称修饰规则
__stdcall:编译后,函数名被修饰为“?functionname@@YG******@Z”。
__cdecl:编译后,函数名被修饰为“?functionname@@YA******@Z”。


上文提到的“Name-Mangling”是名字修饰或名字改编、标识符重命名的意思。C++标准并没有规定Name-Mangling的方案,所以不同编译器使用的是不同的,例如:Borland C++跟Mircrosoft C++就不同,而且可能不同版本的编译器他们的Name-Mangling规则也是不同的。这样的话,不同编译器编译出来的目标文件.obj 是不通用的,因为同一个函数,使用不同的Name-Mangling在obj文件中就会有不同的名字。如果DLL里的函数重命名规则跟DLL的使用者采用的重命名规则不一致,那就会找不到这个函数。C标准规定了C语言Name-Mangling的规范(林锐的书有这样说过)。这样就使得,任何一个支持C语言的编译器,它编译出来的obj文件可以共享,链接成可执行文件。这是一种标准,如果DLL跟其使用者都采用这种约定,那么就可以解决函数重命名规则不一致导致的错误。
影响符号名的除了C++和C的区别、编译器的区别之外,还要考虑调用约定导致的Name Mangling。如extern “c” __stdcall的调用方式就会在原来函数名上加上写表示参数的符号,而extern “c” __cdecl则不会附加额外的符号。
dll中的函数在被调用时是以函数名或函数编号的方式被索引的。这就意味着采用某编译器的C++的Name-Mangling方式产生的dll文件可能不通用。因为它们的函数名重命名方式不同。为了使得dll可以通用些,很多时候都要使用C的Name-Mangling方式,即是对每一个导出函数声明为extern “C”,而且采用_stdcall调用约定,接着还需要对导出函数进行重命名,以便导出不加修饰的函数名。所以,我们终于说明了extern “C”的作用:是为了解决函数符号名的问题,这对于动态链接库的制造者和动态链接库的使用者都需要遵守的规则。

什么情况下(不)需要考虑函数重命名的问题?
1)、隐式调用(通过lib)
如果dll的制造者跟dll的使用者采用同样的语言、同样编程环境,那么就不需要考虑函数重命名。使用者在调用函数的时候,通过Name Mangling后的函数名能在lib里找到该函数。如果dll的制造者跟dll使用不同的语言、或者不同的编译器,那就需要考虑重命名了。
2)、显示调用(通过GetProcessAddress)
这绝对是必须考虑函数重命名的。

总的来说,在编写DLL的时候,写个头文件,头文件里声明函数的NameMingling方式、调用约定(主要是为了隐式调用)。再写个*.def文件把函数重命名了(主要是为了显式调用)。提供*.DLL\*.lib\*.h给dll的使用者,这样无论是隐式的调用,还是显式的调用,都可以方便的进行。

  • 3
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
好的,以下是一个简单的 `hid.dll` 导出函数的示例代码,供参考: ```c #include <Windows.h> #include <hidsdi.h> BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { switch (fdwReason) { case DLL_PROCESS_ATTACH: // DLL 初始化 break; case DLL_PROCESS_DETACH: // DLL 卸载 break; case DLL_THREAD_ATTACH: // 新线程创建 break; case DLL_THREAD_DETACH: // 线程结束 break; } return TRUE; } // 导出函数:获取 HID 设备信息 extern "C" __declspec(dllexport) BOOL GetHidDeviceInfo( DWORD dwVendorId, // 设备厂商 ID DWORD dwProductId, // 设备产品 ID PHIDD_ATTRIBUTES pHidAttributes // 输出设备属性结构体 ) { BOOL bResult = FALSE; HDEVINFO hDeviceInfo = NULL; // 枚举所有 HID 设备 hDeviceInfo = SetupDiGetClassDevs(&GUID_DEVINTERFACE_HID, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); if (hDeviceInfo == INVALID_HANDLE_VALUE) { return FALSE; } // 遍历所有 HID 设备 DWORD dwIndex = 0; SP_DEVICE_INTERFACE_DATA deviceInterfaceData = { 0 }; deviceInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA); while (SetupDiEnumDeviceInterfaces(hDeviceInfo, NULL, &GUID_DEVINTERFACE_HID, dwIndex, &deviceInterfaceData)) { // 获取设备接口数据 DWORD dwRequiredSize = 0; SetupDiGetDeviceInterfaceDetail(hDeviceInfo, &deviceInterfaceData, NULL, 0, &dwRequiredSize, NULL); if (dwRequiredSize == 0) { break; } PSP_DEVICE_INTERFACE_DETAIL_DATA pDeviceInterfaceDetailData = (PSP_DEVICE_INTERFACE_DETAIL_DATA)LocalAlloc(LPTR, dwRequiredSize); if (pDeviceInterfaceDetailData == NULL) { break; } pDeviceInterfaceDetailData->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA); if (SetupDiGetDeviceInterfaceDetail(hDeviceInfo, &deviceInterfaceData, pDeviceInterfaceDetailData, dwRequiredSize, NULL, NULL)) { // 打开设备 HANDLE hDevice = CreateFile(pDeviceInterfaceDetailData->DevicePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if (hDevice != INVALID_HANDLE_VALUE) { // 获取设备属性 HIDD_ATTRIBUTES hidAttributes = { 0 }; hidAttributes.Size = sizeof(HIDD_ATTRIBUTES); if (HidD_GetAttributes(hDevice, &hidAttributes)) { // 比较设备厂商 ID 和产品 ID if (hidAttributes.VendorID == dwVendorId && hidAttributes.ProductID == dwProductId) { // 输出设备属性 CopyMemory(pHidAttributes, &hidAttributes, sizeof(HIDD_ATTRIBUTES)); bResult = TRUE; CloseHandle(hDevice); break; } } CloseHandle(hDevice); } } LocalFree(pDeviceInterfaceDetailData); ++dwIndex; } SetupDiDestroyDeviceInfoList(hDeviceInfo); return bResult; } ``` 此示例代码中,我们定义了一个名为 `GetHidDeviceInfo` 的函数,该函数可以用来获取指定厂商 ID 和产品 ID 的 HID 设备的属性信息。我们将该函数作为 `hid.dll` 的导出函数,供其他程序调用。 需要注意的是,在 `DllMain` 函数中,我们可以进行 DLL 的初始化和卸载工作,例如打开和关闭一些资源,但是我们需要避免在此函数中进行太多的操作,以免影响系统的性能和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值