P/Invoke

簡介,走進 P/Invoke,樣式,屬性,數據處理,指針參數,封送文本,

簡介

基於 Microsoft® .NET Framework 的應用程式中完成了大量的 Win32® Interop。我並不是要說我的應用程式充滿了自定義的 interop 代碼,但有時我會在 .NET Framework 類庫中碰到一些次要但又繁絮、不充分的內容,通過調用該 Windows® API,可以快速減少這樣的麻煩,.NET Framework 1.0 或 1.1 版類庫中存在任何 Windows 所沒有的功能限制都不足為怪。畢竟,32 位的 Windows(不管何種版本)是一個成熟的作業系統,為廣大客戶服務了十多年。相比之下,.NET Framework 卻是一個新事物。
隨著越來越多的開發人員將生產應用程式轉到託管代碼,開發人員更頻繁地研究底層作業系統以圖找出一些關鍵功能顯得很自然 — 至少目前是如此。
值得慶幸的是,公共語言運行庫 (CLR) 的 interop 功能(稱為平台調用 (P/Invoke))非常完善。在本專欄中,我將重點介紹如何實際使用 P/Invoke 來調用 Windows API 函式。當指 CLR 的 COM Interop 功能時,P/Invoke 當作名詞使用;當指該功能的使用時,則將其當作動詞使用。我並不打算直接介紹 COM Interop,因為它比 P/Invoke 具有更好的可訪問性,卻更加複雜,這有點自相矛盾,這使得將 COM Interop 作為專欄主題來討論不太簡明扼要。

走進 P/Invoke

