2

我从事的项目需要 .Net 与非托管代码的互操作性。几周前我开始使用 .Net,虽然我在 C/C++ 方面有很多经验,但我很惊讶 CLR 如何处理 P/Invoke。这是详细信息。我的同事写了这个函数

__declspec(dllexport) int __stdcall ReadIPWSensor(unsigned int deviceClassId, void *buffer) {...}

我不得不从 C# 模块中调用它。我将函数导入为

[DllImport("ipw", CallingConvention = CallingConvention.StdCall)]
extern static int ReadIPWSensor(uint deviceClassId, IntPtr buffer);

只是为了找出异常(System.EntryPointNotFoundException,无法在 DLL 'ipw' 中找到名为 'ReadIPWSensor' 的入口点)。我使用了 DependencyWalker 工具,发现该函数导出为 ?ReadIPWSensor@@YGHIPAX@Z (我的同事忘记将其导出到 DEF 文件中)。只是为了快速测试(非托管 DLL 编译速度非常慢),我将导入定义更改为:

[DllImport("ipw", EntryPoint = "#22", CallingConvention = CallingConvention.StdCall)]
extern static int ReadIPWSensor(uint deviceClassId, IntPtr buffer);

因为序数是 22。使用新的导入定义成功通过了测试。

我的第一个问题是:处理损坏的函数导出时有哪些好的做法?使用导出序号是一个好习惯吗?

就我而言,我可以访问 C++ 源代码和 DEF 文件,因此我添加了导出并将导入定义改回

[DllImport("ipw", CallingConvention = CallingConvention.StdCall)]
extern static int ReadIPWSensor(uint deviceClassId, IntPtr buffer);

我知道我们已经在项目中使用了另一个函数,并且想将我的代码与现有代码进行比较。函数定义为

extern "C" __declspec(dllexport) int __stdcall LoadIPWData(void *buffer)

并导入为

[DllImport("ipw", CallingConvention = CallingConvention.StdCall)]
extern static int LoadIPWData(IntPtr buffer);

令我惊讶的是,DependencyWalker 工具显示该函数导出为 _LoadIPWData@4(我的同事忘记再次将其导出到 DEF 文件中)。但是,使用此函数没有 System.EntryPointNotFoundException 错误。显然,CLR 以某种方式设法解析了正确的名称。似乎有某种回退机制允许 CLR 找到正确的函数。我可以很容易地想象它对参数的大小求和并正在寻找“function_name@the_sum_of_all_parameter_sizes”,尽管它看起来很简单。

我的第二个问题是:CLR 在 P/Invoke 期间如何匹配导出的函数名称?

在这种情况下,我认为 CLR 非常聪明,以至于它实际上隐藏了一个错误 - LoadIPWData 函数应该可以通过其名称从其他非托管模块访问。也许我有点偏执,但我更喜欢了解 CLR 的实际工作原理。不幸的是,我在该主题上的所有谷歌搜索都没有结果。

4

1 回答 1

3

pinvoke marshaller 内置了一些常见的 DLL 导出命名方案的知识。它知道 __cdecl 函数通常有一个前导下划线,并且 32 位模式下的 __stdcall 通常用一个前导下划线和一个尾随 @x 修饰,其中 x 是在堆栈上传递的参数的大小(以字节为单位)。它还知道 winapi 函数在导出时带有额外的 A 或 W,这是一种命名方案,用于区分接受字符串的函数,并且有 ansi 和 Unicode 版本。对应的 [DllImport] 属性是 CharSet。它只是尝试所有这些,直到找到匹配项。

它对 C++ 编译器名称修饰规则(又名 mangling)一无所知,所以这就是为什么您必须使用extern "C"手动来抑制它。

于 2012-05-06T19:35:13.167 回答