C#/C++混合編程一二事
C#/C++混編的情形經(jīng)常會(huì)碰到反砌,下面就來講一講一些需要注意的點(diǎn)。廢話不多說膀藐,Let's get started. (時(shí)間有限屠阻,暫時(shí)沒有寫完,后續(xù)會(huì)持續(xù)更細(xì)额各。如果有寫的不嚴(yán)謹(jǐn)甚至錯(cuò)誤的地方歡迎大家指正)
一国觉、C++導(dǎo)出函數(shù)的聲明
在導(dǎo)出函數(shù)的頭文件,經(jīng)常見到如下的模板
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#ifdef TEST_EXPORTS
#define TEST_API __declspec(dllexport)
#else
#define TEST_API __declspec(dllimport)
#endif
#define CALLINGCONVENTION __cdecl
//導(dǎo)出的函數(shù)一
TEST_API int CALLING_CONVENTION Add(int a, int b);
//導(dǎo)出的函數(shù)二
TEST_API int CALLING_CONVENTION Subtract(int a, int b);
#ifdef __cplusplus
};
#endif
下面就來給出解釋虾啦。
#ifdef __cplusplus這條預(yù)編譯指令麻诀。如果項(xiàng)目是C++的項(xiàng)目痕寓,則用extern "C" {} 將所有的代碼包起來。這樣的好處是導(dǎo)出的函數(shù)名仍和代碼中定義的一致蝇闭,否則函數(shù)名前后會(huì)加上一些看起來很奇怪的符號(hào)呻率。這是因?yàn)镃++的函數(shù)有重載機(jī)制,同一個(gè)函數(shù)名呻引,可以有多種參數(shù)形式礼仗,不加extern "C"包起來的話记餐,編譯器就會(huì)在函數(shù)名前后加上一些符號(hào)汹桦,來具體到這個(gè)函數(shù)的具體的某一種形式既绕。
#ifdef TEST_EXPORTS這條預(yù)編譯指令网缝。這段話的意思就是如果定義了TEST_EXPORTS這個(gè)宏肥哎,則把TEST_API這個(gè)宏定義成__declspec(dllexport)挽封,否則把TEST_API定義成__declspec(dllimport)墨林。這個(gè)設(shè)計(jì)是為了開發(fā)者和調(diào)用者的便利性設(shè)計(jì)的巍沙。因?yàn)檫@個(gè)頭文件饥伊,后來也會(huì)拷貝給調(diào)用者用慎恒,開發(fā)者的導(dǎo)出函數(shù)的這個(gè)項(xiàng)目中定義一個(gè)宏TEST_EXPORTS,而調(diào)用者那邊沒有這個(gè)宏撵渡。所以TEST_API對(duì)于開發(fā)者就是__declspec(dllexport)融柬,對(duì)于調(diào)用者就是__declspec(dllimport)。__declspec(dllexport)告訴編譯器這是一個(gè)需要導(dǎo)出的函數(shù)趋距,__declspec(dllimport)告訴編譯器這是一個(gè)從外部的庫文件中導(dǎo)入的函數(shù)粒氧。
#define CALLINGCONVENTION _cdecl 這個(gè)宏,就是調(diào)用約定节腐,可以根據(jù)需要把它定義成__cdecl或者_(dá)_stdcall或者其他外盯。調(diào)用約定關(guān)系到函數(shù)參數(shù)入棧和出棧的清理方式。常見的有__cdecl翼雀、_stdcall饱苟、_fastcall. 最常用的就是_cdecl、_stdcall. 如果調(diào)用方用C#狼渊,則推薦用_cdecl. 因?yàn)開stdcall也會(huì)像沒加extern "C" 那樣導(dǎo)致導(dǎo)出的函數(shù)名和頭文件中定義的不一致箱熬。(__cdecl許多人記不住,其實(shí)它就是C Declaration的縮寫狈邑。
二城须、C++導(dǎo)出函數(shù)的實(shí)現(xiàn)
聲明寫好了,實(shí)現(xiàn)就簡(jiǎn)單了米苹。參考如下糕伐,不解釋。
#include “相關(guān)的頭文件"
//加法
TEST_API int CALLING_CONVENTION Add(int a, int b)
{
return a + b;
}
//減法
TESTT_API void CALLING_CONVENTION Subtract(int a, int b)
{
return a - b;
}
三蘸嘶、C#導(dǎo)入函數(shù)的聲明
C#調(diào)用之前良瞧,先寫一個(gè)類陪汽,里面用來聲明從C++導(dǎo)入的函數(shù)
class TestApi
{
private const string strDllName = "Test.dll";
private const CallingConvention ccDef = CallingConvention.Cdecl;
private const CharSet csDef = CharSet.Unicode;
[DllImport(strDllName, CallingConvention = ccDef, CharSet = csDef, EntryPoint = "Add")]
public static extern int Add(int a, int b);
[DllImport(strDllName, CallingConvention = ccDef, CharSet = csDef, EntryPoint = "Add")]
public static extern int Subtract(int a, int b);
}
說明:
記得添加命名空間using System.Runtime.InteropServices;
函數(shù)聲明前寫上DllImport屬性,包含導(dǎo)入的庫文件名褥蚯,調(diào)用約定掩缓,字符集,入口點(diǎn)遵岩。庫文件名可以寫相對(duì)路徑你辣,也可以寫絕對(duì)路徑。
調(diào)用約定和C++保持一致尘执,推薦用Cdecl方式舍哄,如果強(qiáng)行用Stdcall會(huì)直接報(bào)錯(cuò)。
字符集可寫可不寫誊锭,一般不會(huì)用上表悬。但是如果C++參數(shù)中有char或者wchart時(shí),就需要根據(jù)實(shí)際情況填寫字符集這個(gè)屬性丧靡。如果參數(shù)為wchar_t*時(shí)蟆沫,指定字符集為Unicode, C#的參數(shù)使用string即可(當(dāng)然也可以使用StringBuilder,具體不討論)温治。當(dāng)然也可以不指定字符集饭庞,在參數(shù)前加上如下聲明[MarshalAs(UnmanagedType.LPWStr)]也可。
入口點(diǎn)熬荆,就是dll中導(dǎo)出的函數(shù)的符號(hào)舟山,當(dāng)且僅當(dāng)調(diào)用約定為__cdecl、并且加了extern "C"包裝的情況下卤恳,入口點(diǎn)才和函數(shù)名相同累盗。導(dǎo)出的函數(shù)名可以通過軟件Dependency Walker查看。
四突琳、參數(shù)的傳遞
基本類型若债,比如int、float拆融、double這些直接傳就好蠢琳,不再贅述。
字符串類型冠息。舉個(gè)例子:
TEST_API void CALLING_CONVENTION DoSomthing(wchat_t* path);
[DllImport(strDllName, CallingConvention = ccDef, CharSet = csDef, EntryPoint = "DoSomthing")]
public static extern void DoSomthing(string path);
//說明:C++聲明用char*挪凑、wchart孕索,則C#端用string或者StringBuilder(可以自行嘗試逛艰,不贅述)類型傳遞,
//但是需要指定string傳遞給char或者wchar_t*的編碼方式搞旭,
//指定有兩種方式散怖。一種是在DllImport中寫Charset = Charset.Unicode(對(duì)應(yīng)wchar_t*),
//Charset = Charset.Ansi(對(duì)應(yīng)char*)菇绵,
//另一種是在string前加屬性[MarshalAs(UnmanagedType.LPWStr)](對(duì)應(yīng)wchar_t*),
//或者[MarshalAs(UnmanagedType.LPStr)](對(duì)應(yīng)char*)
- 一般指針镇眷,比如void咬最、自定義類的指針YourClass、句柄如HANDLE欠动、HWND等永乌。舉個(gè)例子
typedef void* IDicomSCU;
TEST_API void CALLING_CONVENTION IDicomSCU_Close(IDicomSCU handle);
//C#:
[DllImport(strDllName, CallingConvention = ccDef, CharSet = csDef, EntryPoint = "IDicomSCU_Close")]
public static extern void IDicomSCU_Close(IntPtr handle);
//說明:既然這么多種東西都可以用C#的IntPtr來傳遞
//那在編碼時(shí)一定需要注意IntPtr具體指向的一個(gè)什么東西
//切不可混用。
- 函數(shù)指針具伍。這種情形翅雏,C++需要一個(gè)函數(shù)指針,C#傳委托即可人芽。舉個(gè)例子
//聲明函數(shù)指針
typedef bool(*IDriverCommandEvent)(int command, wchar_t *val);
TEST_API bool CALLING_CONVENTION IDriver_Open(HWND hWnd, IDriverCommandEvent event);
//先聲明一個(gè)委托望几,注意委托的參數(shù)中有string類型,我就在前方加上了[MarshalAs(UnmanagedType.LPWStr)]以和C++的wchar_t*對(duì)應(yīng)
//并且聲明這個(gè)委托的調(diào)用約定是Cdecl還是Stdcall萤厅,這個(gè)很重要橄抹,必須和C++端的調(diào)用約定保持一致。
[UnmanagedFunctionPointerAttribute(CallingConvention.Cdecl)]
public delegate bool DriverCommandEventHandler(int command, [MarshalAs(UnmanagedType.LPWStr)] string val);
//導(dǎo)出函數(shù)聲明
[DllImport(strDllName, CallingConvention = ccDef, CharSet = csDef, EntryPoint = "IDriver_Open")]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool IDriver_Open(HWND hWnd, DriverCommandEventHandler driverCommandEvent);
//說明:注意C#在傳遞形參的時(shí)候惕味,形參不要是一個(gè)臨時(shí)對(duì)象楼誓,因?yàn)榕R時(shí)對(duì)象的引用計(jì)數(shù)為0后會(huì)被GC回收,
//導(dǎo)致C++調(diào)用這個(gè)函數(shù)指針的時(shí)候找不到而報(bào)錯(cuò)
- 結(jié)構(gòu)體指針名挥。參照C++端定義的結(jié)構(gòu)體也在C#端定義同樣的結(jié)構(gòu)體慌随,具體參見第六節(jié)。
然后C#的形參用ref YourStruct類型躺同,C++端用YourStruct*接收阁猜。
五、返回值的傳遞
基本類型基本可以直接聲明蹋艺,這里就把特殊的返回值拿出來提一下
- bool類型:如果按照常規(guī)的寫法剃袍,返回值總是錯(cuò)誤的。我搜索了stackoverflow捎谨,發(fā)現(xiàn)了這個(gè)問題的解決方案民效,
參考鏈接如下:
https://stackoverflow.com/questions/4608876/c-sharp-dllimport-with-c-boolean-function-not-returning-correctly
因此,聲明就應(yīng)該這樣寫:
TEST_API bool CALLING_CONVENTION DoSomething();
[DllImport(strDllName, CallingConvention = ccDef, CharSet = csDef, EntryPoint ="DoSomething")]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool DoSomething();