程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> C#調用C++DLL注意事項

C#調用C++DLL注意事項

編輯:C#入門知識

 


C#調用C++DLL注意事項:

 

1>C#值類型與引用類型的內存特點

2>平台調用中DllImport,StructLayout,MarshalAS的各屬性及其含義

3>C++中結構體的內存布局規則

4>C#調用非托管代碼時,各種參數的送封特點(主要是結構體,數組,字符串)

5>使用Marshal類的靜態方法實現托管內存與非托管內存之間的轉換

6>內存釋放問題,即C#中如何釋放非托管代碼申請的內存

 

 


1>C#值類型與引用類型的內存特點

         C#值類型的對象是在堆棧上分配的,不受垃圾回收器的影響。

         C#引用類型的對象是在托管堆上分配的,對應的引用地址會存儲在線程的堆棧區。

         值類型包括C#的基本類型(用關鍵字int、char、float等來聲明),結構(用struct關鍵字聲明的類型),枚舉(用enum關鍵字聲明的類型);引用類型包括類(用class關鍵字聲明的類型),委托(用delegate關鍵字聲明的特殊類)和數組。

 

2>平台調用中DllImport,StructLayout,MarshalAS的各字段及其含義

DllImport屬性:

dllName:

         必須字段,指定對應的dll的路徑,可以使用絕對路徑也可以使用相對路徑。如果使用相對路徑,則系統會在以下三個文件夾下尋找該相對路徑:1,exe所在文件夾;2,系統文件夾;3,Path環境變量指定的文件夾。

 

EntryPoint:

         指定要調用的DLL入口點。注意如果使用extern"C" + __stdcall 則編譯器會對函數名進行重整。最終的函數名會是_FuncName@8,其中8為FuncName函數所有參數的字節數。如果是extern "C"+__cdecl調用約定,則函數名不變。

舉例:函數聲明如下:


對應的函數入口點如下:


注:

其中extern"C" 使得C++編譯器生成的函數名對於C編譯器是能夠理解的,因為C++編譯器為了處理函數重載的情況,將參數類型加入到了函數的簽名中,所以生成的函數入口只能C++編譯器自己懂。而加入extern "C"則使得生成的函數簽名能夠被其他編譯器理解。

__stdcall和__cdecl是兩種調用約定方式。主要區別在於壓入堆棧中的函數參數的清理方式,__stdcall規定被調用函數負責參數出棧,稱自動清除;__cdecl則規定調用函數方負責參數出棧,稱手動清除。編譯器一般默認使用__cdel方式。

 

CharSet:

         控制函數中與字符串有關的參數或 結構體參數中與字符串有關的參數 在封送時的編碼方式。 編碼方式有兩種:ANSI和UNICODE。ANSI使用1個字節對前256個字符進行編碼,而UNICODE使用兩個字節對所有字符進行編碼。.Net平台中使用的是Unicode格式。在C++中可以使用多種字符集。

         從托管代碼中傳遞字符串到非托管代碼中時,如果非托管代碼使用的是ANSI,則需指定封送方式為Charset.Ansi,封送拆收器會根據該設置將Unicode字符轉換為ANSI字符,再復制到非托管內存中,如果非托管代碼使用Unicode,則需要指定Charset.Unicode,封送拆收器則直接復制過去,在效率上會好一些。

 

MarshalAS屬性:

         用來指定單個參數或者是結構體中單個字段的封送方式。該屬性有以下字段:

UnmanagedType:

         必須字段。用於指定該參數對應非托管數據類型。由於C#中的數據類型和C++中的數據類型不是一一對應的,有些時候C#中的同一種數據類型可以對應於C++中的幾種數據類型,所以需要指定該參數,封送拆收器會在兩個類型之間進行相應的類型轉換。

       比如C#中的String類型,則可以對應於非托管C++中的char * 或者 wchat_t*。如果是char*,則指定UnmanagedType.LPStr,如果是wchat_t*,則指定為UnmanagedType.LPWStr。

         另一個例子是C#中的托管類型System.Boolean可以對應非托管C++中的bool,但是C++中的bool可能是1個字節,2個字節或者4個字節。這時就需要指定為UnmanagedType.U1,UnmanagedType.U2或者UnmanagedType.U4。

         本項目中KXTV_BOOLEAN為1字節無符號數:

typedef unsigned char                                   KXTV_BOOLEAN;

所以在C#中:

using KXTV_BOOLEAN =System.Boolean;

[MarshalAs(UnmanagedType.U1)]

   KXTV_BOOLEAN NetUserFlag,

 

count:

         對於需要傳遞定長字符串或者數組的情況,需要使用count字段來指定字符串長度或者數組中元素的個數。

 

