托管代码C#调用非托管C++ API, 封送嵌套结构体数组
一、前言:
? ? ? ? ?最近這兩天由于項目需要,提供給客戶的C++ 動態庫需要返回自定義結構體數組,網上也查了很多資料, 推薦一本書, 《精通.NET互操作:P/Invoke、C++ Interop和COM Interop》 , 介紹Windows平臺上的托管代碼與非托管代碼之間進行互操作的各種技術, 雖然里面沒有結構體數組的傳參例子。以前都是返回字節數組的,本以為很簡單,意想不到的是,遇到各種坑。其中一個就是在C#中如何處理結構體嵌套以及定義結構體數組的問題,第二個是如何成功的獲取由C++返回到結構體數組等問題。為了防止自己下次忘記,并重蹈覆轍,因此,寫下這篇文章,記錄下來。
二、相關資料
1、托管代碼與非托管代碼
1.1 托管代碼 (managed code)
.NET Framework的核心是其運行庫的執行環境,稱為公共語言運行庫(CLR)或.NET運行庫。通常將在CLR的控制下運行的代碼稱為托管代碼(managed code)。
運行庫環境(而不是直接由操作系統)執行的代碼。托管代碼應用程序可以獲得公共語言運行庫服務,例如自動垃圾回收、運行庫類型檢查和安全支持等。這些服務幫助提供獨立于平臺和語言的、統一的托管代碼應用程序行為。
1.2 非托管代碼 (unmanaged code)
在公共語言運行庫環境的外部,由操作系統直接執行的代碼。非托管代碼必須提供自己的垃圾回收、類型檢查、安全支持等服務;它與托管代碼不同,后者從公共語言運行庫中獲得這些服務。
1.3 什么是托管?托管是什么意思?
托管代碼就是基于.net元數據格式的代碼,運行于.net平臺之上,所有的與操作系統的交換有.net來完成,就像是把這些功能委托給.net,所以稱之為托管代碼。非托管代碼則反之。
1.4 托管代碼如何調用非托管代碼(c sharp如何調用c++代碼)?
兩種常用的做法:
1. COM interop。
2. P/Invoke
a. 在托管客戶端增加一條 DllImport語句和一個方法的調用。(摘自:超詳細解析托管與非托管)
下面主要講解P/Invoke方式。
2、P/Invoke:C#調用C++
P/Invoke的全稱是Platform?Invoke?(平臺調用)?它實際上是一種函數調用機制通 過P/Invoke我們就可以調用非托管DLL中的函數。
P/Invoke依次執行以下操作:
1.?查找包含該函數的非托管DLL
2.?將該非托管DLL加載到內存中
3.?查找函數在內存中的地址并將其參數按照函數的調用約定壓棧
4.?將控制權轉移給非托管函數
DllImport: 用來標識該方法是非托管的代碼方法,在編譯器編譯的時候它能夠正確的認識出被該特性標記的是外來代碼段。當到達程序運行的時候,也能夠正確的認識出該代碼是引用非托管的代碼,這樣CLR會去加載非托管DLL文件,然后查找到入口點進行調用(關于此部分,《超詳細解析托管與非托管》有詳細說明)
CallingConvention:在平臺調用的過程中起到查找入口點的作用,在托管代碼進行非托管代碼入口點查找時,會通過CallingConvention中的值進行確認非托管入口點的調用約定。
3、非托管函數和托管方法中數據類型對應
3.1 將C/C++ 等托管代碼API中數據類型與C#托管方法數據類型進行轉換,是數據封送。
對于每個 .NET Framework 類型均有一個默認非托管類型,公共語言運行庫將使用此非托管類型在托管到非托管的函數調用中封送數據。string 類型默認非托管類型是LPTSTR,可以在非托管函數的 C# 聲明中使用?MarshalAs?屬性重寫默認封送處理。例如:
[DllImport("msvcrt.dll")]
public static extern int puts([MarshalAs(UnmanagedType.LPStr)]?string m);
puts?函數的參數的默認封送處理已從默認值 LPTSTR 重寫為 LPSTR。
修改默認封送有什么用?
默認情況下,本機結構和托管結構在內存中的布局有所不同,因此,若要跨托管/非托管邊界成功傳遞結構,需要執行一些額外步驟來保持數據的完整性。(摘自:《.NET學習之路----我對P/Invoke技術的理解(一)》)
因此,我傳遞結構體也是一樣,由于自定義API中使用一個結構體,那么C#中沒有一個對應的托管類型與之對應,需要為用戶定義的結構指定自定義封送處理需要對參數進行處理。
可以為傳遞到非托管函數或從非托管函數返回的結構體和類的字段指定自定義封送處理屬性。通過向結構體或類的字段中添加 MarshalAs 屬性可以做到這一點。還必須使用 StructLayout 屬性設置結構體或者類的布局,還可以控制字符串成員的默認封送處理,并設置默認封裝大小。
這篇文章《MarshalAs的使用》有關于MarshalAs 詳細說明。
3.2 封送,在此看來算是傳數據,但是為什么要封送呢?封送是為了在托管內存和非托管內存中正常傳遞數據。有時,出于對性能的考慮,會對托管結構的成員進行重新排列,因此有必要使用?StructLayoutAttribute?特性指示該結構為順序布局。?將結構封裝設置顯式設置為與本機結構所使用的設置相同的設置也是一個好辦法。(摘自:《.NET學習之路----我對P/Invoke技術的理解(一)》)
其中,這個還是涉及C/C++結構體內存對齊,
根據相關資料,摘自《C#調用C++ dll時,結構體引用傳參的方法》
在C/C++中,struct類型中的成員的一旦聲明,則實例中成員在內存中的布局(Layout)順序(C/C++結構體內存對齊)就定下來了,即與成員聲明的順序相同,并且在默認情況下總是按照結構中占用空間最大的成員進行對齊(Align);
然而在.net托管環境中,CLR提供了更自由的方式來控制struct中Layout:我們可以在定義struct時,在struct上運用StructLayoutAttribute特性來控制成員的內存布局。默認情況下,struct實例中的字段在棧上的布局(Layout)順序與聲明中的順序相同,即在struct上運用[StructLayoutAttribute(LayoutKind.Sequential)]特性,這樣做的原因是結構常用于和非托管代碼交互的情形。如果我們正在創建一個與非托管代碼沒有任何互操作的struct類型,我們很可能希望改變C#編譯器的這種默認規則,因此LayoutKind除了Sequential成員之外,還有兩個成員Auto和Explicit,給StructLayoutAttribute傳入LayoutKind.Auto可以讓CLR按照自己選擇的最優方式來排列實例中的字段;傳入LayoutKind.Explicit可以使字段按照我們的在字段上設定的FieldOffset來更靈活的設置字段排序方式,但這種方式也挺危險的,如果設置錯誤后果將會比較嚴重。
因此,把結構體顯示的聲明為? [StructLayout(LayoutKind.Sequential)]即可.
默認(LayoutKind.Sequential)情況下,CLR對struct的Layout的處理方法與C/C++中默認的處理方式相同,即按照結構中占用空間最大的成員進行對齊(Align);?
使用LayoutKind.Explicit的情況下,CLR不對結構體進行任何內存對齊(Align),而且我們要小心就是FieldOffset;?
使用LayoutKind.Auto的情況下,CLR會對結構體中的字段順序進行調整,使實例占有盡可能少的內存,并進行byte的內存對齊(Align)。
3.3?數據封送中指針處理
數據封送中指針處理的兩種情況 :
3.3.1?普通指針
在非托管代碼中,基本數據類型對應的指針變量和數組,及單個結構體或類的指針,在C#中使用ref或out來實行封送。3,
3.3.2?Handle類型和自定義結構和類等數組
C#中使用IntPtr來進行封送。
三、托管代碼C#與非托管C++ 嵌套結構體的對應
1、在最近的項目中,非托管C++ 的結構體定義如下:
typedef struct CARDINFOBEAN {unsigned char channel; ? ?// 通道 0~23unsigned char exist; ? ? ?// 0---當前通道沒有掃描到卡,1---當前通道掃描到卡unsigned char snlen; ? ? ?// 0---序列號4字節, 1---序列號7字節unsigned char alarm; ? ? ?// 報警unsigned char cardtype;?? // 卡片類型unsigned char sn[7];?? ? ?// 卡號 }CardInfoBean;typedef struct PROTOCOLBEAN {unsigned char cardInfoBeanListCount;?? ??? ??? ? //卡片信息結構體實際個數int protocolLength;?? ??? ??? ??? ??? ??? ? //協議數據長度int channel;?? ??? ??? ??? ??? ??? ??? ? //天線通道號unsigned char protocol[1024];?? ??? ??? ??? ??? ? //協議數據CardInfoBean cardInfoBeanList[255];?? ??? ? //天線號每個對應的卡片信息 }ProtocolBean;在.h中API聲明如下:
ReaderDLL int CallReader CardTest(UCHAR addr, UCHAR testCardType, ProtocolBean *protocolBeanList, int *protocolBeanListCount, int maxLength);在上面的聲明中,使用到了結構體的嵌套,API中通過指針返回嵌套結構體數組。
2、項目中需要使用C#調用C++ 中CardTest() API ,這個時候就遇到了,非托管代碼與托管代碼中自定義數據類型的對應和封送問題。
在前面的篇幅中,已經講解的很明了,因此不做過多的敘述。
2.1 在C#對應的結構體定義如下:
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Ansi)] public struct CARDINFOBEAN {/// unsigned charpublic byte channel;/// unsigned charpublic byte exist;/// unsigned charpublic byte snlen;/// unsigned charpublic byte alarm;/// unsigned charpublic byte cardtype;/// unsigned char[7][System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.ByValArray, SizeConst = 7)]public byte[] sn;}[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Ansi)] public struct PROTOCOLBEAN {/// unsigned charpublic byte cardInfoBeanListCount;/// intpublic int protocolLength;/// intpublic int channel;/// unsigned char[1024]? ? ? [System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.ByValArray, SizeConst = 1024)]public byte[] protocol;/// CardInfoBean[255] ? [System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.ByValArray, SizeConst = 255, ArraySubType = System.Runtime.InteropServices.UnmanagedType.Struct)]public CARDINFOBEAN[] cardInfoBeanList; }在結構體CARDINFOBEAN中,對于基本數據類型只要與c++中的基本數據類型對應即可,需要特別說明的是基本數據類型數組的對應,默認在C#結構體中,與C++ 對應數組都需要使用MarshalAsAttribute指示指示如何在托管代碼和非托管代碼對應,并且使用UnmanagedType 的ByValArray?用于在結構體中出現的數組,應始終使用MarshalAsAttribute的SizeConst字段來指示數組的大小。
驗證結構體的大小是否正確,可以使用Marshal.SizeOf(),即獲取對象的非托管大小時,獲得的是自己在C#定義的大小,然后使用sizeof()計算C/C++結構體的大小,即獲取非托管的大小,對比是否一致。
在結構體PROTOCOLBEAN中,cardInfoBeanList是嵌套了CARDINFOBEAN的結構體數組,所以對應C#的數組,同樣需要使用MarshalAsAttribute指定類型為ByValArray ,并且使用SizeConst字段來指示數組的大小。同樣也需要使用ArraySubType = System.Runtime.InteropServices.UnmanagedType.Struct 指定成員為結構體類型。
MarshalAs這個屬性很難用,很容易用錯,用好需要對C#、C++布局方式有一定的了解才能做。
因此為微軟提供了一個很好用的工具,Signature Tool ,具體使用可以閱讀《使用Signature Tool自動生成P/Invoke調用Windows API的C#函數聲明》這篇文章。
如下圖,直接可以使用工具生成:
?
四、嵌套結構體的封送
4.1 在前面也提到,對于當個結構體,可以直接使用ref 或out 。對于結構體數組,則使用使用IntPtr來進行封送。
在C# 中,CardTest() API 對應方法定義如下:
?[DllImport("ICReader.dll", EntryPoint = "CardTest", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]public static extern int CardTest(byte addr, byte testCardType, IntPtr protocolBeanList, ref int protocolBeanListCount, int maxLength);則C#調用如下:
int memSize = Marshal.SizeOf(typeof(PROTOCOLBEAN)) * 1024; IntPtr protocolBeanListBuf = Marshal.AllocHGlobal(memSize); // 通過使用指定的字節數,從進程的非托管內存中分配內存//調用非托管類型api CardTest(mAddrress, testCardType, protocolBeanListBuf, ref protocolBeanListCount, 1024);//分配托管類型的內存 PROTOCOLBEAN[] protocolBeanList = new PROTOCOLBEAN[1024]; for (int ik = 0; ik < protocolBeanListCount; ik++) {IntPtr ptr = new IntPtr(protocolBeanListBuf.ToInt64() + Marshal.SizeOf(typeof(PROTOCOLBEAN)) * ik);protocolBeanList[i] = (PROTOCOLBEAN)Marshal.PtrToStructure(ptr, typeof(PROTOCOLBEAN)); //保存數據 }Marshal.FreeHGlobal(protocolBeanListBuf); // 釋放內存在上面中通過Marshal.Sizeof(),計算非托管類型所占用的空間,然后通過Marshal.AllocHGlobal()方法從進程的非托管內存中分配內存(字節流)。然后調用非托管類型的API,將API返回數據暫存到前面通過Marshal.AllocHGlobal()方法分配的內存中。接著,就講非托管內存中的數據拷貝出來,使用下面這個方法:
//// 摘要:// 將數據從非托管內存塊封送到新分配的指定類型的托管對象。//// 參數:// ptr:// 指向非托管內存塊的指針。//// structureType:// 待創建對象的類型。此對象必須表示格式化類或結構。//// 返回結果:// 一個托管對象,包含 ptr 參數指向的數據。//// 異常:// System.ArgumentException:// structureType 參數布局不是連續或顯式的。- 或 -structureType 參數是泛型類型。//// System.ArgumentNullException:// structureType 為 null。[ComVisible(true)][SecurityCritical]public static object PtrToStructure(IntPtr ptr, Type structureType);這個方法需要一個帶有當前非托管內存地址的IntPtr指針對象,因此,可以根據結構體大小和非托管內存的首地址計算每個結構體地址(類似C/C++ 內存操作)
IntPtr ptr = new IntPtr(protocolBeanListBuf.ToInt64() + Marshal.SizeOf(typeof(PROTOCOLBEAN)) * ik);protocolBeanListBuf.ToInt64() 記錄的就是非托管內存首地址,然后Marshal.SizeOf(typeof(PROTOCOLBEAN)) 是每個非托管類型所占用的空間。
最后,使用Marshal.PtrToStructure(),?將數據從非托管內存塊封送到新分配的指定類型的托管對象(類似C/C++ 的memcpy() )
其實在C/C++ 中,結構體的拷貝和復制都是將結構體當做字節流進行操作的,因此這個與C/C++ 類似的。
最后使用Marshal.FreeHGlobal(),?釋放以前從進程的非托管內存中分配的內存。
?
好了,終于寫完了,又到半夜12點了,睡覺。
追加結構體,工具類:
class StructUtils<T>{/// <summary>/// 獲取非托管內存/// </summary>/// <param name="count"></param>/// <returns></returns>public static IntPtr GetStructToIntPtr(int count){int memSize = Marshal.SizeOf(typeof(T)) * count;IntPtr intPtr = Marshal.AllocHGlobal(memSize); // 通過使用指定的字節數,從進程的非托管內存中分配內存return intPtr;}/// <summary>/// 將非托管內存數據封送到托管內存/// </summary>/// <param name="intPtr"></param>/// <param name="count"></param>/// <returns></returns>public static T[] IntPtrtToStructArray(IntPtr intPtr, int count){T[] tArray = new T[count];IntPtr ptr;for (int ik = 0; ik < count; ik++){ptr = new IntPtr(intPtr.ToInt64() + Marshal.SizeOf(typeof(T)) * ik);tArray[ik] = (T)Marshal.PtrToStructure(ptr, typeof(T)); //保存數據}return tArray;}/// <summary>/// 將非托管內存數據封送到托管內存/// </summary>/// <param name="intPtr"></param>/// <param name="count"></param>/// <returns></returns>public static void IntPtrtToStructArray(T[] tArray, int offset, int length, IntPtr intPtr){IntPtr ptr;for (int ik = 0; ik < length; ik++){ptr = new IntPtr(intPtr.ToInt64() + Marshal.SizeOf(typeof(T)) * ik);tArray[offset + ik] = (T)Marshal.PtrToStructure(ptr, typeof(T)); //保存數據}}/// <summary>/// 將托管內存數據封送到非托管內存/// </summary>/// <param name="tArray"></param>/// <param name="intPtr"></param>public static void CopyStructToIntPtr(T[] tArray, IntPtr intPtr){IntPtr ptr;for (int ik = 0; ik < tArray.Length; ik++){ptr = new IntPtr(intPtr.ToInt64() + Marshal.SizeOf(typeof(T)) * ik);Marshal.StructureToPtr(tArray[ik], ptr, true);}} /// <summary>/// 將托管內存數據封送到非托管內存/// </summary>/// <param name="tArray"></param>/// <param name="intPtr"></param>public static void CopyStructToIntPtr(T[] tArray, int offset, int length, IntPtr intPtr){IntPtr ptr;for (int ik = 0; ik < length; ik++){ptr = new IntPtr(intPtr.ToInt64() + Marshal.SizeOf(typeof(T)) * ik);Marshal.StructureToPtr(tArray[offset + ik], ptr, true);}}/// <summary>/// 釋放資源/// </summary>/// <param name="ptr"></param>public static void CloseIntPtr(IntPtr ptr){Marshal.FreeHGlobal(ptr);}}?
參考大神文章:
C# 調用dll 封送結構體 結構體數組
使用Signature Tool自動生成P/Invoke調用Windows API的C#函數聲明
超詳細解析托管與非托管
.NET學習之路----我對P/Invoke技術的理解(一)
總結
以上是生活随笔為你收集整理的托管代码C#调用非托管C++ API, 封送嵌套结构体数组的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: UG NX 12 显示曲面网格
- 下一篇: 大连理工计算机保研面试,保研经【大连理工