C#與C/C++的交互

C#與C/C++的交互html

最近在編寫Warensoft3D遊戲引擎,並預計明年年初發布測試版本,底層引擎使用DirectX和MONO來編寫,上層的邏輯使用C#來編寫,所以編寫了大量C#與C++互調的代碼,如今經驗寫出來與你們分享,並但願後來者少走彎路。ios

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++方式導出,那個函數的名字就會變得亂七八糟,咱們的程序就沒法找到入口點了。spa

本文將說明如下幾點:線程

  • 互調的基本原理
  • 基本數據類型的傳遞
  • 指針的傳遞
  • 函數指針的傳遞
  • 結構體的傳遞

 

 

  1. 互調的基本原理

    首先,咱們來看一個再常規不過的概念—"數據類型"

    咱們知道在大多數的靜態語言中定義變量的時候都要先指定其數據類型,所謂數據類型,都是人們強加的一個便於記憶的名稱,究其本質就是指明瞭這個數據在內存裏究竟是佔用了幾個字節,程序在運行的時候,首先找到這個數據的地址,而後再按着該類型的長度,讀取相對應的內存,而後再處理。

    瞭解了前面這個事兒,全部編程語言之間進行互調就有點門道兒了。對於不一樣語言之間的互調,只要將該數據的指針(內存地址)傳遞給另外一個語言,在另外一個語言中根據通訊協議將指針所指向的數據存儲入長度對應的數據類型便可,固然要知足如下幾點:

    1. 對於像Java,.NET這樣有運行時虛擬機編程語言來說,因爲虛擬機會讓堆內存來回轉移,所以,在進行互調的時候,要保證正在被互調的數據所在的內存必定要固定,不能被轉移。
    2. 有一些編程語言支持指針,有一些語言不支持指針(如Java),這個問題並不重要,所謂指針,其實就是一個內存地址,對於32位OS的指針是一個32位整數,而對於64位機OS的指針是一個64位整數。由於大多數語言中都有整型數,因此能夠利用整型來接收指針。
  2. 基本數據類型的傳遞

互調過程當中,最基本要傳遞的無非是數值和字符,即: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);

Console.WriteLine(i);

 

//調用C++中的AddIntArray方法將C#中的數據傳遞到C++中,並在C++中輸出

int[] CSArray = new int[10];

for (int iArr = 0; iArr < 10; iArr++)

{

CSArray[iArr] = iArr;

}

fixed (int* pCSArray = &CSArray[0])

{

AddIntArray(pCSArray, 10);

}

//調用C++中的GetArrayFromCPP方法獲取一個C++中創建的數組

int* pArrayPointer = null;

pArrayPointer = GetArrayFromCPP();

for (int iArr = 0; iArr < 10; iArr++)

{

Console.WriteLine(*pArrayPointer);

pArrayPointer++;

}

}

4. 函數指針的傳遞

前面說明的都是簡單數據類型的及其指針的傳遞,利用PInvoke咱們也能夠實現函數指針的傳遞,C#中並無函數指針的概念,可是可使用委託(delegate)來代替函數指針,關於C#中委託的說明,能夠參考筆者前面的一個文章:《C#委託及事件》

你們可能會問,爲何要傳遞函數指針呢?利用PInvoke能夠實現C#對C/C++函數的調用,反過來,咱們能不能在C/C++程序運行的某一時刻,來調用一個C#對應的函數呢?(例如在C++中存在一個獨立線程,該線程可能在任意時刻觸發一個事件,而且須要通知C#)。這個時候,咱們就有必要將一個C#中已經指向某一個函數的函數指針(委託)傳遞給C++。

想要傳遞函數指針,首先要在C#中定義一個委託,而且在C++中定義一個函數指針,同時要保證委託和函數指針具有相同的函數原型,咱們首先編寫C#的代碼,以下所示:

//定義一個委託,返回值爲空,存在一個整型參數

public delegate void CSCallback(int tick);

//定義一個用於回調的方法,與前面定義的委託的原型同樣

//該方法會被C++所調用

static void CSCallbackFunction(int tick)

{

Console.WriteLine(tick.ToString ());

}

//定義一個委託類型的實例,

//在主程序中該委託實例將指向前面定義的CSCallbackFunction方法