StructLayout屬性:

         控制C#中的結構體在內存中的布局。為了能夠和非托管C++中的結構體在內存中進行轉換,封送拆收器必須知道結構體中每一個字段在結構體中內存中的偏移量。

 

LayoutKind:

         指定結構體的布局類型。有兩種布局類型可以設置,1,Sequential:順序布局。2,Explicit:精確布局。可以精確控制結構體中每個字段在非托管內存中的精確位置。

         一般使用順序布局方式,這也是C#默認的布局方式。

         但是在以下兩種情況下需要使用精確控制Explicit方式:

1,部分定義結構體中的字段。有些結構體很龐大,而C#中僅使用其中幾個字段,則可以只定義那幾個字段,但是要精確指定它們在非托管內存中精確偏移。該偏移應與有其他字段時的偏移一致。

2,非托管代碼中的聯合體,需要使用Explicit將字段重合在一起。

         如:

[StructLayout(LayoutKind.Explicit,Pack=1)]

        public struct KXTV_VALUE

        {

           [FieldOffset(0)]

           public KXTV_UINT16DataType;        ///數ºy據Y類¤¨¤型¨ª(KDB_VALUE_DATA_TYPE)

           [FieldOffset(2)]

           [MarshalAs(UnmanagedType.U1)]

           public KXTV_BOOLEANbitVal;                   ///布?爾?類¤¨¤型¨ª

           [FieldOffset(2)]

           public KXTV_INT8i1Val;            ///單Ì£¤字Á?節¨²整?數ºy

           [FieldOffset(2)]

           public KXTV_INT16i2Val;          ///雙?字Á?節¨²整?數ºy

           [FieldOffset(2)]

           public KXTV_INT32i4Val;          ///四?字Á?節¨²整?數ºy

           [FieldOffset(2)]

           public KXTV_INT64i8Val;          ///八ã?字Á?節¨²整?數ºy

           [FieldOffset(2)]

           public KXTV_UINT8ui1Val;                ///單Ì£¤字Á?節¨²整?數ºy(無T符¤?號?) 

           [FieldOffset(2)]

           public KXTV_UINT16ui2Val;              ///雙?字Á?節¨²整?數ºy(無T符¤?號?)

           [FieldOffset(2)]

           public KXTV_UINT32ui4Val;              ///四?字Á?節¨²整?數ºy(無T符¤?號?)

            [FieldOffset(2)]

           public KXTV_UINT64ui8Val;              ///八ã?字Á?節¨²整?數ºy(無T符¤?號?)

           [FieldOffset(2)]

           public KXTV_FLOAT32r4Val;             ///單Ì£¤精?度¨¨浮?點Ì?數ºy

           [FieldOffset(2)]

           public KXTV_FLOAT64r8Val;             ///雙?精?度¨¨浮?點Ì?數ºy

                                            ///

           [FieldOffset(2)]

           public KXTV_PTRrefVal;            ///其他類型

 

        };

但是需要注意的是值類型和引用類型的地址不能夠重疊。所以上面使用refVal代表所有引用類型,最後通過Marshal類進行轉換即可。

 

3>C++中結構體的內存布局規則

         結構體中字段的偏移量受兩個因素的影響,一個是字段本身在內存中的大小,另一個是對齊方式。

         字段的偏移量為min(字段大小的倍數,對齊方式的倍數),且要保證字段不能重疊,即偏移量應該大於上一個字段的結尾。

         對齊方式默認為8,也可以通過pack來設置新的對齊方式,在KvTXAPI.h中,對齊方式設置為:

#pragma pack( 1 )

即對齊方式設置為1。這時字段在內存中是連續分布的,字段與字段之間沒有空隙。

對應於C#中,所有的結構體都必須使用StructLayout屬性中的Pack字段來指定對齊方式。

如:

        [StructLayout(LayoutKind.Sequential,Pack=1)]

        public struct KXTVTagPubData

        {

           //[FieldOffset(0)]

           public KXTV_UINT32TagID;      

           //[FieldOffset(4)]

           public KXTV_INT16FieldID;        

           //[FieldOffset(6)]

           public KXTV_VALUEFieldValue;      

           //[FieldOffset(16)]

           public FILETIMETimeStamp;        

           //[FieldOffset(24)]

           public KXTV_UINT32QualityStamp;    

        };

如果指定了Pack=1,則FieldValue字段的偏移是6,如果不指定的話,由於FieldValue本身字段大小為10字節,默認對齊方式為8,前面字段已經使用了6個字節了,所以FieldValue的偏移是8(計算公式為min(10*1,8*1))。

結構體在內存中的大小不一致,會導致封送處理器復制內存時出錯。

 

4>C#調用非托管代碼時,各種參數的送封特點(主要是結構體,數組,字符串)

結構體:

         一般使用引用傳遞結構體參數。結構體在C#中本身是值類型,一般在線程的堆棧區分配內存。

         例子:

