如何导出与 C# StdCall 委托匹配的 RP1210 DLL 函数?
结论
在 32 位 Windows 上,C# 委托使用 CallingConvention.StdCall 时,原生函数也要使用 WINAPI 或 __stdcall。如果 DLL 函数实际采用 __cdecl,却由 StdCall 委托调用,通常会导致栈不平衡。
改用 __stdcall 后,如果 GetProcAddress 按 RP1210_ReadVersion 查找失败,常见原因是 x86 下的 __stdcall 函数名经过了修饰。导出名可能是 _RP1210_ReadVersion@16,而不是 RP1210_ReadVersion。可以通过模块定义文件(.def)将修饰后的内部名称映射为 RP1210 要求的导出名称。
匹配调用约定与函数签名
原问题引用的 RP1210 文档示例使用 void 返回类型,而 C# 委托和当前实现使用 short。两种签名不能直接视为等价。应以实际使用的 RP1210 规范为准,确保原生函数与 C# 委托的返回类型、参数类型和调用约定完全一致。以下示例沿用问题中的 short 返回类型;如果规范要求 void,两边都应改为 void。
C++ 实现可以写成:
extern "C" __declspec(dllexport) short WINAPI RP1210_ReadVersion(
char* DLLMajor,
char* DLLMinor,
char* APIMajor,
char* APIMinor)
{
// 按 RP1210 规范写入输出参数,并返回规范要求的值。
}
在 Windows x86 环境下,WINAPI 对应 __stdcall。extern "C" 可以避免 C++ 名称改编,但 x86 下仍可能出现 __stdcall 名称修饰,因此它本身不能保证导出名就是 RP1210_ReadVersion。
在 .def 文件中添加导出别名:
EXPORTS
RP1210_ReadVersion=_RP1210_ReadVersion@16
在 32 位环境下,四个指针参数通常对应 @16。实际内部符号名可能受编译器、语言链接和构建设置影响,因此应先检查 DLL 的导出表,再确认别名右侧与实际符号一致。可以运行:
dumpbin /exports YourDll.dll
确认导出表包含未修饰的 RP1210_ReadVersion 后,C# 的 GetProcAddress(pDll, "RP1210_ReadVersion") 才能按该名称查找。如果项目通过 .def 文件导出别名,也可以移除函数上的 __declspec(dllexport),以免额外导出修饰后的名称。.def 中的别名必须指向实际存在的内部符号。
C# 委托与参数
委托的调用约定应与原生函数一致。如果四个参数是由原生函数写入的 ANSI 字符缓冲区,可以显式指定字符集:
[UnmanagedFunctionPointer(
CallingConvention.StdCall,
CharSet = CharSet.Ansi)]
private delegate short TRP1210_READVERSION(
StringBuilder DLLMajor,
StringBuilder DLLMinor,
StringBuilder APIMajor,
StringBuilder APIMinor);
调用前,应按 RP1210 规范为每个 StringBuilder 分配足够的容量。具体容量以规范或接口约定为准,避免原生函数向容量不足的缓冲区写入数据。
排查步骤
- 确认 DLL、调用进程和目标架构都是 x86。32 位 RP1210 DLL 不能由 64 位进程直接加载。
- 核对原生函数与委托的调用约定。这里应使用
StdCall/WINAPI,而不是__cdecl。 - 按实际 RP1210 规范核对返回类型、参数类型和缓冲区约定,尤其要确认文档中的
void与代码中的short哪个适用。 - 用
dumpbin /exports检查实际导出名称。如果名称经过修饰,可通过.def文件导出未修饰的别名。 - 再用
GetProcAddress查找RP1210_ReadVersion。确认结果不是IntPtr.Zero后,再转换为委托并调用。
备注:内容仅供参考。
