C++經過Callback向C#傳遞數據

本文轉自:http://www.roboby.com/c%E9%80%9A%E8%BF%87callback%E5%90%91c%E4%BC%A0%E9%80%92%E6%95%B0%E6%8D%AE.htmlhtml

如今比較流行C#與C++融合:C#作GUI,開發效率高,C++作運算,運行效率高,兩者兼得。數組

可是C++與C#必然存在數據交互,C#與C++dll的數據交互歷來都是一個讓人頭疼的問題。網絡

從調用方式看也有兩種狀況:函數

一、C#調用C++函數指針

這種狀況用的比較多,數據流向能夠是C#流向C++,經過參數將數據傳遞給C++(如:SetData(double[] data));也能夠是C++流向C#(如:GetData(double[] data))。調試

二、C++ Callbackhtm

這種狀況是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標記,標記爲傳遞數組,並且必須指定傳遞的數量,若是不標記數量,則每次只傳遞一個數值,這個問題折磨我好久才搞定!

其餘注意事項:

一、如何在C#中保持C++的函數指針

回調函數的另外一個注意事項是向C++ dll傳遞迴調函數指針的問題

假設有個函數向C++dll傳遞指針:

1
public delegate void EKFRenderCallback(string data, string colors);
2

3
public class EKFLib
4
{
5
[DllImport("EKFLib.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
6
public static extern void SetRenderCallback(EKFRenderCallback render);
C#中以下傳遞被回調的函數:

01
public void RenderCallback(string data, string color)
02
{
03
// rendering
04
}
05

06
private void Window_Loaded(object sender, RoutedEventArgs e)
07
{
08
EKFLib.SetRenderCallback(RenderCallback);
09
EKFLib.Init();
10
}
這雖然沒什麼問題,可是經過SetRenderCallback()傳入到C++的指針不受託管代碼管理,在C#中認爲此指針對象未被任何代碼引用,GC作垃圾回收時,將會把C#本地的空指針回收,致使C++沒法執行回調,出現「CallbackOnCollectedDelegate」錯誤:

對「MotionCapture!MotionCapture.EKFRenderCallback::Invoke」類型的已垃圾回收委託進行了回調。這可能會致使應用程序崩潰、損壞和數據丟失。向非託管代碼傳遞委託時,託管應用程序必須讓這些委託保持活動狀態,直到確信不會再次調用它們。

微軟官網的例子是控制GC回收機制,這是個比較笨拙的方法,更加理所固然的方法是把委託定義成一個屬性,指向一個new出來的callback,而後再把這個callback傳遞進C++dll中,這樣,在C#端有對象引用,保證了GC不會回收此callback:

01
public void RenderCallback(string data, string color)
02
{
03
// rendering
04
}
05

06
private EKFRenderCallback render;
07
private void Window_Loaded(object sender, RoutedEventArgs e)
08
{
09
render = new EKFRenderCallback(RenderCallback);
10
EKFLib.SetRenderCallback(render);
11
EKFLib.Init();
12
}

二、__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#傳遞數據必須注意如下幾點:

一、C++中的回調函數必須用_stdcall標記,使用stdcall方式回調;

二、若是是數組,必須用 [MarshalAs(UnmanagedType.LPArray, SizeConst = 23)]標記參數,指定爲數組且標記數組長度;

三、C#方必須申明一個變量,用來指向C++的回調指針函數,避免被C#回收掉。

相關文章
相關標籤/搜索