KXTV_RET KXTVAPI KXTVServerConnect(

                                                                             IN PKXTV_CONNECTION_OPTION pConnectOption ,

                                                                             OUT KXTV_HANDLE*            ServerHandle);

其中參數pConnectOption是一個結構體指針類型變量。

由於參數是指針類型則可以使用引用傳遞方式傳遞結構體,在C#中方法的聲明為:

 [DllImport(dllName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode)]

public extern static KXTV_RETKXTVServerConnect(

                                          ref KXTVConnectionOptionConnectOption,

                                          ref  KXTV_HANDLE ServerHandle);

在C#中的調用方式為:

                      KXTV_RET ErrorCode;

           KXTV.KXTVConnectionOptionConnectOption = new KXTV.KXTVConnectionOption();

           ConnectOption.ServerName = "127.0.0.1";

           ConnectOption.ServerPort = "8800";

           ConnectOption.UserName = "KVAdministrator";

           ConnectOption.Password = "KVADMINISTRATOR@KING";

 

           ConnectOption.ConnectionFlags = 2;

           ConnectOption.NetUserFlag = false;

           ConnectOption.NetworkTimeout = 0;

           ConnectOption.Reserved1 = 1;

 

           ErrorCode = KXTV.KXTVServerConnect(ref ConnectOption, refm_hClient);

           if (IsOK(ErrorCode, "KXTVServerConnect") == false)

               return false;

         其中ConnectOption為局部變量,在托管內存的堆棧區分配的空間。函數參數的傳遞方式為引用傳遞,並不是將堆棧區ConnectOption的地址傳遞給非托管函數,而是由封送拆收器根據結構體的定義在非托管空間開辟一塊內存,將堆棧區的ConnectOption的各字段按照指定的方式封送過去,再取得該內存地址封送給非托管函數,非托管函數完成操作後,封送拆收器會將數據按照指定的方式封送回來,最後由封送拆收器釋放掉非托管去的內存。

         所以結構體中使用StructLayout屬性來控制字段在內存中的布局很重要,如果和C++編譯器生成的內存布局不一致,則會導致無法取得結構體中的某些變量,從而導致程序出錯。

 

