下載本文的代碼: NET0307.exe (133KB)
我在自己最近的編程中注意到一個(gè)趨勢,正是這個(gè)趨勢才引出本月的專(zhuān)欄主題。最近,我在基于 Microsoft® .NET Framework的應用程序中完成了大量的 Win32® Interop。我并不是要說(shuō)我的應用程序充滿(mǎn)了自定義的 interop 代碼,但有時(shí)我會(huì )在 .NETFramework 類(lèi)庫中碰到一些次要但又繁絮、不充分的內容,通過(guò)調用該 Windows® API,可以快速減少這樣的麻煩。
因此我認為,.NET Framework 1.0 或 1.1 版類(lèi)庫中存在任何 Windows 所沒(méi)有的功能限制都不足為怪。畢竟,32 位的Windows(不管何種版本)是一個(gè)成熟的操作系統,為廣大客戶(hù)服務(wù)了十多年。相比之下,.NET Framework 卻是一個(gè)新事物。
隨著(zhù)越來(lái)越多的開(kāi)發(fā)人員將生產(chǎn)應用程序轉到托管代碼,開(kāi)發(fā)人員更頻繁地研究底層操作系統以圖找出一些關(guān)鍵功能顯得很自然 — 至少目前是如此。
值得慶幸的是,公共語(yǔ)言運行庫 (CLR) 的 interop 功能(稱(chēng)為平臺調用(P/Invoke))非常完善。在本專(zhuān)欄中,我將重點(diǎn)介紹如何實(shí)際使用 P/Invoke 來(lái)調用 Windows API 函數。當指 CLR 的COM Interop 功能時(shí),P/Invoke 當作名詞使用;當指該功能的使用時(shí),則將其當作動(dòng)詞使用。我并不打算直接介紹 COMInterop,因為它比 P/Invoke 具有更好的可訪(fǎng)問(wèn)性,卻更加復雜,這有點(diǎn)自相矛盾,這使得將 COM Interop作為專(zhuān)欄主題來(lái)討論不太簡(jiǎn)明扼要。
走進(jìn) P/Invoke
首先從考察一個(gè)簡(jiǎn)單的 P/Invoke 示例開(kāi)始。讓我們看一看如何調用 Win32 MessageBeep 函數,它的非托管聲明如以下代碼所示:
BOOL MessageBeep(UINT uType // beep type);
為了調用 MessageBeep,您需要在 C# 中將以下代碼添加到一個(gè)類(lèi)或結構定義中:
[DllImport("User32.dll")]static extern Boolean MessageBeep(UInt32 beepType);令人驚訝的是,只需要這段代碼就可以使托管代碼調用非托管的 MessageBeepAPI。它不是一個(gè)方法調用,而是一個(gè)外部方法定義。(另外,它接近于一個(gè)來(lái)自 C 而 C#允許的直接端口,因此以它為起點(diǎn)來(lái)介紹一些概念是有幫助的。)來(lái)自托管代碼的可能調用如下所示:
MessageBeep(0);
請注意,現在 MessageBeep 方法被聲明為 static。這是 P/Invoke 方法所要求的,因為在該Windows API 中沒(méi)有一致的實(shí)例概念。接下來(lái),還要注意該方法被標記為 extern。這是提示編譯器該方法是通過(guò)一個(gè)從 DLL導出的函數實(shí)現的,因此不需要提供方法體。
說(shuō)到缺少方法體,您是否注意到 MessageBeep聲明并沒(méi)有包含一個(gè)方法體?與大多數算法由中間語(yǔ)言 (IL) 指令組成的托管方法不同,P/Invoke 方法只是元數據,實(shí)時(shí) (JIT)編譯器在運行時(shí)通過(guò)它將托管代碼與非托管的 DLL 函數連接起來(lái)。執行這種到非托管世界的連接所需的一個(gè)重要信息就是導出非托管方法的 DLL的名稱(chēng)。這一信息是由 MessageBeep 方法聲明之前的 DllImport 自定義屬性提供的。在本例中,可以看到,MessageBeep非托管 API 是由 Windows 中的 User32.dll 導出的。
到現在為止,關(guān)于調用 MessageBeep 就剩兩個(gè)話(huà)題沒(méi)有介紹,請回顧一下,調用的代碼與以下所示代碼片段非常相似:
[DllImport("User32.dll")]static extern Boolean MessageBeep(UInt32 beepType);最后這兩個(gè)話(huà)題是與數據封送處理 (data marshaling)和從托管代碼到非托管函數的實(shí)際方法調用有關(guān)的話(huà)題。調用非托管 MessageBeep 函數可以由找到作用域內的externMessageBeep聲明的任何托管代碼執行。該調用類(lèi)似于任何其他對靜態(tài)方法的調用。它與其他任何托管方法調用的共同之處在于帶來(lái)了數據封送處理的需要。
C#的規則之一是它的調用語(yǔ)法只能訪(fǎng)問(wèn) CLR 數據類(lèi)型,例如 System.UInt32 和 System.Boolean。C# 顯然不識別Windows API 中使用的基于 C 的數據類(lèi)型(例如 UINT 和 BOOL),這些類(lèi)型只是 C 語(yǔ)言類(lèi)型的類(lèi)型定義而已。所以當Windows API 函數 MessageBeep 按以下方式編寫(xiě)時(shí)
BOOL MessageBeep( UINT uType )
外部方法就必須使用 CLR 類(lèi)型來(lái)定義,如您在前面的代碼片段中所看到的。需要使用與基礎 API 函數類(lèi)型不同但與之兼容的 CLR 類(lèi)型是 P/Invoke 較難使用的一個(gè)方面。因此,在本專(zhuān)欄的后面我將用完整的章節來(lái)介紹數據封送處理。
樣式
在 C# 中對 Windows API 進(jìn)行 P/Invoke 調用是很簡(jiǎn)單的。但如果類(lèi)庫拒絕使您的應用程序發(fā)出嘟聲,應該想方設法調用 Windows 使它進(jìn)行這項工作,是嗎?
是的。但是與選擇的方法有關(guān),而且關(guān)系甚大!通常,如果類(lèi)庫提供某種途徑來(lái)實(shí)現您的意圖,則最好使用 API 而不要直接調用非托管代碼,因為 CLR類(lèi)型和 Win32 之間在樣式上有很大的不同。我可以將關(guān)于這個(gè)問(wèn)題的建議歸結為一句話(huà)。當您進(jìn)行 P/Invoke時(shí),不要使應用程序邏輯直接屬于任何外部方法或其中的構件。如果您遵循這個(gè)小規則,從長(cháng)遠看經(jīng)常會(huì )省去許多的麻煩。
圖 1 中的代碼顯示了我所討論的 MessageBeep 外部方法的最少附加代碼。圖 1中并沒(méi)有任何顯著(zhù)的變化,而只是對無(wú)包裝的外部方法進(jìn)行一些普通的改進(jìn),這可以使工作更加輕松一些。從頂部開(kāi)始,您會(huì )注意到一個(gè)名為 Sound的完整類(lèi)型,它專(zhuān)用于 MessageBeep。如果我需要使用 Windows API 函數 PlaySound來(lái)添加對播放波形的支持,則可以重用 Sound類(lèi)型。然而,我不會(huì )因公開(kāi)單個(gè)公共靜態(tài)方法的類(lèi)型而生氣。畢竟這只是應用程序代碼而已。還應該注意到,Sound是密封的,并定義了一個(gè)空的私有構造函數。這些只是一些細節,目的是使用戶(hù)不會(huì )錯誤地從 Sound 派生類(lèi)或者創(chuàng )建它的實(shí)例。
圖 1中的代碼的下一個(gè)特征是,P/Invoke 出現位置的實(shí)際外部方法是 Sound 的私有方法。這個(gè)方法只是由公共 MessageBeep方法間接公開(kāi),后者接受 BeepTypes 類(lèi)型的參數。這個(gè)間接的額外層是一個(gè)很關(guān)鍵的細節,它提供了以下好處。首先,應該在類(lèi)庫中引入一個(gè)未來(lái)的beep 托管方法,可以重復地通過(guò)公共 MessageBeep 方法來(lái)使用托管 API,而不必更改應用程序中的其余代碼。
該包裝方法的第二個(gè)好處是:當您進(jìn)行 P/Invoke 調用時(shí),您放棄了免受訪(fǎng)問(wèn)沖突和其他低級破壞的權利,這通常是由 CLR提供的。緩沖方法可以保護您的應用程序的其余部分免受訪(fǎng)問(wèn)沖突及類(lèi)似問(wèn)題的影響(即使它不做任何事而只是傳遞參數)。該緩沖方法將由 P/Invoke調用引入的任何潛在的錯誤本地化。
將私有外部方法隱藏在公共包裝后面的第三同時(shí)也是最后的一個(gè)好處是,提供了向該方法添加一些最小的 CLR 樣式的機會(huì )。例如,在圖 1中,我將 Windows API 函數返回的 Boolean 失敗轉換成更像 CLR 的異常。我還定義了一個(gè)名為 BeepTypes的枚舉類(lèi)型,它的成員對應于同該 Windows API 一起使用的定義值。由于 C#不支持定義,因此可以使用托管枚舉類(lèi)型來(lái)避免幻數向整個(gè)應用程序代碼擴散。
包裝方法的最后一個(gè)好處對于簡(jiǎn)單的 Windows API函數(如 MessageBeep)誠然是微不足道的。但是當您開(kāi)始調用更復雜的非托管函數時(shí),您會(huì )發(fā)現,手動(dòng)將 Windows API樣式轉換成對 CLR 更加友好的方法所帶來(lái)的好處會(huì )越來(lái)越多。越是打算在整個(gè)應用程序中重用 interop功能,越是應該認真地考慮包裝的設計。同時(shí)我認為,在非面向對象的靜態(tài)包裝方法中使用對 CLR 友好的參數也并非不可以。
DLL Import 屬性
現在是更深入地進(jìn)行探討的時(shí)候了。在對托管代碼進(jìn)行 P/Invoke 調用時(shí),DllImportAttribute類(lèi)型扮演著(zhù)重要的角色。DllImportAttribute 的主要作用是給 CLR 指示哪個(gè) DLL 導出您想要調用的函數。相關(guān) DLL的名稱(chēng)被作為一個(gè)構造函數參數傳遞給 DllImportAttribute。
如果您無(wú)法肯定哪個(gè) DLL 定義了您要使用的Windows API 函數,Platform SDK 文檔將為您提供最好的幫助資源。在 Windows API函數主題文字臨近結尾的位置,SDK 文檔指定了 C 應用程序要使用該函數必須鏈接的 .lib 文件。在幾乎所有的情況下,該 .lib文件具有與定義該函數的系統 DLL 文件相同的名稱(chēng)。例如,如果該函數需要 C 應用程序鏈接到 Kernel32.lib,則該函數就定義在Kernel32.dll 中。您可以在 MessageBeep 中找到有關(guān) MessageBeep 的 Platform SDK 文檔主題。在該主題結尾處,您會(huì )注意到它指出庫文件是 User32.lib;這表明 MessageBeep 是從 User32.dll 中導出的。
可選的 DllImportAttribute 屬性
除了指出宿主 DLL 外,DllImportAttribute 還包含了一些可選屬性,其中四個(gè)特別有趣:EntryPoint、CharSet、SetLastError 和 CallingConvention。
EntryPoint在不希望外部托管方法具有與 DLL 導出相同的名稱(chēng)的情況下,可以設置該屬性來(lái)指示導出的 DLL函數的入口點(diǎn)名稱(chēng)。當您定義兩個(gè)調用相同非托管函數的外部方法時(shí),這特別有用。另外,在 Windows 中還可以通過(guò)它們的序號值綁定到導出的DLL 函數。如果您需要這樣做,則諸如“#1”或“#129”的 EntryPoint 值指示 DLL 中非托管函數的序號值而不是函數名。
CharSet 對于字符集,并非所有版本的 Windows 都是同樣創(chuàng )建的。Windows 9x系列產(chǎn)品缺少重要的 Unicode 支持,而 Windows NT 和 Windows CE 系列則一開(kāi)始就使用Unicode。在這些操作系統上運行的 CLR 將Unicode 用于 String 和 Char 數據的內部表示。但也不必擔心 — 當調用Windows 9x API 函數時(shí),CLR 會(huì )自動(dòng)進(jìn)行必要的轉換,將其從 Unicode轉換為 ANSI。
如果DLL 函數不以任何方式處理文本,則可以忽略 DllImportAttribute 的 CharSet 屬性。然而,當 Char 或String 數據是等式的一部分時(shí),應該將 CharSet 屬性設置為 CharSet.Auto。這樣可以使 CLR 根據宿主 OS使用適當的字符集。如果沒(méi)有顯式地設置 CharSet 屬性,則其默認值為 CharSet.Ansi。這個(gè)默認值是有缺點(diǎn)的,因為對于在Windows 2000、Windows XP 和 Windows NT® 上進(jìn)行的 interop調用,它會(huì )消極地影響文本參數封送處理的性能。
應該顯式地選擇 CharSet.Ansi 或 CharSet.Unicode 的CharSet 值而不是使用 CharSet.Auto 的唯一情況是:您顯式地指定了一個(gè)導出函數,而該函數特定于這兩種 Win32 OS中的某一種。ReadDirectoryChangesW API 函數就是這樣的一個(gè)例子,它只存在于基于 Windows NT的操作系統中,并且只支持 Unicode;在這種情況下,您應該顯式地使用 CharSet.Unicode。
有時(shí),WindowsAPI 是否有字符集關(guān)系并不明顯。一種決不會(huì )有錯的確認方法是在 Platform SDK 中檢查該函數的 C語(yǔ)言頭文件。(如果您無(wú)法肯定要看哪個(gè)頭文件,則可以查看 Platform SDK 文檔中列出的每個(gè) API 函數的頭文件。)如果您發(fā)現該API 函數確實(shí)定義為一個(gè)映射到以 A 或 W 結尾的函數名的宏,則字符集與您嘗試調用的函數有關(guān)系。Windows API 函數的一個(gè)例子是在WinUser.h 中聲明的 GetMessage API,您也許會(huì )驚訝地發(fā)現它有 A 和 W 兩種版本。
SetLastError 錯誤處理非常重要,但在編程時(shí)經(jīng)常被遺忘。當您進(jìn)行 P/Invoke 調用時(shí),也會(huì )面臨其他的挑戰 — 處理托管代碼中 Windows API 錯誤處理和異常之間的區別。我可以給您一點(diǎn)建議。
如果您正在使用 P/Invoke 調用 Windows API 函數,而對于該函數,您使用 GetLastError來(lái)查找擴展的錯誤信息,則應該在外部方法的 DllImportAttribute 中將 SetLastError 屬性設置為true。這適用于大多數外部方法。
這會(huì )導致 CLR 在每次調用外部方法之后緩存由 API函數設置的錯誤。然后,在包裝方法中,可以通過(guò)調用類(lèi)庫的 System.Runtime.InteropServices.Marshal類(lèi)型中定義的 Marshal.GetLastWin32Error 方法來(lái)獲取緩存的錯誤值。我的建議是檢查這些期望來(lái)自 API函數的錯誤值,并為這些值引發(fā)一個(gè)可感知的異常。對于其他所有失敗情況(包括根本就沒(méi)意料到的失敗情況),則引發(fā)在System.ComponentModel 命名空間中定義的 Win32Exception,并將Marshal.GetLastWin32Error 返回的值傳遞給它。如果您回頭看一下圖 1 中的代碼,您會(huì )看到我在 extern MessageBeep 方法的公共包裝中就采用了這種方法。
CallingConvention我將在此介紹的最后也可能是最不重要的一個(gè) DllImportAttribute 屬性是 CallingConvention。通過(guò)此屬性,可以給CLR 指示應該將哪種函數調用約定用于堆棧中的參數。CallingConvention.Winapi的默認值是最好的選擇,它在大多數情況下都可行。然而,如果該調用不起作用,則可以檢查 Platform SDK 中的聲明頭文件,看看您調用的API 函數是否是一個(gè)不符合調用約定標準的異常 API。
通常,本機函數(例如 Windows API 函數或 C- 運行時(shí)DLL 函數)的調用約定描述了如何將參數推入線(xiàn)程堆?;驈木€(xiàn)程堆棧中清除。大多數 Windows API函數都是首先將函數的最后一個(gè)參數推入堆棧,然后由被調用的函數負責清理該堆棧。相反,許多 C-運行時(shí) DLL函數都被定義為按照方法參數在方法簽名中出現的順序將其推入堆棧,將堆棧清理工作交給調用者。
幸運的是,要讓 P/Invoke調用工作只需要讓外圍設備理解調用約定即可。通常,從默認值 CallingConvention.Winapi 開(kāi)始是最好的選擇。然后,在 C運行時(shí) DLL 函數和少數函數中,可能需要將約定更改為 CallingConvention.Cdecl。
數據封送處理
數據封送處理是 P/Invoke 具有挑戰性的方面。當在托管和非托管代碼之間傳遞數據時(shí),CLR遵循許多規則,很少有開(kāi)發(fā)人員會(huì )經(jīng)常遇到它們直至可將這些規則記住。除非您是一名類(lèi)庫開(kāi)發(fā)人員,否則在通常情況下沒(méi)有必要掌握其細節。為了最有效地在CLR 上使用 P/Invoke,即使只偶爾需要 interop 的應用程序開(kāi)發(fā)人員仍然應該理解數據封送處理的一些基礎知識。
在本月專(zhuān)欄的剩余部分中,我將討論簡(jiǎn)單數字和字符串數據的數據封送處理。我將從最基本的數字數據封送處理開(kāi)始,然后介紹簡(jiǎn)單的指針?lè )馑吞幚砗妥址馑吞幚怼?/p>
封送數字和邏輯標量
Windows OS 大部分是用 C 編寫(xiě)的。因此,Windows API 所用到的數據類(lèi)型要么是 C 類(lèi)型,要么是通過(guò)類(lèi)型定義或宏定義重新標記的 C 類(lèi)型。讓我們看看沒(méi)有指針的數據封送處理。簡(jiǎn)單起見(jiàn),首先重點(diǎn)討論的是數字和布爾值。
當通過(guò)值向 Windows API 函數傳遞參數時(shí),需要知道以下問(wèn)題的答案:
| • | 數據從根本上講是整型的還是浮點(diǎn)型的? |
| • | 如果數據是整型的,則它是有符號的還是無(wú)符號的? |
| • | 如果數據是整型的,則它的位數是多少? |
| • | 如果數據是浮點(diǎn)型的,則它是單精度的還是雙精度的? |
有時(shí)答案很明顯,但有時(shí)卻不明顯。Windows API 以各種方式重新定義了基本的 C 數據類(lèi)型。圖 2 列出了 C 和 Win32 的一些公共數據類(lèi)型及其規范,以及一個(gè)具有匹配規范的公共語(yǔ)言運行庫類(lèi)型。
通常,只要您選擇一個(gè)其規范與該參數的 Win32 類(lèi)型相匹配的 CLR 類(lèi)型,您的代碼就能夠正常工作。不過(guò)也有一些特例。例如,在 WindowsAPI 中定義的 BOOL 類(lèi)型是一個(gè)有符號的 32 位整型。然而,BOOL 用于指示 Boolean 值 true 或false。雖然您不用將 BOOL 參數作為 System.Int32 值封送,但是如果使用 System.Boolean類(lèi)型,就會(huì )獲得更合適的映射。字符類(lèi)型的映射類(lèi)似于 BOOL,因為有一個(gè)特定的 CLR 類(lèi)型 (System.Char) 指出字符的含義。
在了解這些信息之后,逐步介紹示例可能是有幫助的。依然采用 beep 主題作為例子,讓我們來(lái)試一下 Kernel32.dll 低級 Beep,它會(huì )通過(guò)計算機的揚聲器發(fā)生嘟聲。這個(gè)方法的 Platform SDK 文檔可以在 Beep 中找到。本機 API 按以下方式進(jìn)行記錄:
BOOL Beep(DWORD dwFreq, // FrequencyDWORD dwDuration // Duration in milliseconds);
在參數封送處理方面,您的工作是了解什么 CLR 數據類(lèi)型與 Beep API 函數所使用的 DWORD 和 BOOL 數據類(lèi)型相兼容?;仡櫼幌?a target="_blank">圖 2中的圖表,您將看到 DWORD 是一個(gè) 32 位的無(wú)符號整數值,如同 CLR 類(lèi)型 System.UInt32。這意味著(zhù)您可以使用UInt32 值作為送往 Beep 的兩個(gè)參數。BOOL 返回值是一個(gè)非常有趣的情況,因為該圖表告訴我們,在 Win32 中,BOOL 是一個(gè)32 位的有符號整數。因此,您可以使用 System.Int32 值作為來(lái)自 Beep 的返回值。然而,CLR 也定義了System.Boolean 類(lèi)型作為 Boolean 值的語(yǔ)義,所以應該使用它來(lái)替代。CLR 默認將 System.Boolean 值封送為32 位的有符號整數。此處所顯示的外部方法定義是用于 Beep 的結果 P/Invoke 方法:
[DllImport("Kernel32.dll", SetLastError=true)]static extern Boolean Beep(UInt32 frequency, UInt32 duration);指針參數
許多 Windows API函數將指針作為它們的一個(gè)或多個(gè)參數。指針增加了封送數據的復雜性,因為它們增加了一個(gè)間接層。如果沒(méi)有指針,您可以通過(guò)值在線(xiàn)程堆棧中傳遞數據。有了指針,則可以通過(guò)引用傳遞數據,方法是將該數據的內存地址推入線(xiàn)程堆棧中。然后,函數通過(guò)內存地址間接訪(fǎng)問(wèn)數據。使用托管代碼表示此附加間接層的方式有多種。
在 C# 中,如果將方法參數定義為 ref 或 out,則數據通過(guò)引用而不是通過(guò)值傳遞。即使您沒(méi)有使用 Interop也是這樣,但只是從一個(gè)托管方法調用到另一個(gè)托管方法。例如,如果通過(guò) ref 傳遞 System.Int32參數,則在線(xiàn)程堆棧中傳遞的是該數據的地址,而不是整數值本身。下面是一個(gè)定義為通過(guò)引用接收整數值的方法的示例:
void FlipInt32(ref Int32 num){num = -num;}這里,FlipInt32 方法獲取一個(gè) Int32 值的地址、訪(fǎng)問(wèn)數據、對它求反,然后將求反過(guò)的值賦給原始變量。在以下代碼中,FlipInt32 方法會(huì )將調用程序的變量 x 的值從 10 更改為 -10:
Int32 x = 10;FlipInt32(ref x);
在托管代碼中可以重用這種能力,將指針傳遞給非托管代碼。例如,FileEncryptionStatus API 函數以 32 位無(wú)符號位掩碼的形式返回文件加密狀態(tài)。該 API 按以下所示方式進(jìn)行記錄:
BOOL FileEncryptionStatus(LPCTSTR lpFileName, // file nameLPDWORD lpStatus // encryption status);
請注意,該函數并不使用它的返回值返回狀態(tài),而是返回一個(gè) Boolean值,指示調用是否成功。在成功的情況下,實(shí)際的狀態(tài)值是通過(guò)第二個(gè)參數返回的。它的工作方式是調用程序向該函數傳遞指向一個(gè) DWORD變量的指針,而該 API 函數用狀態(tài)值填充指向的內存位置。以下代碼片段顯示了一個(gè)調用非托管 FileEncryptionStatus函數的可能外部方法定義:
[DllImport("Advapi32.dll", CharSet=CharSet.Auto)]static extern Boolean FileEncryptionStatus(String filename,out UInt32 status);該定義使用 out 關(guān)鍵字來(lái)為 UInt32 狀態(tài)值指示 by-ref 參數。這里我也可以選擇 ref關(guān)鍵字,實(shí)際上在運行時(shí)會(huì )產(chǎn)生相同的機器碼。out 關(guān)鍵字只是一個(gè) by-ref 參數的規范,它向 C#編譯器指示所傳遞的數據只在被調用的函數外部傳遞。相反,如果使用 ref 關(guān)鍵字,則編譯器會(huì )假定數據可以在被調用的函數的內部和外部傳遞。
托管代碼中 out 和 ref 參數的另一個(gè)很好的方面是,地址作為 by-ref參數傳遞的變量可以是線(xiàn)程堆棧中的一個(gè)本地變量、一個(gè)類(lèi)或結構的元素,也可以是具有合適數據類(lèi)型的數組中的一個(gè)元素引用。調用程序的這種靈活性使得by-ref 參數成為封送緩沖區指針以及單數值指針的一個(gè)很好的起點(diǎn)。只有在我發(fā)現 ref 或 out參數不符合我的需要的情況下,我才會(huì )考慮將指針?lè )馑蜑楦鼜碗s的 CLR 類(lèi)型(例如類(lèi)或數組對象)。
如果您不熟悉 C 語(yǔ)法或者調用Windows API 函數,有時(shí)很難知道一個(gè)方法參數是否需要指針。一個(gè)常見(jiàn)的指示符是看參數類(lèi)型是否是以字母 P 或 LP 開(kāi)頭的,例如LPDWORD 或 PINT。在這兩個(gè)例子中,LP 和 P 指示參數是一個(gè)指針,而它們指向的數據類(lèi)型分別為 DWORD 或INT。然而,在有些情況下,可以直接使用 C 語(yǔ)言語(yǔ)法中的星號 (*) 將 API 函數定義為指針。以下代碼片段展示了這方面的示例:
void TakesAPointer(DWORD* pNum);
可以看到,上述函數的唯一一個(gè)參數是指向 DWORD 變量的指針。
當通過(guò) P/Invoke 封送指針時(shí),ref和 out 只用于托管代碼中的值類(lèi)型。當一個(gè)參數的 CLR 類(lèi)型使用 struct 關(guān)鍵字定義時(shí),可以認為該參數是一個(gè)值類(lèi)型。Out 和ref用于封送指向這些數據類(lèi)型的指針,因為通常值類(lèi)型變量是對象或數據,而在托管代碼中并沒(méi)有對值類(lèi)型的引用。相反,當封送引用類(lèi)型對象時(shí),并不需要ref 和 out 關(guān)鍵字,因為變量已經(jīng)是對象的引用了。
如果您對引用類(lèi)型和值類(lèi)型之間的差別不是很熟悉,請查閱 2000 年 12 月 發(fā)行的 MSDN®Magazine,在 .NET 專(zhuān)欄的主題中可以找到更多信息。大多數 CLR 類(lèi)型都是引用類(lèi)型;然而,除了 System.String 和System.Object,所有的基元類(lèi)型(例如 System.Int32 和 System.Boolean)都是值類(lèi)型。
封送不透明 (Opaque) 指針:一種特殊情況
有時(shí)在 Windows API 中,方法傳遞或返回的指針是不透明的,這意味著(zhù)該指針值從技術(shù)角度講是一個(gè)指針,但代碼卻不直接使用它。相反,代碼將該指針?lè )祷亟o Windows 以便隨后進(jìn)行重用。
一個(gè)非常常見(jiàn)的例子就是句柄的概念。在 Windows 中,內部數據結構(從文件到屏幕上的按鈕)在應用程序代碼中都表示為句柄。句柄其實(shí)就是不透明的指針或有著(zhù)指針寬度的數值,應用程序用它來(lái)表示內部的 OS 構造。
少數情況下,API 函數也將不透明指針定義為 PVOID 或 LPVOID 類(lèi)型。在 Windows API 的定義中,這些類(lèi)型意思就是說(shuō)該指針沒(méi)有類(lèi)型。
當一個(gè)不透明指針?lè )祷亟o您的應用程序(或者您的應用程序期望得到一個(gè)不透明指針)時(shí),您應該將參數或返回值封送為 CLR 中的一種特殊類(lèi)型 —System.IntPtr。當您使用 IntPtr 類(lèi)型時(shí),通常不使用 out 或 ref 參數,因為 IntPtr意為直接持有指針。不過(guò),如果您將一個(gè)指針?lè )馑蜑橐粋€(gè)指針,則對 IntPtr 使用 by-ref 參數是合適的。
在 CLR類(lèi)型系統中,System.IntPtr 類(lèi)型有一個(gè)特殊的屬性。不像系統中的其他基類(lèi)型,IntPtr并沒(méi)有固定的大小。相反,它在運行時(shí)的大小是依底層操作系統的正常指針大小而定的。這意味著(zhù)在 32 位的 Windows 中,IntPtr變量的寬度是 32 位的,而在 64 位的 Windows 中,實(shí)時(shí)編譯器編譯的代碼會(huì )將 IntPtr 值看作 64位的值。當在托管代碼和非托管代碼之間封送不透明指針時(shí),這種自動(dòng)調節大小的特點(diǎn)十分有用。
請記住,任何返回或接受句柄的 API 函數其實(shí)操作的就是不透明指針。您的代碼應該將 Windows 中的句柄封送成 System.IntPtr 值。
您可以在托管代碼中將 IntPtr 值強制轉換為 32 位或 64 位的整數值,或將后者強制轉換為前者。然而,當使用 Windows API函數時(shí),因為指針應是不透明的,所以除了存儲和傳遞給外部方法外,不能將它們另做它用。這種“只限存儲和傳遞”規則的兩個(gè)特例是當您需要向外部方法傳遞null 指針值和需要比較 IntPtr 值與 null 值的情況。為了做到這一點(diǎn),您不能將零強制轉換為 System.IntPtr,而應該在IntPtr 類(lèi)型上使用 Int32.Zero 靜態(tài)公共字段,以便獲得用于比較或賦值的 null 值。
封送文本
在編程時(shí)經(jīng)常要對文本數據進(jìn)行處理。文本為 interop 制造了一些麻煩,這有兩個(gè)原因。首先,底層操作系統可能使用 Unicode來(lái)表示字符串,也可能使用 ANSI。在極少數情況下,例如 MultiByteToWideChar API 函數的兩個(gè)參數在字符集上是不一致的。
第二個(gè)原因是,當需要進(jìn)行 P/Invoke 時(shí),要處理文本還需要特別了解到 C 和 CLR 處理文本的方式是不同的。在 C中,字符串實(shí)際上只是一個(gè)字符值數組,通常以 null 作為結束符。大多數 Windows API 函數是按照以下條件處理字符串的:對于A(yíng)NSI,將其作為字符值數組;對于 Unicode,將其作為寬字符值數組。
幸運的是,CLR 被設計得相當靈活,當封送文本時(shí)問(wèn)題得以輕松解決,而不用在意 Windows API 函數期望從您的應用程序得到的是什么。這里是一些需要記住的主要考慮事項:
| • | 是您的應用程序向 API 函數傳遞文本數據,還是 API 函數向您的應用程序返回字符串數據?或者二者兼有? |
| • | 您的外部方法應該使用什么托管類(lèi)型? |
| • | API 函數期望得到的是什么格式的非托管字符串? |
我們首先解答最后一個(gè)問(wèn)題。大多數 Windows API 函數都帶有 LPTSTR 或 LPCTSTR值。(從函數角度看)它們分別是可修改和不可修改的緩沖區,包含以 null結束的字符數組。“C”代表常數,意味著(zhù)使用該參數信息不會(huì )傳遞到函數外部。LPTSTR 中的“T”表明該參數可以是 Unicode 或ANSI,取決于您選擇的字符集和底層操作系統的字符集。因為在 Windows API 中大多數字符串參數都是這兩種類(lèi)型之一,所以只要在DllImportAttribute 中選擇 CharSet.Auto,CLR 就按默認的方式工作。
然而,有些 API函數或自定義的 DLL 函數采用不同的方式表示字符串。如果您要用到一個(gè)這樣的函數,就可以采用 MarshalAsAttribute修飾外部方法的字符串參數,并指明一種不同于默認 LPTSTR 的字符串格式。有關(guān) MarshalAsAttribute 的更多信息,請參閱位于MarshalAsAttribute Class 的 Platform SDK 文檔主題。
現在讓我們看一下字符串信息在您的代碼和非托管函數之間傳遞的方向。有兩種方式可以知道處理字符串時(shí)信息的傳遞方向。第一個(gè)也是最可靠的一個(gè)方法就是首先理解參數的用途。例如,您正調用一個(gè)參數,它的名稱(chēng)類(lèi)似 CreateMutex 并帶有一個(gè)字符串,則可以想像該字符串信息是從應用程序向 API函數傳遞的。同時(shí),如果您調用 GetUserName,則該函數的名稱(chēng)表明字符串信息是從該函數向您的應用程序傳遞的。
除了這種比較合理的方法外,第二種查找信息傳遞方向的方式就是查找 API 參數類(lèi)型中的字母“C”。例如,GetUserName API 函數的第一個(gè)參數被定義為L(cháng)PTSTR 類(lèi)型,它代表一個(gè)指向 Unicode 或 ANSI 字符串緩沖區的長(cháng)指針。但是 CreateMutex 的名稱(chēng)參數被類(lèi)型化為L(cháng)TCTSTR。請注意,這里的類(lèi)型定義是一樣的,但增加一個(gè)字母“C”來(lái)表明緩沖區為常數,API 函數不能寫(xiě)入。
一旦明確了文本參數是只用作輸入還是用作輸入/輸出,就可以確定使用哪種 CLR 類(lèi)型作為參數類(lèi)型。這里有一些規則。如果字符串參數只用作輸入,則使用 System.String 類(lèi)型。在托管代碼中,字符串是不變的,適合用于不會(huì )被本機 API 函數更改的緩沖區。
如果字符串參數可以用作輸入和/或輸出,則使用 System.StringBuilder 類(lèi)型。StringBuilder類(lèi)型是一個(gè)很有用的類(lèi)庫類(lèi)型,它可以幫助您有效地構建字符串,也正好可以將緩沖區傳遞給本機函數,由本機函數為您填充字符串數據。一旦函數調用返回,您只需要調用 StringBuilder 對象的 ToString 就可以得到一個(gè) String 對象。
GetShortPathName API 函數能很好地用于顯示什么時(shí)候使用 String、什么時(shí)候使用 StringBuilder,因為它只帶有三個(gè)參數:一個(gè)輸入字符串、一個(gè)輸出字符串和一個(gè)指明輸出緩沖區的字符長(cháng)度的參數。
圖 3 所示為加注釋的非托管 GetShortPathName 函數文檔,它同時(shí)指出了輸入和輸出字符串參數。它引出了托管的外部方法定義,也如圖 3 所示。請注意第一個(gè)參數被封送為 System.String,因為它是一個(gè)只用作輸入的參數。第二個(gè)參數代表一個(gè)輸出緩沖區,它使用了 System.StringBuilder。
小結
本月專(zhuān)欄所介紹的 P/Invoke 功能足夠調用 Windows 中的許多 API 函數。然而,如果您大量用到interop,則會(huì )最終發(fā)現自己封送了很復雜的數據結構,甚至可能需要在托管代碼中通過(guò)指針直接訪(fǎng)問(wèn)內存。實(shí)際上,本機代碼中的 interop可以是一個(gè)將細節和低級比特藏在里面的真正的潘多拉盒子。CLR、C# 和托管 C++ 提供了許多有用的功能;也許以后我會(huì )在本專(zhuān)欄介紹高級的P/Invoke 話(huà)題。
同時(shí),只要您覺(jué)得 .NET Framework 類(lèi)庫無(wú)法播放您的聲音或者為您執行其他一些功能,您可以知道如何向原始而優(yōu)秀的 Windows API 尋求一些幫助。
將您要發(fā)給 Jason 的問(wèn)題和意見(jiàn)發(fā)送到 dot-net@microsoft.com。
Jason Clark 為 Microsoft 和 Wintellect (http://www.wintellect.com) 提供培訓和咨詢(xún),以前是 Windows NT 和 Windows 2000 Server 團隊的開(kāi)發(fā)人員。他與人合著(zhù)了 Programming Server-side Applications for Microsoft Windows 2000 (Microsoft Press, 2000)。您可以通過(guò) JClark@Wintellect.com 與 Jason 取得聯(lián)系。
聯(lián)系客服