转自:http://www.cnblogs.com/warensoft/archive/2011/12/09/Warenosoft3D.html
最近在编写Warensoft3D游戏引擎,并预计明年年初发布测试版本,底层引擎使用DirectX和MONO来编写,上层的逻辑使用C#来编写,因此编写了大量C#与C++互调的代码,现在经验写出来与大家分享,并希望后来者少走弯路。
C#与C++交互,总体来说可以有两种方法:
-
利用C++/CLI作为代理中间层
-
利用PInvoke实现直接调用
第一种方法:实现起来比较简单直观,并且可以实现C#调用C++所写的类,但是问题是MONO构架不支持C++/CLI功能,因此无法实现脱离Microsoft .NET Framework跨平台运行。
第二种方法:简单的实现并不麻烦,只要添加DllImportAttribute特性即可以导入C/C++的函数,但是问题是PInvoke不能简单的实现对C++类的调用。在Warensoft3D中为了可以使用MONO实现跨平台(当然DirectX是不能跨平台的),所以使用了本方法,下面将对本方法展开详细的说明。
测试平台:
Windows7 64位,VS2010,.NET4.0
注意事项:
PInvoke从功能上来说,只支持函数调用,在被导出的函数前面一定要添加extern "C"来指明导出函数的时候使用C语言方式编译和连接,这样保证函数定义的名字和导出的名字相同,否则如果默认按C++方式导出,那个函数的名字就会变得乱七八糟,我们的程序就无法找到入口点了。
本文将说明以下几点:
-
互调的基本原理
-
基本数据类型的传递
-
指针的传递
-
函数指针的传递
-
结构体的传递
-
互调的基本原理
首先,我们来看一个再常规不过的概念—"数据类型"
我们知道在大多数的静态语言中定义变量的时候都要先指定其数据类型,所谓数据类型,都是人们强加的一个便于记忆的名称,究其本质就是指明了这个数据在内存里到底是占用了几个字节,程序在运行的时候,首先找到这个数据的地址,然后再按着该类型的长度,读取相对应的内存,然后再处理。
了解了前面这个事儿,所有编程语言之间进行互调就有点门道儿了。对于不同语言之间的互调,只要将该数据的指针(内存地址)传递给另一个语言,在另一个语言中根据通信协议将指针所指向的数据存储入长度对应的数据类型即可,当然要满足以下几点:
-
对于像Java,.NET这样有运行时虚拟机编程语言来讲,由于虚拟机会让堆内存来回转移,因此,在进行互调的时候,要保证正在被互调的数据所在的内存一定要固定,不能被转移。
-
有一些编程语言支持指针,有一些语言不支持指针(如Java),这个问题并不重要,所谓指针,其实就是一个内存地址,对于32位OS的指针是一个32位整数,而对于64位机OS的指针是一个64位整数。因为大多数语言中都有整型数,所以可以利用整型来接收指针。
-
-
基本数据类型的传递
互调过程中,最基本要传递的无非是数值和字符,即:int,long,float,char等等,但是此类型非彼类型,C/C++与C#中有一些数据类型长度是不一样的,下表中列出常见数据类型的异同:
C/C++ |
C# |
长度 |
short |
short |
2Bytes |
int |
int |
4Bytes |
long(该类型在传递的时候常常会弄混) |
int |
4Bytes |
bool |
bool |
1Byte |
char(Ascii码字符) |
byte |
1Byte |
wchar_t(Unicode字符,该类型与C#中的Char兼容) |
char |
2Bytes |
float |
float |
4Bytes |
double |
double |
8Bytes |
最容易弄混的是就是long,char两个类型,在C/C++中long和int都是4个字节,都对应着C#中的int类型,而C/C++中的char类型占一个字节,用来表示一个ASCII码字符,在C#中能够表示一个字节的是byte类型。与C#中char类型对应的应该是C/C++中的wchar_t类型,对应的是一个2字节的Unicode字符。
下面通过实例来说明调用过程:
第一步:
建立一个C++的Win32DLL,如下图所示:
这里要注意选择"Export symbols"导出符号。点击完成。
第二步:
由于项目的名称是"TestCPPDLL",因此,会自动生成TestCPPDLL.h和TestCPPDLL.cpp两个文件,.h文件是要导出内容的声明文件,为了能清楚的说明问题,我们将TestCPPDLL.h和TestCPPDLL.cpp两个文件中的所有内容都删除,然后在TestCPPDLL.h中添加如下内容:
第一行代码中定义了一个名为"TESTCPPDLL_API"的宏,该宏对应的内容是"__declspec(dllexport)"意思是将后面修饰的内容定义为DLL中要导出的内容。当然你也可以不使用这个宏,可以直接将"__declspec(dllexport)"写在要导出的函数前面。
第二行中的"EXTERN_C",是在"winnt.h"中定义的宏,在函数前面添加"EXTERN_C"等同于在函数前面添加extern "C",意思是该函数在编译和连接时使用C语言的方式,以保证函数名字不变。
第二行的代码是一个函数的声明,说明该函数可以被模块外部调用,其定义实现在TestCPPDLL.cpp中,TestCPPDLL.cpp的代码如下所示:
第三步:
在编译C++DLL之前,需要做以下配置,在项目属性对话框中选择"C/C++"|"Advanced",将Compile AS 选项的值改为"C++"。然后确定,并编译。
生成的DLL文件如下图所示:
第四步:
首先,添加一个C#的应用程序,如果要在C#中调用C++的DLL文件,先要在C#的类中添加一个静态方法,并且使用DllImportAttribute对该方法进行修饰,代码如下所示:
DllImport中的第一个参数是指明DLL文件的位置,第二个参数"EntryPoint"用来指明对应的C/C++中的函数名称是什么。"extern"关键字表明该处声明的这个Add方法是一个外部调用。
该方法声明完毕之后,就可以像调用一个普通的静态方法一样去使用了。
下面是示例程序:
class Program
{
[DllImport(@"E:\ex\TestCPPDLL\Debug\TestCPPDLL.dll",
EntryPoint = "Add")]
extern static int Add(int a, int b);
static void Main(string[]
args)
{
int c = Add(1,2);
Console.WriteLine(c);
Console.Read();
}
}
在运行C#程序之前,先要修改C#的项目属性,如下图所示:
将platform target设置为x86,并且允许非安全代码(后面有用)。
然后运行该C#程序,其结果如下图所示:
第五步:
前面的Add方法中传递的是数值类型(int),其他的数据类型,如float,double,和bool类型的传递方式是一样的,下面演示如何传递字符串。
在TestCPPDLL.h中添加一个新的函数声明,代码如下:
*content);
这里的参数是wchar_t类型的指针,对应着C#中的char类型。TestCPPDLL.cpp中添加如下代码:
TESTCPPDLL_API void __stdcall WriteString(wchar_t*content)
{
cout<<content;
}
该代码的功能就是将输入的字符串通过C++在控制台上输出。下面是在C#中的声明:
)]
extern unsafe static void WriteString(char*c);
调用过程如下所示:
//因为使用指针,因为要声明非安全域
unsafe
{
//在传递字符串时,将字符所在的内存固化,
//并取出字符数组的指针
fixed (char* p = &("hello".ToCharArray()[0]))
{
//调用方法
WriteString(p);
}
}
其运行效果如下图所示:
3. 指针的传递
根据前面介绍的数据类型对照表,我们可以直接在方法中传递指针,但是要注意的是我们常常需要将数组的指针(数据入口地址,第一个元素的地址),数据从C/C++到C#时问题不大,但是如果从C#到C/C++时一定要将数组先固化,然后再传递处理。
下面演示如何传递指针,首先在TestCPPDLL.h中添加下列声明:
//传入一个整型指针,将其所指向的内容加1
EXTERN_C TESTCPPDLL_API void __stdcall AddInt(int *i);
//传入一个整型数组的指针以及数组长度,遍历每一个元素并且输出
EXTERN_C TESTCPPDLL_API void __stdcall AddIntArray(int *firstElement,int arraylength);
//在C++中生成一个整型数组,并且数组指针返回给C#
EXTERN_C TESTCPPDLL_API int* __stdcall GetArrayFromCPP();
其实现写在TestCPPDLL.cpp中,代码如下所示:
TESTCPPDLL_API void __stdcall AddInt(int *i)
{
(*i)++;
}
TESTCPPDLL_API void __stdcall AddIntArray(int *firstElement,int arrayLength)
{
int*currentPointer=firstElement;
for (int i = 0;
i < arrayLength; i++)
{
cout<<*currentPointer;
currentPointer++;
}
cout<<endl;
}
int *arrPtr;
TESTCPPDLL_API int* __stdcall GetArrayFromCPP()
{
arrPtr=new int[10];
for (int i = 0;
i < 10; i++)
{
arrPtr[i]=i;
}
return arrPtr;
}
对应调用的C#代码如下所示:
[DllImport(@"E:\ex\TestCPPDLL\Debug\TestCPPDLL.dll",
EntryPoint = "AddInt")]
extern unsafe static void AddInt(int*
i);
[DllImport(@"E:\ex\TestCPPDLL\Debug\TestCPPDLL.dll",
EntryPoint = "AddIntArray")]
extern unsafe static void AddIntArray(int*
firstElement, int arraylength);
[DllImport(@"E:\ex\TestCPPDLL\Debug\TestCPPDLL.dll",
EntryPoint = "GetArrayFromCPP")]
extern unsafe static int*
GetArrayFromCPP();
调用过程如下所示:
unsafe
{
// 调用C++中的AddInt方法
int i = 10;
AddInt(&i);<