數組:

         數組是引用類型,對於引用類型按照值傳遞方式傳送過去的也是地址。如果數組中的元素是簡單的blittable類型(該類型變量在C#和C++中的內存結構是一致的,可直接復制到本機代碼(native code)的類型)。則會將數組的指針直接傳送過去,這是C#的優化,沒有值拷貝的過程。

         blittable類型有:

         Byte;SByte;Int16,;Uint16;Int32;Uint32;Int64;Uint64;Single;Double;IntPtr;UIntPtr。

         blittable類型基本上都是整型和實數型,這些類型在托管內存和非托管內存中的大小都是一致的,可以不通過任何轉換進行互操作。

         而對於所有的字符類型,接口,類,結構體等,都需要封送拆收器進行一些轉換,所以都會在非托管內存中復制一份拷貝。而blittable類型則不用。

         所以使用blittable類型的數組作為值傳遞過去,在非托管函數中的任何修改都會在C#中可見的。如果這不是需要的,則必須指定參數傳遞的方向屬性為[in],即不返回修改的值,這時封送拆收器就進行值復制的過程。

 

字符串:

         字符串的封送過程要注意字符集的問題。C#中使用的Unicode編碼,而C++中的則不一定,所以需要CharSet進行修飾String,這樣封送拆收器就能進行相應的轉換。

         C#中的String的值是不可原地修改的,而System.Text.StringBuilder的值是可以原地修改的。在方向屬性上StringBuilder對象比較特殊,在沒有標注參數的方向屬性是,封送拆收器采用[In]的方式進行默認的處理,對於StringBuilder,封送拆收器則采用[In, Out]方式進行封送處理,即如果StringBuilder傳遞過去的字符串發生了修改,封送拆收器默認會將修改的值再封送回StringBuilder對象中。

 

5>使用Marshal類的靜態方法實現托管內存與非托管內存之間的轉換

         對於C++中的一些復雜類型在C#中沒有相應的數據類型與之對應,所以只能使用指針IntPtr來獲取該對象在托管內存中的地址,然後使用Marshal類提供的靜態方法將非托管內存中的對象復制到托管內存中。

         本項目中:

typedef struct KXTVStringArray

{

         KXTV_UINT32                                    SizeOfArray;              /// 數ºy組Á¨¦大䨮小?

         KXTV_WSTR_ARRAY                      StringArray;               /// 字Á?符¤?串ä?數ºy組Á¨¦

}KXTV_STRING_ARRAY,*PKXTV_STRING_ARRAY;

         該結構體的StringArray為指向指針數組的指針,該指針數組中的每個元素指向一個字符串。這些字符串的內存大多在C++非托管內存中申請的。需要使用IntPtr來取代StringArray。

         然後用Marshal中的方法解析出字符串數組:

public static KXTV_WSTR[] parseToString(KXTV.KXTVStringArray TagNameArray)

        {

           uint size = TagNameArray.SizeOfArray;

 

           KXTV_PTR[] ptrs = new KXTV_HANDLE[size];

                      //將指針數組復制到ptrs指向的內存中

           Marshal.Copy(TagNameArray.StringArray,ptrs, 0, (int)size);

 

           KXTV_WSTR[] str = new KXTV_WSTR[size];

           for (uinti = 0; i < size; i++)

           {

                              //將非托管內存中的字符串復制並構建托管內存中的String對象

               str[i] = Marshal.PtrToStringUni(ptrs[i]);

           }

           return str;

        }

 

         對於指針轉換為結構體,Marshal類也提供了PtrToStructure方法,本項目中的應用為:

KXTV.KXTVTagPubDataTagPubData = (KXTV.KXTVTagPubData)Marshal.PtrToStructure(pFieldValueArray, typeof(KXTV.KXTVTagPubData));

之後非托管中的內存需要調用對應的函數進行釋放,見下一節介紹。

 

6>內存釋放問題,即C#中如何釋放非托管代碼申請的內存

         非托管C++代碼中分配內存有三種方式,malloc,new,CoTaskMenAlloc。對於前兩種方式需要在非托管內存中釋放內存,第三種方式可以在非托管內存中釋放,也可以在托管內存中釋放。

         如果C#調用的函數有一個指針參數,該指針參數在C++中分配了內存,並傳遞回來了,這時就需要在使用完這塊內存之後釋放掉,否則會出現內存洩漏。

         在本項目中,也有C++分配內存的例子,如

KXTV_RET KXTVAPI KXTVTagGetTagNamebyID(

         INKXTV_HANDLE          ServerHandle,

         INKXTV_UINT32          TagNum ,

         INKXTV_UINT32*         TagIDArray ,

         OUTPKXTV_STRING_ARRAY  TagNameArray ,

         OUTKXTV_RET*           ErrorCodeArray);

TagNameArray的內存是在KXTVTagGetTagNamebyID函數內部分配的,由於不知道內存的分配方式,所以只能使用運行庫提供的接口來釋放這部分內存。

         釋放內存的接口為:

KXTV_RET KXTVAPI KXTVAPIFreeStringArray(INPKXTV_STRING_ARRAY StringArray);

 

         這裡主要討論一下在托管C++中使用COM的內存分配方法CoTaskMenAlloc的情況。在這種情況下可以在C#中釋放非托管內存。

         如有如下函數:

         bool MallocString( chat * pStr);

         該函數使用CoTaskMenAlloc分配了一段內存給pStr後返回。該函數在C#中對應的聲明可寫為:

    [DllImport("dllName.dll",CharSet= CharSet.Ansi)]

         bool MallocString([Out] Stirng Str );

         函數返回時,封送拆收器在托管內存中建立Str對象,並將非托管內存中的字符串拷貝到托管內存Str對象中,完成轉換後,封送拆收器會調用CoTaskMenFree嘗試釋放pStr指向的非托管內存,如果該內存是由CoTaskMenAlloc方式分配的,則釋放成功。如果是用new或malloc申請的,則釋放失敗,這時就出現了內存洩漏的情況。

         所有封送拆收器在進行類型轉換時,會自動調用CoTaskMenFree方法來嘗試釋放內存,當然也可以顯示釋放內存。

         使用IntPtr數據類型來對應pStr,封送拆收器將非托管數據封送成IntPtr時,直接將指針復制進IntPtr的值中,如上聲明為:

         [DllImport("dllName.dll",CharSet= CharSet.Ansi)]

         bool MallocString([Out] IntPtr pStr );

         使用Marshal類的PtrToStringAni()方法實現數據拷貝:

         String Str = Marshal. PtrToStringAni(pStr );

         這時數據已經復制到Str對象中了,可以釋放非托管內存中pStr的內存了:

         Marshal.FreeCoTaskMem( pStr );

         這就是手動釋放CoTaskMenAlloc分配的內存,當然也可以在C++中提供一個函數來釋放分配的內存:

         bool ReleaseString( char * pStr);

         之後在C#中調用該函數釋放內存也可以。

         注:

                   對於不確定內存分配方式的,只能使用C++提供的函數來釋放內存。

         本項目中的例子:

          KXTV.KXTVTagGetTagNamebyID(m_hClient, 1,TagIDArray,ref TagNameArray, ErrorCodeArray);

      String[] TagNames = parseToString(TagNameArray);

      KXTV.KXTVAPIFreeStringArray(ref TagNameArray);           /釋放內存

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved