現在比較流行C#與C++融合:C#做GUI,開發效率高,C++做運算,運行效率高,二者兼得。 但是C++與C#必然存在數據交互,C#與C++dll的數據交互從來都是一個讓人頭疼的問題。 從調用方式看也有兩種情況: 1、C#調用C++函數 這種情況用的比較多,數據流向可以是C#流向C++,通過參數將數 ...
現在比較流行C#與C++融合:C#做GUI,開發效率高,C++做運算,運行效率高,二者兼得。
但是C++與C#必然存在數據交互,C#與C++dll的數據交互從來都是一個讓人頭疼的問題。
從調用方式看也有兩種情況:
1、C#調用C++函數
這種情況用的比較多,數據流向可以是C#流向C++,通過參數將數據傳遞給C++(如:SetData(double[] data));也可以是C++流向C#(如:GetData(double[] data))。
2、C++ Callback
這種情況是C++中通過Callback的方式調用C#代碼,類似於C++做過一些處理後向C#發送事件,事件可以攜帶數據(如處理後的數據)。則C++中定義函數指針的方式是:
typedef void(*Render)(double* data, BOOL* color);
C#作為委托,定義的函數被C++ callback:
public delegate void RenderCallback([MarshalAs(UnmanagedType.LPArray, SizeConst =23)]double[] data, [MarshalAs(UnmanagedType.LPArray, SizeConst = 23)]int[] colors);
千萬註意,delegate中的double[]數組一定要加上MarshalAs標記,標記為傳遞數組,而且必須指定傳遞的數量,如果不標記數量,則每次只傳遞一個數值,這個問題折磨我很久才搞定!
其他註意事項:
1、如何在C#中保持C++的函數指針
回調函數的另一個註意事項是向C++ dll傳遞迴調函數指針的問題
假設有個函數向C++dll傳遞指針:
public delegate void EKFRenderCallback(string data, string colors); public class EKFLib { [DllImport("EKFLib.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] public static extern void SetRenderCallback(EKFRenderCallback render);
C#中如下傳遞被回調的函數:
public void RenderCallback(string data, string color) { // rendering } private void Window_Loaded(object sender, RoutedEventArgs e) { EKFLib.SetRenderCallback(RenderCallback); EKFLib.Init(); }
這雖然沒什麼問題,但是通過SetRenderCallback()傳入到C++的指針不受托管代碼管理,在C#中認為此指針對象未被任何代碼引用,GC做垃圾回收時,將會把C#本地的空指針回收,導致C++無法執行回調,出現“CallbackOnCollectedDelegate”錯誤:
對“MotionCapture!MotionCapture.EKFRenderCallback::Invoke”類型的已垃圾回收委托進行了回調。這可能會導致應用程式崩潰、損壞和數據丟失。向非托管代碼傳遞委托時,托管應用程式必須讓這些委托保持活動狀態,直到確信不會再次調用它們。
微軟官網的例子是控制GC回收機制,這是個比較笨拙的方法,更加理所當然的方法是把委托定義成一個屬性,指向一個new出來的callback,然後再把這個callback傳遞進C++dll中,這樣,在C#端有對象引用,保證了GC不會回收此callback:
public void RenderCallback(string data, string color) { // rendering } private EKFRenderCallback render; private void Window_Loaded(object sender, RoutedEventArgs e) { render = new EKFRenderCallback(RenderCallback); EKFLib.SetRenderCallback(render); EKFLib.Init(); }
2、__stdcall與_cdecl傳遞數據
最近一個項目是通過C++ 的 dll做高速運算,然後把結果數據通過Callback的方式回調給C#(界面部分),結果總是在C#中接到回調事件後就直接掛掉(程式直接在毫無提示的情況下退出,沒有任何調試信息或者提示)。
導致問題的原因是,預設情況下,C++中如下定義的函數指針,預設是以_cdecl方式調用的:
typedef void(*Render)(double* data, BOOL* color);
這種情況下,參數堆棧是由調用者(C++一側)維護的,在C++調用此回調函數後,會把參數彈出堆棧而釋放,導致C#讀取數據時出現莫名其妙的錯誤。
以上是回調函數傳遞數組可能出現的情況,而如下所示,只傳遞一個參數的情況,甚至會在C#方莫名其妙的卡死:
typedef void (*CalibrationProgressCallback)(double percent);改為__stdcall的方式即可解決問題,申明如下:
typedef void(__stdcall *Render)(double* data, BOOL* color);
以下來自網路的一段_cdecl和__stdcall的解釋,必須牢記:
1. __cdecl
即所謂的C調用規則,按從右至左的順序壓參數入棧,由調用者把參數彈出棧。切記:對於傳送參數的記憶體棧是由調用者來維護的。返回值在EAX中。因此,對於象printf這樣變參數的函數必須用這種規則。編譯器在編譯的時候對這種調用規則的函數生成修飾名的餓時候,僅在輸出函數名前加上一個下劃線首碼,格式為_functionname。
2. __stdcall
按從右至左的順序壓參數入棧,由被調用者把參數彈出棧。_stdcall是Pascal程式的預設調用方式,通常用於Win32 Api中,切記:函數自己在退出時清空堆棧,返回值在EAX中。 __stdcall調用約定在輸出函數名前加上一個下劃線首碼,後面加上一個“@”符號和其參數的位元組數,格式為_functionname@number。如函數int func(int a, double b)的修飾名是_func@12
所以,從C++ dll中回調函數給C#傳遞數據,必須由C#函數在使用完數據後(退出函數時)自己清空堆棧!所C++中的回調函數指針應該如下定義:
typedef void (_stdcall *CalibrationProgressCallback)(double percent);
總結:
C++通過callback向C#傳遞數據必須註意以下幾點:
1、C++中的回調函數必須用_stdcall標記,使用stdcall方式回調;
2、如果是數組,必須用 [MarshalAs(UnmanagedType.LPArray, SizeConst = 23)]標記參數,指定為數組且標記數組長度;
3、C#方必須申明一個變數,用來指向C++的回調指針函數,避免被C#回收掉。