static CSCallback callback;

 

在CS的主程序中讓callback指向CSCallbackFunction方法,代碼以下所示:

//調用委託所指向的方法

callback = CSCallbackFunction;

 

 

而後在C/C++中定義一個函數指針,而且添加一個用於設置函數指針的函數,TestCPPDLL.h中的代碼以下所示:

//定義一個函數指針

typedef void (__stdcall *CPPCallback)(int tick);

//定義一個用於設置函數指針的方法,

//並在該函數中調用C#中傳遞過來的委託

EXTERN_C TESTCPPDLL_API void SetCallback(CPPCallback callback);

SetCallback函數的實如今TestCPPDLL.cpp中,代碼以下所示:

TESTCPPDLL_API void SetCallback(CPPCallback callback)

{

    int tick=rand();

    //下面的代碼是對C#中委託進行調用

    callback(tick);

}

在C#中添加SetCallback函數的聲明,代碼以下所示:

//這裏使用CSCallback委託類型來兼容C++裏的CPPCallback函數指針

[DllImport(@"E:\ex\TestCPPDLL\Debug\TestCPPDLL.dll", EntryPoint = "SetCallback")]

extern static void SetCallback(CSCallback callback);

在C#中的調用過程以下所示:

//讓委託指向將被回調的方法

callback = CSCallbackFunction;

//將委託傳遞給C++

SetCallback(callback);

SetCallback方法被執行後,在C#中定義的CSCallbackFunction就會被C++所調用。

5. 結構體的傳遞

傳遞結構體的想法和傳遞一個int類型數據相似,struct中的數據是在內存中順序排列的,只要保證保證如下幾點,就能夠直接傳遞結構體,甚至是結構體的指針:

  • 要傳遞的成員爲公有的值類型字段
  • C#中結構體字段類型與C++結構體中的字段類型相兼容
  • C#結構中的字段順序與C++結構體中的字段順序相同,要保證該功能,須要將C#結構體標記爲[StructLayout( LayoutKind.Sequential)]

下面經過代碼進行說明,首先在C#中添加一個結構體,代碼以下所示:

[StructLayout( LayoutKind.Sequential)]

struct Vector3

{

public float X, Y, Z;

}

該結構體表示一個3D向量,包括X,Y,Z三個float類型的份量。

而後在TestCPPDLL.h中也定義一個相同結構的結構體,代碼以下所示:

struct Vector3

{

    float X,Y,Z;

};

在TestCPPDLL.h中聲明一個用於傳遞Vector3結構體的一個函數,代碼以下所示:

EXTERN_C TESTCPPDLL_API void __stdcall SendStructFromCSToCPP(Vector3 vector);

在TestCPPDLL.cpp中將其實現,代碼以下所示:

TESTCPPDLL_API void __stdcall SendStructFromCSToCPP(Vector3 vector)

{

    cout<<"got vector3 in cpp,x:";

    cout<<vector.X;

    cout<<",Y:";

    cout<<vector.Y;

    cout<<",Z:";

    cout<<vector.Z;

}

在C#中添加對SendStructFromCSToCPP函數的聲明,代碼以下所示:

[DllImport(@"E:\ex\TestCPPDLL\Debug\TestCPPDLL.dll", EntryPoint = "SendStructFromCSToCPP")]

extern static void SendStructFromCSToCPP(Vector3 vector);

C#中的調用過程以下所示:

//創建一個Vector3的實例

Vector3 vector = new Vector3() { X =10,Y=20,Z=30 };

//將vector傳遞給C++並在C++中輸出

SendStructFromCSToCPP(vector);

基輸出效果以下所示:

 

完整的TestCPPDLL.h代碼以下所示:

 

複製代碼
#define TESTCPPDLL_API __declspec(dllexport)

EXTERN_C TESTCPPDLL_API int __stdcall Add(int a,int b);

EXTERN_C TESTCPPDLL_API void __stdcall WriteString(wchar_t*content);

//傳入一個整型指針,將其所指向的內容加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();



//定義一個函數指針

typedef void (__stdcall *CPPCallback)(int tick);

//定義一個用於設置函數指針的方法,

//並在該函數中調用C#中傳遞過來的委託

EXTERN_C TESTCPPDLL_API void __stdcall SetCallback(CPPCallback callback);



