如何通过禁用名称操纵来调用实例方法

6

在foo.dll中给定以下C++类:

class a{
  private:
    int _answer;

  public:
    a(int answer) { _answer = answer; }
    __declspec(dllexport) int GetAnswer() { return _answer; }
}

我希望能够从C#中调用pInvoke GetAnswer方法。为此,我使用以下方法:

[DllImport("foo.dll", CallingConvention = CallingConvention.ThisCall, EntryPoint= "something")]
public static extern int GetAnswer(IntPtr thisA);

我传入了一个指向a的IntPtr(它来自其他地方,但不重要)。CallingConvention = CallingConvention.ThisCall 确保它会被正确处理。
这个问题很棒的一点是,我知道到目前为止我是对的,因为它已经非常好地运作了!使用Depends.exe,我可以看到 "GetAnswer" 被导出为 ?GetAnswer@a@@UAEHXZ(或者是类似的东西——重点是它已经被名称缩短了)。当我将缩短的名称插入 EntryPoint 的“something”中时,一切都很好!在想到使用Depends.exe之前,我花了大约一天的时间,所以我会把它留在这里,帮助任何遇到类似问题的人。
我的真正问题是:有没有办法禁用 GetAnswer 的 C++ 名称缩短,以便我不需要将缩短后的名称作为我的 entry point。因为我的理解是名称缩短可能会因编译器变化而发生改变,所以在其中放置缩短后的名称似乎会引发问题。此外,对于我想要 pInvoke 的每个实例方法都使用 Depends.exe 是一件麻烦的事情。
编辑:忘记添加我尝试过的内容了: 我似乎无法在函数声明上放置 extern "C",尽管我可以将其放置在定义中。这似乎没有帮助(当你想到它时是显而易见的)。
我能想到的唯一其他解决方案是创建一个包装实例方法并将 a 的实例作为参数传递的 C 风格函数。然后,在该包装器上禁用名称缩短,并 pInvoke 该函数。不过,我宁愿坚持我已经拥有的解决方案。我已经告诉我的同事们 pInvoke 很棒。如果我不得不在我们的 C++ 库中添加特殊函数来使 pInvoke 工作,那我会看起来很傻的。

可能是P/Invoke. How to call unmanaged method with marshalling from C#?的重复问题。 - Sheng Jiang 蒋晟
1
名称修饰符实际上非常有用,当 C++ 代码发生更改时,您将获得非常好的错误提示,而不是在忘记更改 PInvoke 声明时出现讨厌的 AccessViolation。其他查找名称的简单方法包括 dumpbin /exports foo.dll 和链接器的 .map 文件。请注意,您无法使用 PInvoke 调用此函数,需要使用 C++/CLI 包装器。 - Hans Passant
@HansPassant 你是说你不能使用PInvoke吗?我已经在做这件事了! - Pete Baughman
@ShengJiang蒋晟 那个“重复”的问题是关于虚函数的。它的答案在这里不适用,特别是被接受的答案,在这种情况下显然是错误的。 - Pete Baughman
@Pete Baughman,我发布了一个新的答案,可以完全实现您想要做的事情。虽然您已经有了一种解决方法,但您仍然可以在未来使用它,其他人也可能从中受益。提示永远不会太晚。 - xInterop
4个回答

5
您不能禁用C++类方法的混淆,但您可以使用/EXPORT或.def文件将函数导出为自己选择的名称。但是,您的整个方法很脆弱,因为它依赖于实现细节,即隐式传递了this参数。而且,导出类的单个方法会带来麻烦。暴露C++类给.NET语言最明智的策略是:

  1. 创建平面C封装函数并调用它们。
  2. 创建C++/CLI混合模式层,发布包装本地类的托管类。

我认为选项2更可取。


1
我不知道,这个调用似乎已经被很好地记录为实现细节。http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.callingconvention.aspx http://msdn.microsoft.com/en-us/library/ek8tkfbw(v=vs.100).aspx。不过我会尝试一下def文件。 - Pete Baughman
跟进:通过 def 文件重命名可以解决问题,但是存在 Michael Goldshteyn 指出的相同问题。您需要在 def 文件中添加 ?GetAnswer@a@@UAEHXZ = GetAnswer,这要求您事先知道名称的转换形式。 - Pete Baughman

2

你可以使用注释/连接器 #pragma 将 /EXPORT 开关传递给连接器,这应该允许你重命名导出的符号:

#pragma comment(linker, "/EXPORT:GetAnswer=?GetAnswer@a@@UAEHXZ")

不幸的是,这并不能解决您需要使用depends或其他工具查找混淆名称的问题。


我理解,在阅读文档后,#pragma指令将"?GetAnswer@a@@UAEHXZ"转换为导出表中的"GetAnswer"。要使用它,我需要编译我的C++库,查找名称混淆后的名称,然后在其中重新编译库,是吗? - Pete Baughman

1

您不必禁用混淆名称,因为它实际上包含了有关函数本身如何声明的大量信息,它基本上代表了函数名称解混后的整个签名。我知道您已经找到了一种解决方法,并且另一个答案已被标记为正确答案。下面我要写的是如何使其按照您的要求工作。

[DllImport("foo.dll", CallingConvention = CallingConvention.ThisCall, EntryPoint = "#OrdinalNumber")]
public static extern int GetAnswer(IntPtr thisA);

如果您将“#OrdinalNumber”替换为GetAnswer的实际序数,例如“#1”,则它将按照您所需的方式工作。
您可以将EntryPoint属性视为我们传递给GetProcAddress的函数名称相同,其中您可以传递函数名称或函数的序数号。
您调用C ++类的非静态函数成员的方法确实是正确的,并且thiscall被正确使用,这正是C#P / Invoke中使用thiscall调用约定的情况。这种方法的问题是,您将不得不查看DLL的PE信息,导出函数信息并找出每个要调用的函数的序数号,如果您有大量要调用的C ++函数,则可能需要自动化此过程。

0

来自问题作者:我最终采用的解决方案

我最终采用了一个C风格的函数,它包装了实例方法并将a的实例作为参数。这样,如果该类被继承,就会调用正确的虚拟方法。

我故意选择不使用C++/CLI,因为这只是多了一个要管理的项目。如果我需要使用类中的所有方法,我会考虑使用它,但我只需要这个序列化类数据的方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接