首先從考察一個簡單的 P/Invoke 示例開始。讓我們看一看如何調用 Win32 MessageBeep 函式,它的非託管聲明如以下代碼所示:
BOOL MessageBeep( UINT uType // beep type);
為了調用 MessageBeep,您需要在 C# 中將以下代碼添加到一個類或結構定義中:
[DllImport("User32.dll")]static extern Boolean MessageBeep(UInt32 beepType);
令人驚訝的是,只需要這段代碼就可以使託管代碼調用非託管的 MessageBeep API。它不是一個方法調用,而是一個外部方法定義。(另外,它接近於一個來自 C 而 C# 允許的直接連線埠,因此以它為起點來介紹一些概念是有幫助的。)來自託管代碼的可能調用如下所示:
MessageBeep(0);
請注意,現在 MessageBeep 方法被聲明為 static。這是 P/Invoke 方法所要求的,因為在該 Windows API 中沒有一致的實例概念。接下來,還要注意該方法被標記為 extern。這是提示編譯器該方法是通過一個從 DLL 導出的函式實現的,因此不需要提供方法體。
說到缺少方法體,您是否注意到 MessageBeep 聲明並沒有包含一個方法體?與大多數算法由中間語言 (IL) 指令組成的託管方法不同,P/Invoke 方法只是元數據,實時 (JIT) 編譯器在運行時通過它將託管代碼與非託管的 DLL 函式連線起來。執行這種到非託管世界的連線所需的一個重要信息就是導出非託管方法的 DLL 的名稱。這一信息是由 MessageBeep 方法聲明之前的 DllImport 自定義屬性提供的。在本例中,可以看到,MessageBeep 非託管 API 是由 Windows 中的 User32.dll 導出的。
到現在為止,關於調用 MessageBeep 就剩兩個話題沒有介紹,請回顧一下,調用的代碼與以下所示代碼片段非常相似:
[DllImport("User32.dll")]static extern Boolean MessageBeep(UInt32 beepType);
最後這兩個話題是與數據封送處理 (data marshaling) 和從託管代碼到非託管函式的實際方法調用有關的話題。調用非託管 MessageBeep 函式可以由找到作用域內的extern MessageBeep 聲明的任何託管代碼執行。該調用類似於任何其他對靜態方法的調用。它與其他任何託管方法調用的共同之處在於帶來了數據封送處理的需要。
C# 的規則之一是它的調用語法只能訪問 CLR 數據類型,例如 System.UInt32 和 System.Boolean。C# 顯然不識別 Windows API 中使用的基於 C 的數據類型(例如 UINT 和 BOOL),這些類型只是 C 語言類型的類型定義而已。所以當 Windows API 函式 MessageBeep 按以下方式編寫時
BOOL MessageBeep( UINT uType )
外部方法就必須使用 CLR 類型來定義,如您在前面的代碼片段中所看到的。需要使用與基礎 API 函式類型不同但與之兼容的 CLR 類型是 P/Invoke 較難使用的一個方面。因此,在本專欄的後面我將用完整的章節來介紹數據封送處理。

樣式

在 C# 中對 Windows API 進行 P/Invoke 調用是很簡單的。但如果類庫拒絕使您的應用程式發出嘟聲,應該想方設法調用 Windows 使它進行這項工作,是嗎?
是的。但是與選擇的方法有關,而且關係甚大!通常,如果類庫提供某種途徑來實現您的意圖,則最好使用 API 而不要直接調用非託管代碼,因為 CLR 類型和 Win32 之間在樣式上有很大的不同。我可以將關於這個問題的建議歸結為一句話。當您進行 P/Invoke 時,不要使應用程式邏輯直接屬於任何外部方法或其中的構件。如果您遵循這個小規則,從長遠看經常會省去許多的麻煩。
圖 1 中的代碼顯示了我所討論的 MessageBeep 外部方法的最少附加代碼。圖 1 中並沒有任何顯著的變化,而只是對無包裝的外部方法進行一些普通的改進,這可以使工作更加輕鬆一些。從頂部開始,您會注意到一個名為 Sound 的完整類型,它專用於 MessageBeep。如果我需要使用 Windows API 函式 PlaySound 來添加對播放波形的支持,則可以重用 Sound 類型。然而,我不會因公開單個公共靜態方法的類型而生氣。畢竟這只是應用程式代碼而已。還應該注意到,Sound 是密封的,並定義了一個空的私有構造函式。這些只是一些細節,目的是使用戶不會錯誤地從 Sound 派生類或者創建它的實例。
圖 1 中的代碼的下一個特徵是,P/Invoke 出現位置的實際外部方法是 Sound 的私有方法。這個方法只是由公共 MessageBeep 方法間接公開,後者接受 BeepTypes 類型的參數。這個間接的額外層是一個很關鍵的細節,它提供了以下好處。首先,應該在類庫中引入一個未來的 beep 託管方法,可以重複地通過公共 MessageBeep 方法來使用託管 API,而不必更改應用程式中的其餘代碼。
該包裝方法的第二個好處是:當您進行 P/Invoke 調用時,您放棄了免受訪問衝突和其他低級破壞的權利,這通常是由 CLR 提供的。緩衝方法可以保護您的應用程式的其餘部分免受訪問衝突及類似問題的影響(即使它不做任何事而只是傳遞參數)。該緩衝方法將由 P/Invoke 調用引入的任何潛在的錯誤本地化。
將私有外部方法隱藏在公共包裝後面的第三同時也是最後的一個好處是,提供了向該方法添加一些最小的 CLR 樣式的機會。例如,在圖 1 中,我將 Windows API 函式返回的 Boolean 失敗轉換成更像 CLR 的異常。我還定義了一個名為 BeepTypes 的枚舉類型,它的成員對應於同該 Windows API 一起使用的定義值。由於 C# 不支持定義,因此可以使用託管枚舉類型來避免幻數向整個應用程式代碼擴散。
包裝方法的最後一個好處對於簡單的 Windows API 函式(如 MessageBeep)誠然是微不足道的。但是當您開始調用更複雜的非託管函式時,您會發現,手動將 Windows API 樣式轉換成對 CLR 更加友好的方法所帶來的好處會越來越多。越是打算在整個應用程式中重用 interop 功能,越是應該認真地考慮包裝的設計。同時我認為,在非面向對象的靜態包裝方法中使用對 CLR 友好的參數也並非不可以。

屬性

現在是更深入地進行探討的時候了。在對託管代碼進行 P/Invoke 調用時,DllImportAttribute 類型扮演著重要的角色。DllImportAttribute 的主要作用是給 CLR 指示哪個 DLL 導出您想要調用的函式。相關 DLL 的名稱被作為一個構造函式參數傳遞給 DllImportAttribute。
如果您無法肯定哪個 DLL 定義了您要使用的 Windows API 函式,Platform SDK 文檔將為您提供最好的幫助資源。在 Windows API 函式主題文字臨近結尾的位置,SDK 文檔指定了 C 應用程式要使用該函式必須連結的 .lib 檔案。在幾乎所有的情況下,該 .lib 檔案具有與定義該函式的系統 DLL 檔案相同的名稱。例如,如果該函式需要 C 應用程式連結到 Kernel32.lib,則該函式就定義在 Kernel32.dll 中。您可以在 MessageBeep 中找到有關 MessageBeep 的 Platform SDK 文檔主題。在該主題結尾處,您會注意到它指出庫檔案是 User32.lib;這表明 MessageBeep 是從 User32.dll 中導出的。
可選的 DllImportAttribute 屬性
除了指出宿主 DLL 外,DllImportAttribute 還包含了一些可選屬性,其中四個特別有趣:EntryPoint、CharSet、SetLastError 和 CallingConvention。
EntryPoint 在不希望外部託管方法具有與 DLL 導出相同的名稱的情況下,可以設定該屬性來指示導出的 DLL 函式的入口點名稱。當您定義兩個調用相同非託管函式的外部方法時,這特別有用。另外,在 Windows 中還可以通過它們的序號值綁定到導出的 DLL 函式。如果您需要這樣做,則諸如“#1”或“#129”的 EntryPoint 值指示 DLL 中非託管函式的序號值而不是函式名。
CharSet 對於字元集,並非所有版本的 Windows 都是同樣創建的。Windows 9x 系列產品缺少重要的 Unicode 支持,而 Windows NT 和 Windows CE 系列則一開始就使用 Unicode。在這些作業系統上運行的 CLR 將Unicode 用於 String 和 Char 數據的內部表示。但也不必擔心 — 當調用 Windows 9x API 函式時,CLR 會自動進行必要的轉換,將其從 Unicode轉換為 ANSI。
如果 DLL 函式不以任何方式處理文本,則可以忽略 DllImportAttribute 的 CharSet 屬性。然而,當 Char 或 String 數據是等式的一部分時,應該將 CharSet 屬性設定為 CharSet.Auto。這樣可以使 CLR 根據宿主 OS 使用適當的字元集。如果沒有顯式地設定 CharSet 屬性,則其默認值為 CharSet.Ansi。這個默認值是有缺點的,因為對於在 Windows 2000、Windows XP 和 Windows NT® 上進行的 interop 調用,它會消極地影響文本參數封送處理的性能。
應該顯式地選擇 CharSet.Ansi 或 CharSet.Unicode 的 CharSet 值而不是使用 CharSet.Auto 的唯一情況是:您顯式地指定了一個導出函式,而該函式特定於這兩種 Win32 OS 中的某一種。ReadDirectoryChangesW API 函式就是這樣的一個例子,它只存在於基於 Windows NT 的作業系統中,並且只支持 Unicode;在這種情況下,您應該顯式地使用 CharSet.Unicode。
有時,Windows API 是否有字元集關係並不明顯。一種決不會有錯的確認方法是在 Platform SDK 中檢查該函式的 C 語言頭檔案。(如果您無法肯定要看哪個頭檔案,則可以查看 Platform SDK 文檔中列出的每個 API 函式的頭檔案。)如果您發現該 API 函式確實定義為一個映射到以 A 或 W 結尾的函式名的宏,則字元集與您嘗試調用的函式有關係。Windows API 函式的一個例子是在 WinUser.h 中聲明的 GetMessage API,您也許會驚訝地發現它有 A 和 W 兩種版本。
SetLastError 錯誤處理非常重要,但在編程時經常被遺忘。當您進行 P/Invoke 調用時,也會面臨其他的挑戰 — 處理託管代碼中 Windows API 錯誤處理和異常之間的區別。我可以給您一點建議。
如果您正在使用 P/Invoke 調用 Windows API 函式,而對於該函式,您使用 GetLastError 來查找擴展的錯誤信息,則應該在外部方法的 DllImportAttribute 中將 SetLastError 屬性設定為 true。這適用於大多數外部方法。
這會導致 CLR 在每次調用外部方法之後快取由 API 函式設定的錯誤。然後,在包裝方法中,可以通過調用類庫的 System.Runtime.InteropServices.Marshal 類型中定義的 Marshal.GetLastWin32Error 方法來獲取快取的錯誤值。我的建議是檢查這些期望來自 API 函式的錯誤值,並為這些值引發一個可感知的異常。對於其他所有失敗情況(包括根本就沒意料到的失敗情況),則引發在 System.ComponentModel 命名空間中定義的 Win32Exception,並將 Marshal.GetLastWin32Error 返回的值傳遞給它。如果您回頭看一下圖 1 中的代碼,您會看到我在 extern MessageBeep 方法的公共包裝中就採用了這種方法。
CallingConvention 我將在此介紹的最後也可能是最不重要的一個 DllImportAttribute 屬性是 CallingConvention。通過此屬性,可以給 CLR 指示應該將哪種函式調用約定用於堆疊中的參數。CallingConvention.Winapi 的默認值是最好的選擇,它在大多數情況下都可行。然而,如果該調用不起作用,則可以檢查 Platform SDK 中的聲明頭檔案,看看您調用的 API 函式是否是一個不符合調用約定標準的異常 API。
通常,本機函式(例如 Windows API 函式或 C- 運行時 DLL 函式)的調用約定描述了如何將參數推入執行緒堆疊或從執行緒堆疊中清除。大多數 Windows API 函式都是首先將函式的最後一個參數推入堆疊,然後由被調用的函式負責清理該堆疊。相反,許多 C-運行時 DLL 函式都被定義為按照方法參數在方法簽名中出現的順序將其推入堆疊,將堆疊清理工作交給調用者。
幸運的是,要讓 P/Invoke 調用工作只需要讓外圍設備理解調用約定即可。通常,從默認值 CallingConvention.Winapi 開始是最好的選擇。然後,在 C 運行時 DLL 函式和少數函式中,可能需要將約定更改為 CallingConvention.Cdecl。

數據處理

數據封送處理是 P/Invoke 具有挑戰性的方面。當在託管和非託管代碼之間傳遞數據時,CLR 遵循許多規則,很少有開發人員會經常遇到它們直至可將這些規則記住。除非您是一名類庫開發人員,否則在通常情況下沒有必要掌握其細節。為了最有效地在 CLR 上使用 P/Invoke,即使只偶爾需要 interop 的應用程式開發人員仍然應該理解數據封送處理的一些基礎知識。
在本月專欄的剩餘部分中,我將討論簡單數字和字元串數據的數據封送處理。我將從最基本的數字數據封送處理開始,然後介紹簡單的指針封送處理和字元串封送處理。
封送數字和邏輯標量
Windows OS 大部分是用 C 編寫的。因此,Windows API 所用到的數據類型要么是 C 類型,要么是通過類型定義或宏定義重新標記的 C 類型。讓我們看看沒有指針的數據封送處理。簡單起見,首先重點討論的是數字和布爾值。
當通過值向 Windows API 函式傳遞參數時,需要知道以下問題的答案:
  • 數據從根本上講是整型的還是浮點型的?
  • 如果數據是整型的,則它是有符號的還是無符號的?
  • 如果數據是整型的,則它的位數是多少?
  • 如果數據是浮點型的,則它是單精度的還是雙精度的?
有時答案很明顯,但有時卻不明顯。Windows API 以各種方式重新定義了基本的 C 數據類型。圖 2 列出了 C 和 Win32 的一些公共數據類型及其規範,以及一個具有匹配規範的公共語言運行庫類型。
通常,只要您選擇一個其規範與該參數的 Win32 類型相匹配的 CLR 類型,您的代碼就能夠正常工作。不過也有一些特例。例如,在 Windows API 中定義的 BOOL 類型是一個有符號的 32 位整型。然而,BOOL 用於指示 Boolean 值 true 或 false。雖然您不用將 BOOL 參數作為 System.Int32 值封送,但是如果使用 System.Boolean 類型,就會獲得更合適的映射。字元類型的映射類似於 BOOL,因為有一個特定的 CLR 類型 (System.Char) 指出字元的含義。
在了解這些信息之後,逐步介紹示例可能是有幫助的。依然採用 beep 主題作為例子,讓我們來試一下 Kernel32.dll 低級 Beep,它會通過計算機的揚聲器發生嘟聲。這個方法的 Platform SDK 文檔可以在 Beep 中找到。本機 API 按以下方式進行記錄:
BOOL Beep( DWORD dwFreq, // Frequency DWORD dwDuration // Duration in milliseconds);
在參數封送處理方面,您的工作是了解什麼 CLR 數據類型與 Beep API 函式所使用的 DWORD 和 BOOL 數據類型相兼容。回顧一下圖 2 中的圖表,您將看到 DWORD 是一個 32 位的無符號整數值,如同 CLR 類型 System.UInt32。這意味著您可以使用 UInt32 值作為送往 Beep 的兩個參數。BOOL 返回值是一個非常有趣的情況,因為該圖表告訴我們,在 Win32 中,BOOL 是一個 32 位的有符號整數。因此,您可以使用 System.Int32 值作為來自 Beep 的返回值。然而,CLR 也定義了 System.Boolean 類型作為 Boolean 值的語義,所以應該使用它來替代。CLR 默認將 System.Boolean 值封送為 32 位的有符號整數。此處所顯示的外部方法定義是用於 Beep 的結果 P/Invoke 方法:
[DllImport("Kernel32.dll", SetLastError=true)]static extern Boolean Beep( UInt32 frequency, UInt32 duration);

指針參數

許多 Windows API 函式將指針作為它們的一個或多個參數。指針增加了封送數據的複雜性,因為它們增加了一個間接層。如果沒有指針,您可以通過值線上程堆疊中傳遞數據。有了指針,則可以通過引用傳遞數據,方法是將該數據的記憶體地址推入執行緒堆疊中。然後,函式通過記憶體地址間接訪問數據。使用託管代碼表示此附加間接層的方式有多種。
在 C# 中,如果將方法參數定義為 ref 或 out,則數據通過引用而不是通過值傳遞。即使您沒有使用 Interop 也是這樣,但只是從一個託管方法調用到另一個託管方法。例如,如果通過 ref 傳遞 System.Int32 參數,則線上程堆疊中傳遞的是該數據的地址,而不是整數值本身。下面是一個定義為通過引用接收整數值的方法的示例:
void FlipInt32(ref Int32 num){ num = -num;}
這裡,FlipInt32 方法獲取一個 Int32 值的地址、訪問數據、對它求反,然後將求反過的值賦給原始變數。在以下代碼中,FlipInt32 方法會將調用程式的變數 x 的值從 10 更改為 -10:
Int32 x = 10;FlipInt32(ref x);
在託管代碼中可以重用這種能力,將指針傳遞給非託管代碼。例如,FileEncryptionStatus API 函式以 32 位無符號位掩碼的形式返回檔案加密狀態。該 API 按以下所示方式進行記錄:
BOOL FileEncryptionStatus( LPCTSTR lpFileName, // file name LPDWORD lpStatus // encryption status);
請注意,該函式並不使用它的返回值返回狀態,而是返回一個 Boolean 值,指示調用是否成功。在成功的情況下,實際的狀態值是通過第二個參數返回的。它的工作方式是調用程式向該函式傳遞指向一個 DWORD 變數的指針,而該 API 函式用狀態值填充指向的記憶體位置。以下代碼片段顯示了一個調用非託管 FileEncryptionStatus 函式的可能外部方法定義:
[DllImport("Advapi32.dll", CharSet=CharSet.Auto)]static extern Boolean FileEncryptionStatus(String filename, out UInt32 status);
該定義使用 out 關鍵字來為 UInt32 狀態值指示 by-ref 參數。這裡我也可以選擇 ref 關鍵字,實際上在運行時會產生相同的機器碼。out 關鍵字只是一個 by-ref 參數的規範,它向 C# 編譯器指示所傳遞的數據只在被調用的函式外部傳遞。相反,如果使用 ref 關鍵字,則編譯器會假定數據可以在被調用的函式的內部和外部傳遞。
託管代碼中 out 和 ref 參數的另一個很好的方面是,地址作為 by-ref 參數傳遞的變數可以是執行緒堆疊中的一個本地變數、一個類或結構的元素,也可以是具有合適數據類型的數組中的一個元素引用。調用程式的這種靈活性使得 by-ref 參數成為封送緩衝區指針以及單數值指針的一個很好的起點。只有在我發現 ref 或 out 參數不符合我的需要的情況下,我才會考慮將指針封送為更複雜的 CLR 類型(例如類或數組對象)。
如果您不熟悉 C 語法或者調用 Windows API 函式,有時很難知道一個方法參數是否需要指針。一個常見的指示符是看參數類型是否是以字母 P 或 LP 開頭的,例如 LPDWORD 或 PINT。在這兩個例子中,LP 和 P 指示參數是一個指針,而它們指向的數據類型分別為 DWORD 或 INT。然而,在有些情況下,可以直接使用 C 語言語法中的星號 (*) 將 API 函式定義為指針。以下代碼片段展示了這方面的示例:
void TakesAPointer(DWORD* pNum);
可以看到,上述函式的唯一一個參數是指向 DWORD 變數的指針。
當通過 P/Invoke 封送指針時,ref 和 out 只用於託管代碼中的值類型。當一個參數的 CLR 類型使用 struct 關鍵字定義時,可以認為該參數是一個值類型。Out 和 ref 用於封送指向這些數據類型的指針,因為通常值類型變數是對象或數據,而在託管代碼中並沒有對值類型的引用。相反,當封送引用類型對象時,並不需要 ref 和 out 關鍵字,因為變數已經是對象的引用了。
如果您對引用類型和值類型之間的差別不是很熟悉,請查閱 2000 年 12 月 發行的 MSDN® Magazine,在 .NET 專欄的主題中可以找到更多信息。大多數 CLR 類型都是引用類型;然而,除了 System.String 和 System.Object,所有的基元類型(例如 System.Int32 和 System.Boolean)都是值類型。
封送不透明 (Opaque) 指針:一種特殊情況
有時在 Windows API 中,方法傳遞或返回的指針是不透明的,這意味著該指針值從技術角度講是一個指針,但代碼卻不直接使用它。相反,代碼將該指針返回給 Windows 以便隨後進行重用。
一個非常常見的例子就是句柄的概念。在 Windows 中,內部數據結構(從檔案到螢幕上的按鈕)在應用程式代碼中都表示為句柄。句柄其實就是不透明的指針或有著指針寬度的數值,應用程式用它來表示內部的 OS 構造。
少數情況下,API 函式也將不透明指針定義為 PVOID 或 LPVOID 類型。在 Windows API 的定義中,這些類型意思就是說該指針沒有類型。
當一個不透明指針返回給您的應用程式(或者您的應用程式期望得到一個不透明指針)時,您應該將參數或返回值封送為 CLR 中的一種特殊類型 — System.IntPtr。當您使用 IntPtr 類型時,通常不使用 out 或 ref 參數,因為 IntPtr 意為直接持有指針。不過,如果您將一個指針封送為一個指針,則對 IntPtr 使用 by-ref 參數是合適的。
在 CLR 類型系統中,System.IntPtr 類型有一個特殊的屬性。不像系統中的其他基類型,IntPtr 並沒有固定的大小。相反,它在運行時的大小是依底層作業系統的正常指針大小而定的。這意味著在 32 位的 Windows 中,IntPtr 變數的寬度是 32 位的,而在 64 位的 Windows 中,實時編譯器編譯的代碼會將 IntPtr 值看作 64 位的值。當在託管代碼和非託管代碼之間封送不透明指針時,這種自動調節大小的特點十分有用。
請記住,任何返回或接受句柄的 API 函式其實操作的就是不透明指針。您的代碼應該將 Windows 中的句柄封送成 System.IntPtr 值。
您可以在託管代碼中將 IntPtr 值強制轉換為 32 位或 64 位的整數值,或將後者強制轉換為前者。然而,當使用 Windows API 函式時,因為指針應是不透明的,所以除了存儲和傳遞給外部方法外,不能將它們另做它用。這種“只限存儲和傳遞”規則的兩個特例是當您需要向外部方法傳遞 null 指針值和需要比較 IntPtr 值與 null 值的情況。為了做到這一點,您不能將零強制轉換為 System.IntPtr,而應該在 IntPtr 類型上使用 Int32.Zero 靜態公共欄位,以便獲得用於比較或賦值的 null 值。

封送文本

在編程時經常要對文本數據進行處理。文本為 interop 製造了一些麻煩,這有兩個原因。首先,底層作業系統可能使用 Unicode 來表示字元串,也可能使用 ANSI。在極少數情況下,例如 MultiByteToWideChar API 函式的兩個參數在字元集上是不一致的。
第二個原因是,當需要進行 P/Invoke 時,要處理文本還需要特別了解到 C 和 CLR 處理文本的方式是不同的。在 C 中,字元串實際上只是一個字元值數組,通常以 null 作為結束符。大多數 Windows API 函式是按照以下條件處理字元串的:對於 ANSI,將其作為字元值數組;對於 Unicode,將其作為寬字元值數組。
幸運的是,CLR 被設計得相當靈活,當封送文本時問題得以輕鬆解決,而不用在意 Windows API 函式期望從您的應用程式得到的是什麼。這裡是一些需要記住的主要考慮事項:
  • 是您的應用程式向 API 函式傳遞文本數據,還是 API 函式向您的應用程式返回字元串數據?或者二者兼有?
  • 您的外部方法應該使用什麼託管類型?
  • API 函式期望得到的是什麼格式的非託管字元串?
我們首先解答最後一個問題。大多數 Windows API 函式都帶有 LPTSTR 或 LPCTSTR 值。(從函式角度看)它們分別是可修改和不可修改的緩衝區,包含以 null 結束的字元數組。“C”代表常數,意味著使用該參數信息不會傳遞到函式外部。LPTSTR 中的“T”表明該參數可以是 Unicode 或 ANSI,取決於您選擇的字元集和底層作業系統的字元集。因為在 Windows API 中大多數字元串參數都是這兩種類型之一,所以只要在 DllImportAttribute 中選擇 CharSet.Auto,CLR 就按默認的方式工作。
然而,有些 API 函式或自定義的 DLL 函式採用不同的方式表示字元串。如果您要用到一個這樣的函式,就可以採用 MarshalAsAttribute 修飾外部方法的字元串參數,並指明一種不同於默認 LPTSTR 的字元串格式。有關 MarshalAsAttribute 的更多信息,請參閱位於 MarshalAsAttribute Class 的 Platform SDK 文檔主題。
現在讓我們看一下字元串信息在您的代碼和非託管函式之間傳遞的方向。有兩種方式可以知道處理字元串時信息的傳遞方向。第一個也是最可靠的一個方法就是首先理解參數的用途。例如,您正調用一個參數,它的名稱類似 CreateMutex 並帶有一個字元串,則可以想像該字元串信息是從應用程式向 API 函式傳遞的。同時,如果您調用 GetUserName,則該函式的名稱表明字元串信息是從該函式向您的應用程式傳遞的。
除了這種比較合理的方法外,第二種查找信息傳遞方向的方式就是查找 API 參數類型中的字母“C”。例如,GetUserName API 函式的第一個參數被定義為 LPTSTR 類型,它代表一個指向 Unicode 或 ANSI 字元串緩衝區的長指針。但是 CreateMutex 的名稱參數被類型化為 LTCTSTR。請注意,這裡的類型定義是一樣的,但增加一個字母“C”來表明緩衝區為常數,API 函式不能寫入。
一旦明確了文本參數是只用作輸入還是用作輸入/輸出,就可以確定使用哪種 CLR 類型作為參數類型。這裡有一些規則。如果字元串參數只用作輸入,則使用 System.String 類型。在託管代碼中,字元串是不變的,適合用於不會被本機 API 函式更改的緩衝區。
如果字元串參數可以用作輸入和/或輸出,則使用 System.StringBuilder 類型。StringBuilder 類型是一個很有用的類庫類型,它可以幫助您有效地構建字元串,也正好可以將緩衝區傳遞給本機函式,由本機函式為您填充字元串數據。一旦函式調用返回,您只需要調用 StringBuilder 對象的 ToString 就可以得到一個 String 對象。
GetShortPathName API 函式能很好地用於顯示什麼時候使用 String、什麼時候使用 StringBuilder,因為它只帶有三個參數:一個輸入字元串、一個輸出字元串和一個指明輸出緩衝區的字元長度的參數。
圖 3 所示為加注釋的非託管 GetShortPathName 函式文檔,它同時指出了輸入和輸出字元串參數。它引出了託管的外部方法定義,也如圖 3 所示。請注意第一個參數被封送為 System.String,因為它是一個只用作輸入的參數。第二個參數代表一個輸出緩衝區,它使用了 System.StringBuilder。

相關詞條

熱門詞條

聯絡我們