struct Vector3

{

float X,Y,Z;

};

EXTERN_C TESTCPPDLL_API void __stdcall SendStructFromCSToCPP(Vector3 vector);
複製代碼



 

完整的TestCPPDLL.CPP代碼以下所示:

 

複製代碼
#include "stdafx.h"

#include <iostream>

#include "TestCPPDLL.h"

using namespace std;

TESTCPPDLL_API int __stdcall Add(int a,int b)

{

return a+b;

}

TESTCPPDLL_API void __stdcall WriteString(wchar_t*content)

{

wprintf(content);

printf("\n");

}



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;

}



TESTCPPDLL_API void __stdcall SetCallback(CPPCallback callback)

{

int tick=100;

//下面的代碼是對C#中委託進行調用

callback(tick);

}



TESTCPPDLL_API void __stdcall SendStructFromCSToCPP(Vector3 vector)

{

cout<<"got vector3 in cpp,x:";

cout<<vector.X;

cout<<",Y:";

cout<<vector.Y;

cout<<",Z:";

cout<<vector.Z;

}
複製代碼



 

 

完整的C#代碼以下所示:

 

複製代碼
using System;

using System.Collections.Generic;

using System.Runtime.InteropServices;

using System.Text;



namespace ConsoleApplication1

{

class Program

{

[DllImport(@"E:\ex\TestCPPDLL\Debug\TestCPPDLL.dll", EntryPoint = "Add")]

extern static int Add(int a, int b);

[DllImport(@"E:\ex\TestCPPDLL\Debug\TestCPPDLL.dll", EntryPoint = "WriteString")]

extern unsafe static void WriteString(char* 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();







//定義一個委託,返回值爲空,存在一個整型參數

public delegate void CSCallback(int tick);

//定義一個用於回調的方法,與前面定義的委託的原型同樣

//該方法會被C++所調用

static void CSCallbackFunction(int tick)

{

Console.WriteLine(tick.ToString());



}

//定義一個委託類型的實例,

//在主程序中該委託實例將指向前面定義的CSCallbackFunction方法

static CSCallback callback;





//這裏使用CSCallback委託類型來兼容C++裏的CPPCallback函數指針

[DllImport(@"E:\ex\TestCPPDLL\Debug\TestCPPDLL.dll", EntryPoint = "SetCallback")]

extern static void SetCallback(CSCallback callback);



[StructLayout(LayoutKind.Sequential)]

struct Vector3

{

public float X, Y, Z;

}



[DllImport(@"E:\ex\TestCPPDLL\Debug\TestCPPDLL.dll", EntryPoint = "SendStructFromCSToCPP")]

extern static void SendStructFromCSToCPP(Vector3 vector);



static void Main(string[] args)

{

int c = Add(1, 2);

Console.WriteLine(c);

//由於使用指針,由於要聲明非安全域

unsafe

{

//在傳遞字符串時,將字符所在的內存固化,

//並取出字符數組的指針

fixed (char* p = &("hello".ToCharArray()[0]))

{

//調用方法

WriteString(p);

}



}

unsafe

{

// 調用C++中的AddInt方法

int i = 10;



AddInt(&i);

Console.WriteLine(i);



//調用C++中的AddIntArray方法將C#中的數據傳遞到C++中,並在C++中輸出

int[] CSArray = new int[10];

for (int iArr = 0; iArr < 10; iArr++)

{

CSArray[iArr] = iArr;

}

fixed (int* pCSArray = &CSArray[0])

{

AddIntArray(pCSArray, 10);

}

//調用C++中的GetArrayFromCPP方法獲取一個C++中創建的數組

int* pArrayPointer = null;

pArrayPointer = GetArrayFromCPP();

for (int iArr = 0; iArr < 10; iArr++)

{

Console.WriteLine(*pArrayPointer);

pArrayPointer++;

}

}









//讓委託指向將被回調的方法

callback = CSCallbackFunction;

//將委託傳遞給C++

SetCallback(callback);



//創建一個Vector3的實例

Vector3 vector = new Vector3() { X = 10, Y = 20, Z = 30 };

//將vector傳遞給C++並在C++中輸出

SendStructFromCSToCPP(vector);





Console.Read();

}

}

}
複製代碼