讓我們面對現實吧。這個世界並不完美。幾乎很少有公司在完全用托管代碼開發程序,除此之外仍存在很多需要您處理的舊式非托管代碼。您怎樣將托管和非托管項目集成起來呢?在形式上是采用從托管應用程序調用非托管代碼,還是從非托管代碼應用程序調用托管代碼?
幸運的是,Microsoft® .NET Framework 互操作在托管和非托管代碼之間開辟了一條通道,而封送處理則在該連接中扮演著非常重要的角色,因為它允許在兩者之間進行數據交換(請參見圖 1)。有很多因素會影響 CLR 在非托管和托管領域之間封送數據的方式,包括諸如 [MarshalAs]、[StructLayout]、[InAttribute] 和 [OutAttribute] 等屬性,以及 C# 中 out 和 ref 之類的語言關鍵字。
Figure 1 Bridging the Gap between Managed and Unmanaged Code
因為這些因素很多,所以它可能是進行正確封送的一大難題,因為這項工作要求了解很多有關非托管和托管代碼的詳細情況。在本專欄中,我們會介紹您在日常工作中嘗試進行封送處理時將遇到的一些基本卻又容易混淆的主題。我們不會介紹自定義封送處理、封送處理復雜的結構或其他高級主題,但是如果真正理解了這些基本的概念,您就為處理這些問題做好准備了。
[InAttribute] 和 [OutAttribute]
我們要討論的第一個封送處理主題是關於 InAttribute 和 OutAttribute 的使用,這是位於 System.Runtime.InteropServices 命名空間中的兩種屬性類型。(在將這些屬性應用到您的代碼中時,C# 和 Visual Basic® 允許使用縮寫形式 [In] 和 [Out],但是為了避免混淆我們堅持使用全名。)
當應用於方法參數和返回值時,這些屬性會控制封送處理的方向,因此它們又被稱為方向屬性。[InAttribute] 告知 CLR 在調用開始的時候將數據從調用方封送到被調用方,[OutAttribute] 則告知 CLR 在返回的時候將數據從被調用方封送回調用方。調用方和被調用方都可以是非托管或托管代碼。例如,在 P/Invoke 調用中,是托管代碼在調用非托管代碼。但是在反向 P/Invoke 調用中,就可能是非托管代碼通過函數指針調用托管代碼。
[InAttribute] 和 [OutAttribute] 有四種可能的使用組合:只用 [InAttribute]、只用 [OutAttribute]、同時使用 [InAttribute, OutAttribute] 以及兩者都不用。如果沒有指定任何一個屬性,那就是要 CLR 自己確定方向屬性,默認情況下通常是使用 [InAttribute]。但是,如果是 StringBuilder 類,則在沒有指定任何一個屬性的情況下,會同時使用 [InAttribute] 和 [OutAttribute]。(有關詳細信息,請參閱後面有關 StringBuilder 的部分。)另外,使用 C# 中的 out 和 ref 關鍵字可能會更改已應用的屬性,如圖 2 所示。請注意,如果沒有為參數指定關鍵字,就意味著它是默認的輸入參數。
Figure 2 Out and Ref and Their Associated Attributes
C# 關鍵字 屬性 (未指定) [InAttribute] out [OutAttribute] ref [InAttribute],[OutAttribute]
請看一下圖 3 中的代碼。其中有三個本機 C++ 函數,並且它們都對 arg 進行相同的更改。此外,請注意對字符串操作使用 strcpy 僅僅是為了便於說明——生產代碼應改用這些函數的安全版本,它們可在 msdn.microsoft.com/msdnmag/issues/05/05/SafeCandC 中找到。
Figure 3 Trying Out Directional Attributes
MARSHALLIB_API void __stdcall Func_In_Attribute(char *arg) { printf("Inside Func_In_Attribute: arg = %sn", arg); strcpy(arg, "New"); } MARSHALLIB_API void __stdcall Func_Out_Attribute(char *arg) { printf("Inside Func_Out_Attribute: arg = %sn", arg); strcpy(arg, "New"); } MARSHALLIB_API void __stdcall Func_InOut_Attribute(char *arg) { printf("Inside Func_InOut_Attribute: arg = %sn", arg); strcpy(arg, "New"); }
唯一的不同是我們在 P/Invoke 簽名中使用方向屬性調用它們的方式,如下面的 C# 代碼所示:
[DllImport(@"MarshalLib.dll")] public static extern void Func_In_Attribute([In]char[] arg); [DllImport(@"MarshalLib.dll")] public static extern void Func_Out_Attribute([Out]char[] arg); [DllImport(@"MarshalLib.dll")] public static extern void Func_InOut_Attribute([In, Out]char[] arg);
如果您通過 P/Invoke 從托管代碼調用這些函數,並將“Old”作為字符數組傳遞給這些函數,就會獲得以下輸出(出於演示目的而有所縮減):
Before Func_In_Attribute: arg = Old Inside Func_In_Attribute: arg = Old After Func_In_Attribute: arg = Old Before Func_Out_Attribute: arg = Old Inside Func_Out_Attribute: arg = After Func_Out_Attribute: arg = New Before Func_InOut_Attribute: arg = Old Inside Func_InOut_Attribute: arg = Old After Func_InOut_Attribute: arg = New
讓我們進一步看一下結果。在 Func_In_Attribute 中,傳入了原始值,但是在 Func_In_Attribute 內部發生的更改並沒有傳播回來。在 Func_Out_Attribute 中,沒有傳入原始值,而 Func_Out_Attribute 內部發生的更改已傳播回來了。在 Func_InOut_Attribute 中,傳入了原始值,並且 Func_Out_Attribute 內部發生的更改也已傳播回來。然而,只要您稍做修改,情況就完全不同了。這一次讓我們修改一下本機函數以使用 Unicode,如下所示:
MARSHALLIB_API void __stdcall Func_Out_Attribute_Unicode(wchar_t *arg) { wprintf(L"Inside Func_Out_Attribute_Unicode: arg = %sn", arg); printf("Inside Func_Out_Attribute_Unicode: strcpy(arg, "New")n"); wcscpy(arg, L"New"); }
在此我們聲明了 C# 函數,僅應用 [OutAttribute],並將 CharSet 更改為 CharSet.Unicode:
[DllImport(@"MarshalLib.dll", CharSet=CharSet.Unicode)] public static extern void Func_Out_Attribute_Unicode([Out]char[] arg);
以下是輸出:
Before Func_Out_Attribute_Unicode: arg = Old Inside Func_Out_Attribute_Unicode: arg = Old After Func_Out_Attribute_Unicode: arg = New
有趣的是,盡管沒有 [InAttribute],也還是傳遞了原始值。[DllImportAttribute] 會告知 CLR 封送 Unicode,而且由於 CLR 中的字符類型也是 Unicode,所以 CLR 發現了一個優化封送處理的機會,即固定字符數組然後直接傳遞該字符的地址。(稍後您將看到有關復制和固定的詳細介紹。)然而,這並不意味著您應依賴這種行為。相反,在不依賴 CLR 默認封送行為的時候,應始終使用正確的封送方向屬性。這種默認行為的典型例子是使用 int 參數的情況;不必指定 [InAttribute] int arg。
某些情況下,[OutAttribute] 將被忽略。例如,由於 [OutAttribute]int 沒有任何意義,所以 CLR 便忽略這個 [OutAttribute]。同樣,[OutAttribute] 字符串也是如此,因為字符串是固定不變的。
接口定義 (IDL) 文件也具有 [in] 和 [out] 屬性,它們可視為與 CLR 中的 [InAttribute] 和 [OutAttribute] 相同。
關鍵字 Out 和 Ref 以及通過引用傳遞
之前,我們已經介紹了 C# 的 out 和 ref 關鍵字可以被直接映射到 [InAttribute] 和 [OutAttribute]。事實上,out 和 ref 還可以改變作為 CLR 封送對象或封送目標的數據類型。將數據作為 out 或 ref 傳遞與通過引用傳遞相同。如果使用 ILDASM 來檢查中間語言 (IL) 中對應的函數簽名,您會看到該類型旁邊有一個 & 字符,它表示該參數應通過引用傳遞。在通過引用傳遞時,CLR 會增加額外的中間環節。圖 4 列舉了幾個示例。
Figure 4 Marshaling Results
C# 簽名 非托管簽名 MSIL 簽名 CLR 看到的實際 MSIL 簽名 基本類型 int arg int arg int [in] int out int arg int *arg [out] int & [out] int & ref int arg int *arg int & [in, out] int & 結構 MyStruct arg MyStruct arg MyStruct [in] MyStruct out MyStruct arg MyStruct *arg [out] MyStruct & [out] MyStruct & ref MyStruct arg MyStruct *arg MyStruct & [in, out] MyStruct & 字符串 string arg char *arg string [in] string out string arg char **arg [out] string & [out] string & ref string arg char **arg string & [in, out] string & 類 MyClass arg MyClass *arg MyClass [in] MyClass out MyClass arg MyClass **arg [out] MyClass & [out] MyClass & ref MyClass arg MyClass **arg MyClass & [in, out] Myclass &
讓我們總結一下針對圖 5 所示表格中的 out 和 ref 所討論的內容。
Figure 5 Default Attributes
C# 簽名 MSIL 簽名 默認方向屬性 <type> type [InAttribute] out <type> [OutAttribute] type & [OutAttribute] ref <type> type & [InAttribute, OutAttribute]請注意,在通過引用傳遞時,如果沒有指定方向屬性,CLR 就會自動應用 [InAttribute] 和 [OutAttribute],這就是圖 4 中的 Microsoft 中間語言 (MSIL) 簽名中只有“string &”的原因。如果指定了任何這些屬性,CLR 將遵循它們,而不是采用默認行為,如下例所示:
public static extern void PassPointerToComplexStructure( [In]ref ComplexStructure pStructure);
以上簽名會替代 ref 的默認方向行為,將它變成僅使用 [InAttribute]。在此特定的情況下,如果您執行 P/Invoke,那麼指向 ComplexStructure(它是一個值類型)的指針會從 CLR 端傳遞到本機端,但是被調用方無法使任何改動對 pStructure 指針所指向的 ComplexStructure 可見。圖 6 列舉了一些方向屬性和關鍵字組合的其他示例。
Figure 6 More Attributes and Keywords
C# 簽名 非托管 IDL 簽名 MSIL 簽名 Out [InAttribute] out int arg 編譯器錯誤 CS0036。out 參數不能有 In 屬性。 不適用 [OutAttribute] out int arg [out] int *arg [out] int & [InAttribute, OutAttribute] out int arg 編譯器錯誤 CS0036。out 參數不能有 In 屬性。 不適用 Ref [InAttribute] ref int arg [in] int *arg [in] int & [OutAttribute] ref int arg 編譯器錯誤 CS0662,不能對 ref 參數只指定 Out 屬性。請同時使用 In 和 Out 兩個屬性,或者都不用。 不適用 [InAttribute, OutAttribute] ref int arg [in, out] int *arg [in] [out] int &
返回值
至此為止我們僅討論了參數。從函數返回的值又如何呢?CLR 會自動將返回值視為使用 [OutAttribute] 的普通參數。同時,CLR 還可以轉換函數簽名,這是一個由 PreserveSigAttribute 控制的過程。如果在應用於 P/Invoke 簽名時 [PreserveSigAttribute] 被設為 false,CLR 就會將 HRESULT 返回值映射到托管異常,並且它會將 [out, retval] 參數映射到該函數的返回值。因此下面的托管函數簽名
public static string extern GetString(int id);
會變成非托管簽名:
HRESULT GetString([in]int id, [out, retval] char **pszString);
如果 [PreserveSigAttribute] 被設為 true(P/Invoke 的默認值),此轉換就不會發生。請注意,對於 COM 函數而言,[PreserveSigAttribute] 通常默認設為 false,不過有很多方法可以改變此設置。有關詳細信息,請查看有關 TlbExp.exe 和 TlbImp.exe 的 MSDN® 文檔。
StringBuilder 和封送處理
CLR 封送拆收器具有內置的 StringBuilder 類型知識,並且處理它的方式與處理其他類型不同。默認情況下,StringBuilder 作為 [InAttribute, OutAttribute] 傳遞。StringBuilder 很特別,因為具有 Capacity 屬性(該屬性可以在運行時確定必需緩沖區的大小),並且它可被動態地更改。因此,在封送過程中,CLR 可以固定 StringBuilder,直接傳遞在 StringBuilder 中使用的內部緩沖區的地址,並允許適當的本機代碼更改該緩沖區的內容。
為了充分利用 StringBuilder,您將需要遵循下列所有規則:
不要通過引用傳遞 StringBuilder(使用 out 或 ref)。否則,CLR 會認為該參數的簽名是 wchar_t **,而不是 wchar_t *,並且它將無法固定 StringBuilder 的內部緩沖區。性能會大大降低。
當非托管代碼使用 Unicode 時使用 StringBuilder。否則,CLR 將不得不復制該字符串,並將它在 Unicode 和 ANSI 之間轉換,這樣會降低性能。通常情況下,您應將 StringBuilder 作為 Unicode 字符的 LPARRAY 或作為 LPWSTR 封送。
始終提前指定 StringBuilder 的容量,並確保該容量對存放緩沖區而言足夠大。在非托管代碼端的最佳做法是接受字符串緩沖區的大小作為參數,以避免緩沖區溢出。在 COM 中,您還可以使用 IDL 中的 size_is 來指定大小。
復制和固定
當 CLR 執行數據封送時,它有兩個選擇:復制和固定(請參閱 msdn2.microsoft.com/23acw07k)。
默認情況下,CLR 會創建一個將在封送過程中使用的副本。例如,如果托管代碼要將某個字符串作為 ANSI C-String 傳遞到非托管代碼,CLR 會復制該字符串,將其轉換成 ANSI,然後將該臨時對象的指針傳遞到非托管代碼。該復制過程可能會相當慢,並可能造成性能問題。
在某些情況下,CLR 可通過將托管對象直接固定到垃圾收集器 (GC) 堆來優化封送處理,這樣在調用過程中就無法重定位它。指向托管對象(或指向托管對象內部某個位置)的指針將被直接傳遞到非托管代碼。
當滿足下列所有條件之後就可以執行固定:第一,托管代碼必須調用本機代碼,而不是本機代碼調用托管代碼。第二,該類型必須可直接復制或者必須可以在某些情況下變得可直接復制。第三,您不是通過引用傳遞(使用 out 或 ref)。第四,調用方和被調用方位於同一線程上下文或單元中。
第二條規則需要進一步說明一下。可直接復制類型是指在托管和非托管內存中具有共同表示方法的類型。因此,在進行封送處理時可直接復制類型不需要進行轉換。不可直接復制但能夠變成可直接復制的類型的典型例子是字符類型。默認情況下,它不可直接復制,因為它可被映射到 Unicode 或 ANSI。然而,由於字符在 CLR 中始終是 Unicode,所以當指定了 [DllImportAttribute(CharSet= Unicode)] 或 [MarshalAsAttribute(UnmanagedType.LPWSTR)] 時,它會變成可直接復制。在下面的示例中,arg 可被固定在 PassUnicodeString 中,但是無法固定在 PassAnsiString 中:
[DllImport(@"MarshalLib.dll", CharSet = CharSet.Unicode)] public static extern string PassUnicodeString(string arg); [DllImport(@"MarshalLib.dll", CharSet = CharSet.Ansi)] public static extern string PassAnsiString(string arg);
內存所有權
在函數調用期間,函數可對它的參數進行兩種類型的更改:引用更改或就地更改。引用更改涉及更改指針指向的位置;如果指針已指向一塊已分配的內存,那麼就需要首先釋放內存,否則指向它的指針會丟失。就地更改涉及更改引用所指向位置的內存。
進行哪一種更改取決於參數的類型以及(最重要的是)被調用方和調用方之間的約定。但是,由於 CLR 無法自動了解合約,所以它不得不依賴有關類型的常識,如圖 7 所示。
Figure 7 CLR Type Knowledge
IDL 簽名 更改類型 [In] Type 不允許更改 [In] Type * 不允許更改 [Out] Type * 就地更改 [In, Out] Type * 就地更改 [In] Type ** 不允許更改 [Out] Type ** 引用更改 [In, Out] Type ** 引用更改或就地更改如前所述,在通過引用傳遞時只有引用類型有兩層中間環節(但是也有一些例外,例如“[MarshalAs(UnmanagedType.LPStruct)]ref Guid”),所以只有指向引用類型的指針或引用可以更改,如圖 8 所示。
Figure 8 Type Change Rules
C# 簽名 更改類型 int arg 不允許更改 out int arg 就地更改 ref int arg 就地更改 string arg 不允許更改 out string arg 引用更改 ref string arg 引用更改或就地更改 [InAttribute, OutAttribute] StringBuilder arg 就地更改 [OutAttribute] StringBuilder arg 就地更改
您不必擔心就地更改所需的內存所有權,因為調用方已為被調用方分配了內存,而且調用方擁有這些內存。在此我們以“[OutAttribute] StringBuilder”為例。相應的本機類型為 char *(假設是 ANSI),因為我們不是通過引用傳遞。數據被封送出去,而不是封送進來。內存由調用方(在本例中是 CLR)分配。內存的大小由 StringBuilder 對象的容量確定。被調用方不需要關注內存。
為了更改字符串,被調用方會自己直接更改該內存。但是,在進行引用更改時,分清誰擁有哪個內存非常重要,否則可能會發生很多意外的後果。關於所有權問題,CLR 遵循 COM 風格的約定:
作為 [in] 傳遞的內存歸調用方所有,應由調用方分配,由調用方釋放。被調用方不應嘗試釋放或修改該內存。
由被調用方分配並作為 [out] 傳遞或返回的內存歸調用方所有,應由調用方釋放。
被調用方可釋放作為 [in, out] 傳遞自調用方的內存,為其分配新的內存,並覆蓋原有的指針值,從而將其傳遞出去。新內存歸調用方所有。這需要兩層中間環節,例如 char **。
在互操作領域中,調用方/被調用方變成了 CLR/本機代碼。上述規則意味著,在解除固定的情況下,如果在本機代碼中接收到作為 [out] 傳遞自 CLR 的一個內存塊的指針,您就需要釋放它。另一方面,如果 CLR 接收到作為 [out] 傳遞自本機代碼的指針,CLR 就需要釋放它。顯然,在第一種情況下,本機代碼需要解除分配,而在第二種情況下,托管代碼需要解除分配。
由於這涉及到內存分配和解除分配,所以最大的問題是要使用什麼函數。有很多選擇:HeapAlloc/HeapFree、malloc/free、new/delete 等等。但是,由於 CLR 在非 BSTR 情況下使用 CoTaskMemAlloc/CoTaskMemFree,而在 BSTR 情況下使用 SysStringAlloc/SysStringAllocByteLen/SysStringFree,所以您就必須使用這些函數。否則就很可能會在某個版本的 Windows® 下發生內存洩漏或故障。我們已經看到過這樣的情況,即在 Windows XP 中將經過 malloc 的內存傳遞給 CLR 之後程序沒有發生故障,但在 Windows Vista® 中卻發生了故障。
除了這些函數以外,從 CoGetMalloc 返回的系統實現的 IMalloc 接口也很好用,因為在內部它們使用的是同一個堆。但是,最好始終堅持使用 CoTaskMemAlloc/CoTaskMemFree 和 SysStringAlloc/ SysStringAllocByteLen/SysStringFree,因為 CoGetMalloc 將來可能會發生變化。
讓我們看一個示例。GetAnsiStringFromNativeCode 采用 char ** 參數作為 [in, out],並返回 char * 作為 [out, retval]。對於 char ** 參數,它可以選擇調用 CoTaskMemFree 來釋放由 CLR 分配的內存,然後通過使用 CoTaskMemAlloc 來分配新內存,並用新內存的指針覆蓋該指針。隨後,CLR 會釋放該內存,並為托管字符串創建一個副本。對於返回值,它只需要通過使用 CoTaskMemAlloc 來分配新的內存塊,然後將其返回給調用方。返回後,新分配的內存即歸 CLR 所有。CLR 會先使用它創建新的托管字符串,然後再調用 CoTaskMemFree 釋放它。
讓我們看一下第一個選擇(請參見圖 9)。相應的 C# 函數聲明如下所示:
Figure 9 Using Pointers
MARSHALLIB_API char *__stdcall GetAnsiStringFromNativeCode(char **arg) { char *szRet = (char *)::CoTaskMemAlloc(255); strcpy(szRet, "Returned String From Native Code"); printf("Inside GetAnsiStringFromNativeCode: *arg = %sn", *arg); printf("Inside GetAnsiStringFromNativeCode: CoTaskMemFree(*arg); *arg = CoTaskMemAlloc(100); strcpy(*arg, "Changed")n"); ::CoTaskMemFree(*arg); *arg = (char *)::CoTaskMemAlloc(100); strcpy(*arg, "Changed"); return szRet; } class Lib { [DllImport(@"MarshalLib.dll", CharSet= CharSet.Ansi)] public static extern string GetAnsiStringFromNativeCode( ref string inOutString); }
當下面的 C# 代碼調用 GetAnsiStringFromNativeCode
string argStr = "Before"; Console.WriteLine("Before GetAnsiStringFromNativeCode : argStr = "" + argStr + """); string retStr = Lib.GetAnsiStringFromNativeCode(ref argStr); Console.WriteLine("AnsiStringFromNativeCode() returns "" + retStr + """ ); Console.WriteLine("After GetAnsiStringFromNativeCode : argStr = "" + argStr + """);
輸出是:
Before GetAnsiStringFromNativeCode : argStr = "Before" Inside GetAnsiStringFromNativeCode: *arg = Before Inside GetAnsiStringFromNativeCode: CoTaskMemFree(*arg); *arg = CoTaskMemAlloc(100); strcpy(*arg, "Changed") AnsiStringFromNativeCode() returns "Returned String From Native Code" After GetAnsiStringFromNativeCode : argStr = "Changed"
如果您准備調用的本機函數沒有遵循這個約定,您就必須親自封送,以避免內存損壞。這種情況很容易發生,因為非托管函數的功能可能會返回它需要的任何內容;它可以每次返回同一塊內存,也可以返回由 malloc/new 分配的新內存塊,等等,這些還是取決於約定。
除了內存分配以外,傳進或傳出的內存大小也非常重要。正如在 StringBuilder 情形中所討論的那樣,修改 Capacity 屬性也很重要,這樣 CLR 便可以分配足夠大的內存來存放結果。此外,將字符串作為 [InAttribute, OutAttribute](不使用 out 或 ref 以及任何其他屬性)封送並不是明智的辦法,因為您不知道該字符串是否夠大。您可以使用 MarshalAsAttribute 中的 SizeParamIndex 和 SizeConst 字段指定緩沖區的大小。但是,在通過引用傳遞時,這些屬性將無法使用。
反向 P/Invoke 和委托生存期
CLR 允許將委托傳遞到非托管領域,這樣便可將該委托作為非托管函數指針調用。實際上,結果就是 CLR 創建了一個 thunk,後者將來自本機代碼的調用轉發給實際委托,然後再轉發給真正的函數(請參見圖 10)。
Figure 10 Using a Thunk
通常情況下,您不必擔心委托的生存期。將委托傳遞到非托管代碼時,CLR 就會確保委托在調用期間處於活動狀態。
但是,如果本機代碼在超過調用時間跨度的情況下保留某個指針副本,而且准備在以後通過該指針回調,您可能需要使用 GCHandle 顯式阻止垃圾收集器收集該委托。必須提醒您的是,固定的 GCHandle 可能對程序性能有很大的負面影響。幸運的是,在本示例中,您不必分配固定的 GC 句柄,因為該 thunk 已在非托管堆中分配,並且通過 GC 已知的引用間接引用該委托。因此,thunk 無法四處移動,並且當委托自身處於活動狀態時本機代碼應始終能夠通過非托管指針調用該委托。
Marshal.GetFunctionPointerForDelegate 可以將委托轉換成函數指針,但是它對保證委托的生存期沒有任何作用。請看一下下面的函數聲明:
public delegate void PrintInteger(int n);
[DllImport(@"MarshalLib.dll", EntryPoint="CallDelegate")]
public static extern void CallDelegateDirectly(
IntPtr printIntegerProc);
如果您為其調用 Marshal.GetFunctionPointerForDelegate 並存儲返回的 IntPtr,則將 IntPtr 傳遞到您准備調用的函數,如下所示:
IntPtr printIntegerCallback = Marshal.GetFunctionPointerForDelegate(
new Lib.PrintInteger(MyPrintInteger));
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
CallDelegateDirectly(printIntegerCallback);
該委托可能會在您調用 CallDelegateDirectly 之前被收集,您會收到一個 MDA 錯誤,指出檢測到 CallbackOnCollectedDelegate。要修復這個錯誤,可以將對委托的引用存儲在內存中或分配 GC 句柄。
如果本機代碼將非托管函數的指針返回給 CLR,本機代碼將負責保留實際的函數代碼。這通常不是問題,除非該代碼位於動態加載的 DLL 中或者是動態生成的。
P/Invoke Interop Assistant
了解並記住到目前為止已介紹的所有屬性和規則可能會有一點困難。畢竟,大多數托管代碼的開發人員只需要能夠快速找到用於 Win32 API 函數的 P/Invoke 簽名,將其粘貼到他們的代碼中就完成任務了。這正是 P/Invoke Interop Assistant(可從《MSDN 雜志》網站獲得)可以發揮作用的地方。此工具可以有效地幫助從 C++ 轉換成托管的 P/Invoke 簽名,以及它們之間的反向轉換。它甚至帶有一個包含 Win32 函數、數據類型和常量的數據庫,所以將 Win32 P/Invoke 添加到您的 C# 或 Visual Basic 源文件之類常見任務會變得非常簡單。此工具軟件包中有兩個命令行工具 SigImp 和 SigExp,它們可用於文件批處理。該軟件包還包含一個 GUI 工具,其中包括上述兩個工具的功能。
該 GUI 工具對於進行簡單的轉換來說非常方便。它包含三個選項卡:SigExp、SigImp Search 和 SigImp Translate Snippet。
SigExp 可將托管簽名轉換成非托管簽名。它反射托管程序集以找到所有 P/Invoke 聲明和 COM 導入的類型。根據此輸入,它會生成相應的本機 C 簽名(請參見圖 11)。
Figure 11 P/Invoke Interop Assistant GUI Tool—SigExp
SigImp Search 和 SigImp Translate Snippet 可將非托管簽名轉換成托管簽名。它們會根據手動輸入的本機函數簽名的本機類型、函數、常量和代碼段,使用 C# 或 Visual Basic 生成托管簽名和定義。
SigImp Search 允許用戶選擇他們希望用於生成代碼的托管代碼語言,然後選擇代碼生成所基於的本機類型、過程或常量。該工具會顯示從 Windows SDK 頭文件收集的受支持類型、方法和常量的列表(請參見圖 12)。
Figure 12 P/Invoke Interop Assistant GUI Tool—SigImp Search
SigImp Translate Snippet 允許用戶將他們自己的本機代碼段寫入工具中。然後工具會在主窗口中生成並顯示托管代碼等效項體,如圖 13 所示。
Figure 13 P/Invoke Interop Assistant GUI Tool—SigImp Translate Snippet
有關 P/Invoke Interop Assistant 中 GUI 工具或命令行工具的詳細信息,請參閱該工具隨附的文檔。
嘗試一下
如您所知,封送處理是一個復雜的主題,您可以使用很多技巧來更改封送處理過程,以適應自己的需要。我們建議您嘗試一下這裡介紹的幾種做法。它們肯定可以幫助您找到走出我們稱之為封送處理迷宮的方法。
請將您想詢問的問題和提出的意見發送至 [email protected].