编写旨在从C#调用的C ++?

所以我这样做是一个学习时刻,我不怕说我不知道​​我在这里做什么。 值得一提的是,在这种情况下我对C ++知之甚少。

在C#中,我已经多次使用DllImport从user32.dll或其他我未编写的DLL中引入内容,但我希望更好地理解另一半(C ++的一半)是如何实现的这发生了。

我所拥有的C ++代码很简单,只是为了validation调用是否成功完成:

 #include  using namespace std; __declspec(dllexport) void HelloWorld() { cout << "Hello, World" << endl; } 

我不知道__declspec(dllexport)的重要性是什么,但我在几个 网站上看到它并没有太多关于它的重要性。

我的C#与我之前做过的DllImport没有什么不同:

 [DllImport("TestDLL.dll")] static extern void HelloWorld(); static void Main(string[] args) { HelloWorld(); } 

我编译了C ++ DLL并将其放在C#项目中,然后将其复制到bin文件夹中。 当我运行C#项目时,我在main函数内调用HelloWorld()得到一个EntryPointNotFoundException

我的猜测是我需要更改C ++代码或C ++项目的编译标志。 目前,“使用MFC”设置为“使用标准Windows库”,并且不使用ATL或CLR。 任何帮助将不胜感激。

C ++是一种支持重载的语言。 换句话说,您可以拥有多个版本的HelloWorld()。 您还可以导出HelloWorld(int),一个不同的版本。 它也是一种需要链接器的语言。 为了不使链接器与不同函数的相同名称混淆,编译器会修饰函数的名称。 阿卡“改名”。

您要用来解决此类问题的工具是Dumpbin.exe。 使用/ exports选项从DLL上的Visual Studio命令提示符运行它。 你会看到这个:

 ordinal hint RVA name 1 0 000110EB ?HelloWorld@@YAXXZ = @ILT+230(?HelloWorld@@YAXXZ) 

一堆gobbledegook,导出的名称显示在括号中。 注意? 在前面和名字后面的@@ YAXXZ,这就是CLR无法找到导出function的原因。 带有int参数的函数将导出为?HelloWorld @@ YAXH @ Z(试一试)。

[DllImport]指令支持这一点,您可以使用EntryPoint属性来提供导出的名称。 或者你可以告诉C ++编译器它应该生成C编译器可以使用的代码。 将extern "C"放在声明前面,C ++编译器将禁止名称修饰。 当然不会再支持函数重载了。 Dumpbin.exe现在显示如下:

 ordinal hint RVA name 1 0 00011005 HelloWorld = @ILT+0(_HelloWorld) 

请注意,名称仍然不是普通的“HelloWorld”,名称前面有一个下划线。 这是一个装饰,有助于捕捉调用约定的错误。 在32位代码中,有5种不同的方法来调用函数。 其中三个与DLL相同,__ cdecl,__ stdcall和__thiscall。 对于常规自由函数,C ++编译器默认为__cdecl。

这也是[DllImport]属性CallingConvention属性的属性。 如果未指定,则使用的默认值为CallingConvention.StdCall。 哪个匹配许多DLL的调用约定,特别是Windows的调用约定,但是与C ++编译器的默认值不匹配,所以你仍然有问题。 只需使用属性或声明您的C ++函数,如下所示:

 extern "C" __declspec(dllexport) void __stdcall HelloWorld() { // etc.. } 

Dumpbin.exe输出现在看起来像:

 ordinal hint RVA name 1 0 000110B9 _HelloWorld@0 = @ILT+180(_HelloWorld@0) 

注意添加的@ 0,它描述了堆栈激活帧的大小。 换句话说,传递了多少字节的参数。 这有助于在链接时捕获声明错误,这样的错误在运行时很难诊断。

您现在可以使用[DllImport]属性,因为您最初拥有它,pinvoke marshaller足够聪明,可以理清实际function的装饰。 您可以使用ExactSpelling和EntryPoint属性来帮助它,它会稍微快一些,但你没有注意到。

第一个问题:__ declspec(dllexport)只是编译器的一个提示,你打算从DLL中导出函数。 它将生成一些额外的代码,可以帮助使导出的函数调用更快(CLR使用的没有)。 并将指令传递给需要导出函数的链接器。 导出函数也可以使用.def文件完成,但这样做很难。

这可能是最好的方法: 如何从C#导入和使用非托管C ++类?

我建议您创建一个与纯C ++静态链接的C ++ / CLI项目。 C ++ / CLI项目将生成DLL,您将像使用C#中的任何其他DLL一样使用它。 再次,请参阅上面的链接。

基本上有两件事会影响名称修改,这就是为什么你在导入函数时遇到的问题,如果extern "C"是你的函数定义和函数的调用约定。

带有cdecl调用约定的extern“C”将为您提供一个易于导入的干净名称,但您需要将调用约定添加到DllImportAttribute