隨著越來越多的開發人員將生產應用程序轉到托管代碼,開發人員更頻繁地研究底層操作系統以圖找出一些關鍵功能顯得很自然 — 至少目前是如此。
值得慶幸的是,公共語言運行庫 (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 友好的參數也並非不可以。