免费视频淫片aa毛片_日韩高清在线亚洲专区vr_日韩大片免费观看视频播放_亚洲欧美国产精品完整版

打開APP
userphoto
未登錄

開通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

開通VIP
在 C# 中通過 P/Invoke 調(diào)用Win32 DLL

在 C# 中通過 P/Invoke 調(diào)用Win32 DLL

發(fā)布日期: 1/13/2005 | 更新日期: 1/13/2005

Jason Clark

下載本文的代碼: NET0307.exe (133KB)

我在自己最近的編程中注意到一個(gè)趨勢(shì),正是這個(gè)趨勢(shì)才引出本月的專欄主題。最近,我在基于 Microsoft® .NET Framework的應(yīng)用程序中完成了大量的 Win32® Interop。我并不是要說我的應(yīng)用程序充滿了自定義的 interop 代碼,但有時(shí)我會(huì)在 .NETFramework 類庫中碰到一些次要但又繁絮、不充分的內(nèi)容,通過調(diào)用該 Windows® API,可以快速減少這樣的麻煩。

因此我認(rèn)為,.NET Framework 1.0 或 1.1 版類庫中存在任何 Windows 所沒有的功能限制都不足為怪。畢竟,32 位的Windows(不管何種版本)是一個(gè)成熟的操作系統(tǒng),為廣大客戶服務(wù)了十多年。相比之下,.NET Framework 卻是一個(gè)新事物。

隨著越來越多的開發(fā)人員將生產(chǎn)應(yīng)用程序轉(zhuǎn)到托管代碼,開發(fā)人員更頻繁地研究底層操作系統(tǒng)以圖找出一些關(guān)鍵功能顯得很自然 — 至少目前是如此。

值得慶幸的是,公共語言運(yùn)行庫 (CLR) 的 interop 功能(稱為平臺(tái)調(diào)用(P/Invoke))非常完善。在本專欄中,我將重點(diǎn)介紹如何實(shí)際使用 P/Invoke 來調(diào)用 Windows API 函數(shù)。當(dāng)指 CLR 的COM Interop 功能時(shí),P/Invoke 當(dāng)作名詞使用;當(dāng)指該功能的使用時(shí),則將其當(dāng)作動(dòng)詞使用。我并不打算直接介紹 COMInterop,因?yàn)樗?P/Invoke 具有更好的可訪問性,卻更加復(fù)雜,這有點(diǎn)自相矛盾,這使得將 COM Interop作為專欄主題來討論不太簡明扼要。

走進(jìn) P/Invoke

首先從考察一個(gè)簡單的 P/Invoke 示例開始。讓我們看一看如何調(diào)用 Win32 MessageBeep 函數(shù),它的非托管聲明如以下代碼所示:

BOOL MessageBeep(UINT uType   // beep type);

為了調(diào)用 MessageBeep,您需要在 C# 中將以下代碼添加到一個(gè)類或結(jié)構(gòu)定義中:

[DllImport("User32.dll")]static extern Boolean MessageBeep(UInt32 beepType);

令人驚訝的是,只需要這段代碼就可以使托管代碼調(diào)用非托管的 MessageBeepAPI。它不是一個(gè)方法調(diào)用,而是一個(gè)外部方法定義。(另外,它接近于一個(gè)來自 C 而 C#允許的直接端口,因此以它為起點(diǎn)來介紹一些概念是有幫助的。)來自托管代碼的可能調(diào)用如下所示:

MessageBeep(0);

請(qǐng)注意,現(xiàn)在 MessageBeep 方法被聲明為 static。這是 P/Invoke 方法所要求的,因?yàn)樵谠揥indows API 中沒有一致的實(shí)例概念。接下來,還要注意該方法被標(biāo)記為 extern。這是提示編譯器該方法是通過一個(gè)從 DLL導(dǎo)出的函數(shù)實(shí)現(xiàn)的,因此不需要提供方法體。

說到缺少方法體,您是否注意到 MessageBeep聲明并沒有包含一個(gè)方法體?與大多數(shù)算法由中間語言 (IL) 指令組成的托管方法不同,P/Invoke 方法只是元數(shù)據(jù),實(shí)時(shí) (JIT)編譯器在運(yùn)行時(shí)通過它將托管代碼與非托管的 DLL 函數(shù)連接起來。執(zhí)行這種到非托管世界的連接所需的一個(gè)重要信息就是導(dǎo)出非托管方法的 DLL的名稱。這一信息是由 MessageBeep 方法聲明之前的 DllImport 自定義屬性提供的。在本例中,可以看到,MessageBeep非托管 API 是由 Windows 中的 User32.dll 導(dǎo)出的。

到現(xiàn)在為止,關(guān)于調(diào)用 MessageBeep 就剩兩個(gè)話題沒有介紹,請(qǐng)回顧一下,調(diào)用的代碼與以下所示代碼片段非常相似:

[DllImport("User32.dll")]static extern Boolean MessageBeep(UInt32 beepType);

最后這兩個(gè)話題是與數(shù)據(jù)封送處理 (data marshaling)和從托管代碼到非托管函數(shù)的實(shí)際方法調(diào)用有關(guān)的話題。調(diào)用非托管 MessageBeep 函數(shù)可以由找到作用域內(nèi)的externMessageBeep聲明的任何托管代碼執(zhí)行。該調(diào)用類似于任何其他對(duì)靜態(tài)方法的調(diào)用。它與其他任何托管方法調(diào)用的共同之處在于帶來了數(shù)據(jù)封送處理的需要。

C#的規(guī)則之一是它的調(diào)用語法只能訪問 CLR 數(shù)據(jù)類型,例如 System.UInt32 和 System.Boolean。C# 顯然不識(shí)別Windows API 中使用的基于 C 的數(shù)據(jù)類型(例如 UINT 和 BOOL),這些類型只是 C 語言類型的類型定義而已。所以當(dāng)Windows API 函數(shù) MessageBeep 按以下方式編寫時(shí)

BOOL MessageBeep( UINT uType )

外部方法就必須使用 CLR 類型來定義,如您在前面的代碼片段中所看到的。需要使用與基礎(chǔ) API 函數(shù)類型不同但與之兼容的 CLR 類型是 P/Invoke 較難使用的一個(gè)方面。因此,在本專欄的后面我將用完整的章節(jié)來介紹數(shù)據(jù)封送處理。

樣式

在 C# 中對(duì) Windows API 進(jìn)行 P/Invoke 調(diào)用是很簡單的。但如果類庫拒絕使您的應(yīng)用程序發(fā)出嘟聲,應(yīng)該想方設(shè)法調(diào)用 Windows 使它進(jìn)行這項(xiàng)工作,是嗎?

是的。但是與選擇的方法有關(guān),而且關(guān)系甚大!通常,如果類庫提供某種途徑來實(shí)現(xiàn)您的意圖,則最好使用 API 而不要直接調(diào)用非托管代碼,因?yàn)?CLR類型和 Win32 之間在樣式上有很大的不同。我可以將關(guān)于這個(gè)問題的建議歸結(jié)為一句話。當(dāng)您進(jìn)行 P/Invoke時(shí),不要使應(yīng)用程序邏輯直接屬于任何外部方法或其中的構(gòu)件。如果您遵循這個(gè)小規(guī)則,從長遠(yuǎn)看經(jīng)常會(huì)省去許多的麻煩。

圖 1 中的代碼顯示了我所討論的 MessageBeep 外部方法的最少附加代碼。圖 1中并沒有任何顯著的變化,而只是對(duì)無包裝的外部方法進(jìn)行一些普通的改進(jìn),這可以使工作更加輕松一些。從頂部開始,您會(huì)注意到一個(gè)名為 Sound的完整類型,它專用于 MessageBeep。如果我需要使用 Windows API 函數(shù) PlaySound來添加對(duì)播放波形的支持,則可以重用 Sound類型。然而,我不會(huì)因公開單個(gè)公共靜態(tài)方法的類型而生氣。畢竟這只是應(yīng)用程序代碼而已。還應(yīng)該注意到,Sound是密封的,并定義了一個(gè)空的私有構(gòu)造函數(shù)。這些只是一些細(xì)節(jié),目的是使用戶不會(huì)錯(cuò)誤地從 Sound 派生類或者創(chuàng)建它的實(shí)例。

圖 1中的代碼的下一個(gè)特征是,P/Invoke 出現(xiàn)位置的實(shí)際外部方法是 Sound 的私有方法。這個(gè)方法只是由公共 MessageBeep方法間接公開,后者接受 BeepTypes 類型的參數(shù)。這個(gè)間接的額外層是一個(gè)很關(guān)鍵的細(xì)節(jié),它提供了以下好處。首先,應(yīng)該在類庫中引入一個(gè)未來的beep 托管方法,可以重復(fù)地通過公共 MessageBeep 方法來使用托管 API,而不必更改應(yīng)用程序中的其余代碼。

該包裝方法的第二個(gè)好處是:當(dāng)您進(jìn)行 P/Invoke 調(diào)用時(shí),您放棄了免受訪問沖突和其他低級(jí)破壞的權(quán)利,這通常是由 CLR提供的。緩沖方法可以保護(hù)您的應(yīng)用程序的其余部分免受訪問沖突及類似問題的影響(即使它不做任何事而只是傳遞參數(shù))。該緩沖方法將由 P/Invoke調(diào)用引入的任何潛在的錯(cuò)誤本地化。

將私有外部方法隱藏在公共包裝后面的第三同時(shí)也是最后的一個(gè)好處是,提供了向該方法添加一些最小的 CLR 樣式的機(jī)會(huì)。例如,在圖 1中,我將 Windows API 函數(shù)返回的 Boolean 失敗轉(zhuǎn)換成更像 CLR 的異常。我還定義了一個(gè)名為 BeepTypes的枚舉類型,它的成員對(duì)應(yīng)于同該 Windows API 一起使用的定義值。由于 C#不支持定義,因此可以使用托管枚舉類型來避免幻數(shù)向整個(gè)應(yīng)用程序代碼擴(kuò)散。

包裝方法的最后一個(gè)好處對(duì)于簡單的 Windows API函數(shù)(如 MessageBeep)誠然是微不足道的。但是當(dāng)您開始調(diào)用更復(fù)雜的非托管函數(shù)時(shí),您會(huì)發(fā)現(xiàn),手動(dòng)將 Windows API樣式轉(zhuǎn)換成對(duì) CLR 更加友好的方法所帶來的好處會(huì)越來越多。越是打算在整個(gè)應(yīng)用程序中重用 interop功能,越是應(yīng)該認(rèn)真地考慮包裝的設(shè)計(jì)。同時(shí)我認(rèn)為,在非面向?qū)ο蟮撵o態(tài)包裝方法中使用對(duì) CLR 友好的參數(shù)也并非不可以。

DLL Import 屬性

現(xiàn)在是更深入地進(jìn)行探討的時(shí)候了。在對(duì)托管代碼進(jìn)行 P/Invoke 調(diào)用時(shí),DllImportAttribute類型扮演著重要的角色。DllImportAttribute 的主要作用是給 CLR 指示哪個(gè) DLL 導(dǎo)出您想要調(diào)用的函數(shù)。相關(guān) DLL的名稱被作為一個(gè)構(gòu)造函數(shù)參數(shù)傳遞給 DllImportAttribute。

如果您無法肯定哪個(gè) DLL 定義了您要使用的Windows API 函數(shù),Platform SDK 文檔將為您提供最好的幫助資源。在 Windows API函數(shù)主題文字臨近結(jié)尾的位置,SDK 文檔指定了 C 應(yīng)用程序要使用該函數(shù)必須鏈接的 .lib 文件。在幾乎所有的情況下,該 .lib文件具有與定義該函數(shù)的系統(tǒng) DLL 文件相同的名稱。例如,如果該函數(shù)需要 C 應(yīng)用程序鏈接到 Kernel32.lib,則該函數(shù)就定義在Kernel32.dll 中。您可以在 MessageBeep 中找到有關(guān) MessageBeep 的 Platform SDK 文檔主題。在該主題結(jié)尾處,您會(huì)注意到它指出庫文件是 User32.lib;這表明 MessageBeep 是從 User32.dll 中導(dǎo)出的。

可選的 DllImportAttribute 屬性

除了指出宿主 DLL 外,DllImportAttribute 還包含了一些可選屬性,其中四個(gè)特別有趣:EntryPoint、CharSet、SetLastError 和 CallingConvention。

EntryPoint在不希望外部托管方法具有與 DLL 導(dǎo)出相同的名稱的情況下,可以設(shè)置該屬性來指示導(dǎo)出的 DLL函數(shù)的入口點(diǎn)名稱。當(dāng)您定義兩個(gè)調(diào)用相同非托管函數(shù)的外部方法時(shí),這特別有用。另外,在 Windows 中還可以通過它們的序號(hào)值綁定到導(dǎo)出的DLL 函數(shù)。如果您需要這樣做,則諸如“#1”或“#129”的 EntryPoint 值指示 DLL 中非托管函數(shù)的序號(hào)值而不是函數(shù)名。

CharSet 對(duì)于字符集,并非所有版本的 Windows 都是同樣創(chuàng)建的。Windows 9x系列產(chǎn)品缺少重要的 Unicode 支持,而 Windows NT 和 Windows CE 系列則一開始就使用Unicode。在這些操作系統(tǒng)上運(yùn)行的 CLR 將Unicode 用于 String 和 Char 數(shù)據(jù)的內(nèi)部表示。但也不必?fù)?dān)心 — 當(dāng)調(diào)用Windows 9x API 函數(shù)時(shí),CLR 會(huì)自動(dòng)進(jìn)行必要的轉(zhuǎn)換,將其從 Unicode轉(zhuǎn)換為 ANSI。

如果DLL 函數(shù)不以任何方式處理文本,則可以忽略 DllImportAttribute 的 CharSet 屬性。然而,當(dāng) Char 或String 數(shù)據(jù)是等式的一部分時(shí),應(yīng)該將 CharSet 屬性設(shè)置為 CharSet.Auto。這樣可以使 CLR 根據(jù)宿主 OS使用適當(dāng)?shù)淖址?。如果沒有顯式地設(shè)置 CharSet 屬性,則其默認(rèn)值為 CharSet.Ansi。這個(gè)默認(rèn)值是有缺點(diǎn)的,因?yàn)閷?duì)于在Windows 2000、Windows XP 和 Windows NT® 上進(jìn)行的 interop調(diào)用,它會(huì)消極地影響文本參數(shù)封送處理的性能。

應(yīng)該顯式地選擇 CharSet.Ansi 或 CharSet.Unicode 的CharSet 值而不是使用 CharSet.Auto 的唯一情況是:您顯式地指定了一個(gè)導(dǎo)出函數(shù),而該函數(shù)特定于這兩種 Win32 OS中的某一種。ReadDirectoryChangesW API 函數(shù)就是這樣的一個(gè)例子,它只存在于基于 Windows NT的操作系統(tǒng)中,并且只支持 Unicode;在這種情況下,您應(yīng)該顯式地使用 CharSet.Unicode。

有時(shí),WindowsAPI 是否有字符集關(guān)系并不明顯。一種決不會(huì)有錯(cuò)的確認(rèn)方法是在 Platform SDK 中檢查該函數(shù)的 C語言頭文件。(如果您無法肯定要看哪個(gè)頭文件,則可以查看 Platform SDK 文檔中列出的每個(gè) API 函數(shù)的頭文件。)如果您發(fā)現(xiàn)該API 函數(shù)確實(shí)定義為一個(gè)映射到以 A 或 W 結(jié)尾的函數(shù)名的宏,則字符集與您嘗試調(diào)用的函數(shù)有關(guān)系。Windows API 函數(shù)的一個(gè)例子是在WinUser.h 中聲明的 GetMessage API,您也許會(huì)驚訝地發(fā)現(xiàn)它有 A 和 W 兩種版本。

SetLastError 錯(cuò)誤處理非常重要,但在編程時(shí)經(jīng)常被遺忘。當(dāng)您進(jìn)行 P/Invoke 調(diào)用時(shí),也會(huì)面臨其他的挑戰(zhàn) — 處理托管代碼中 Windows API 錯(cuò)誤處理和異常之間的區(qū)別。我可以給您一點(diǎn)建議。

如果您正在使用 P/Invoke 調(diào)用 Windows API 函數(shù),而對(duì)于該函數(shù),您使用 GetLastError來查找擴(kuò)展的錯(cuò)誤信息,則應(yīng)該在外部方法的 DllImportAttribute 中將 SetLastError 屬性設(shè)置為true。這適用于大多數(shù)外部方法。

這會(huì)導(dǎo)致 CLR 在每次調(diào)用外部方法之后緩存由 API函數(shù)設(shè)置的錯(cuò)誤。然后,在包裝方法中,可以通過調(diào)用類庫的 System.Runtime.InteropServices.Marshal類型中定義的 Marshal.GetLastWin32Error 方法來獲取緩存的錯(cuò)誤值。我的建議是檢查這些期望來自 API函數(shù)的錯(cuò)誤值,并為這些值引發(fā)一個(gè)可感知的異常。對(duì)于其他所有失敗情況(包括根本就沒意料到的失敗情況),則引發(fā)在System.ComponentModel 命名空間中定義的 Win32Exception,并將Marshal.GetLastWin32Error 返回的值傳遞給它。如果您回頭看一下圖 1 中的代碼,您會(huì)看到我在 extern MessageBeep 方法的公共包裝中就采用了這種方法。

CallingConvention我將在此介紹的最后也可能是最不重要的一個(gè) DllImportAttribute 屬性是 CallingConvention。通過此屬性,可以給CLR 指示應(yīng)該將哪種函數(shù)調(diào)用約定用于堆棧中的參數(shù)。CallingConvention.Winapi的默認(rèn)值是最好的選擇,它在大多數(shù)情況下都可行。然而,如果該調(diào)用不起作用,則可以檢查 Platform SDK 中的聲明頭文件,看看您調(diào)用的API 函數(shù)是否是一個(gè)不符合調(diào)用約定標(biāo)準(zhǔn)的異常 API。

通常,本機(jī)函數(shù)(例如 Windows API 函數(shù)或 C- 運(yùn)行時(shí)DLL 函數(shù))的調(diào)用約定描述了如何將參數(shù)推入線程堆棧或從線程堆棧中清除。大多數(shù) Windows API函數(shù)都是首先將函數(shù)的最后一個(gè)參數(shù)推入堆棧,然后由被調(diào)用的函數(shù)負(fù)責(zé)清理該堆棧。相反,許多 C-運(yùn)行時(shí) DLL函數(shù)都被定義為按照方法參數(shù)在方法簽名中出現(xiàn)的順序?qū)⑵渫迫攵褩?,將堆棧清理工作交給調(diào)用者。

幸運(yùn)的是,要讓 P/Invoke調(diào)用工作只需要讓外圍設(shè)備理解調(diào)用約定即可。通常,從默認(rèn)值 CallingConvention.Winapi 開始是最好的選擇。然后,在 C運(yùn)行時(shí) DLL 函數(shù)和少數(shù)函數(shù)中,可能需要將約定更改為 CallingConvention.Cdecl。

數(shù)據(jù)封送處理

數(shù)據(jù)封送處理是 P/Invoke 具有挑戰(zhàn)性的方面。當(dāng)在托管和非托管代碼之間傳遞數(shù)據(jù)時(shí),CLR遵循許多規(guī)則,很少有開發(fā)人員會(huì)經(jīng)常遇到它們直至可將這些規(guī)則記住。除非您是一名類庫開發(fā)人員,否則在通常情況下沒有必要掌握其細(xì)節(jié)。為了最有效地在CLR 上使用 P/Invoke,即使只偶爾需要 interop 的應(yīng)用程序開發(fā)人員仍然應(yīng)該理解數(shù)據(jù)封送處理的一些基礎(chǔ)知識(shí)。

在本月專欄的剩余部分中,我將討論簡單數(shù)字和字符串?dāng)?shù)據(jù)的數(shù)據(jù)封送處理。我將從最基本的數(shù)字?jǐn)?shù)據(jù)封送處理開始,然后介紹簡單的指針封送處理和字符串封送處理。

封送數(shù)字和邏輯標(biāo)量

Windows OS 大部分是用 C 編寫的。因此,Windows API 所用到的數(shù)據(jù)類型要么是 C 類型,要么是通過類型定義或宏定義重新標(biāo)記的 C 類型。讓我們看看沒有指針的數(shù)據(jù)封送處理。簡單起見,首先重點(diǎn)討論的是數(shù)字和布爾值。

當(dāng)通過值向 Windows API 函數(shù)傳遞參數(shù)時(shí),需要知道以下問題的答案:

數(shù)據(jù)從根本上講是整型的還是浮點(diǎn)型的?

如果數(shù)據(jù)是整型的,則它是有符號(hào)的還是無符號(hào)的?

如果數(shù)據(jù)是整型的,則它的位數(shù)是多少?

如果數(shù)據(jù)是浮點(diǎn)型的,則它是單精度的還是雙精度的?

有時(shí)答案很明顯,但有時(shí)卻不明顯。Windows API 以各種方式重新定義了基本的 C 數(shù)據(jù)類型。圖 2 列出了 C 和 Win32 的一些公共數(shù)據(jù)類型及其規(guī)范,以及一個(gè)具有匹配規(guī)范的公共語言運(yùn)行庫類型。

通常,只要您選擇一個(gè)其規(guī)范與該參數(shù)的 Win32 類型相匹配的 CLR 類型,您的代碼就能夠正常工作。不過也有一些特例。例如,在 WindowsAPI 中定義的 BOOL 類型是一個(gè)有符號(hào)的 32 位整型。然而,BOOL 用于指示 Boolean 值 true 或false。雖然您不用將 BOOL 參數(shù)作為 System.Int32 值封送,但是如果使用 System.Boolean類型,就會(huì)獲得更合適的映射。字符類型的映射類似于 BOOL,因?yàn)橛幸粋€(gè)特定的 CLR 類型 (System.Char) 指出字符的含義。

在了解這些信息之后,逐步介紹示例可能是有幫助的。依然采用 beep 主題作為例子,讓我們來試一下 Kernel32.dll 低級(jí) Beep,它會(huì)通過計(jì)算機(jī)的揚(yáng)聲器發(fā)生嘟聲。這個(gè)方法的 Platform SDK 文檔可以在 Beep 中找到。本機(jī) API 按以下方式進(jìn)行記錄:

BOOL Beep(DWORD dwFreq,      // FrequencyDWORD dwDuration   // Duration in milliseconds);

在參數(shù)封送處理方面,您的工作是了解什么 CLR 數(shù)據(jù)類型與 Beep API 函數(shù)所使用的 DWORD 和 BOOL 數(shù)據(jù)類型相兼容?;仡櫼幌?a target="_blank">圖 2中的圖表,您將看到 DWORD 是一個(gè) 32 位的無符號(hào)整數(shù)值,如同 CLR 類型 System.UInt32。這意味著您可以使用UInt32 值作為送往 Beep 的兩個(gè)參數(shù)。BOOL 返回值是一個(gè)非常有趣的情況,因?yàn)樵搱D表告訴我們,在 Win32 中,BOOL 是一個(gè)32 位的有符號(hào)整數(shù)。因此,您可以使用 System.Int32 值作為來自 Beep 的返回值。然而,CLR 也定義了System.Boolean 類型作為 Boolean 值的語義,所以應(yīng)該使用它來替代。CLR 默認(rèn)將 System.Boolean 值封送為32 位的有符號(hào)整數(shù)。此處所顯示的外部方法定義是用于 Beep 的結(jié)果 P/Invoke 方法:

[DllImport("Kernel32.dll", SetLastError=true)]static extern Boolean Beep(UInt32 frequency, UInt32 duration);

指針參數(shù)

許多 Windows API函數(shù)將指針作為它們的一個(gè)或多個(gè)參數(shù)。指針增加了封送數(shù)據(jù)的復(fù)雜性,因?yàn)樗鼈冊(cè)黾恿艘粋€(gè)間接層。如果沒有指針,您可以通過值在線程堆棧中傳遞數(shù)據(jù)。有了指針,則可以通過引用傳遞數(shù)據(jù),方法是將該數(shù)據(jù)的內(nèi)存地址推入線程堆棧中。然后,函數(shù)通過內(nèi)存地址間接訪問數(shù)據(jù)。使用托管代碼表示此附加間接層的方式有多種。

在 C# 中,如果將方法參數(shù)定義為 ref 或 out,則數(shù)據(jù)通過引用而不是通過值傳遞。即使您沒有使用 Interop也是這樣,但只是從一個(gè)托管方法調(diào)用到另一個(gè)托管方法。例如,如果通過 ref 傳遞 System.Int32參數(shù),則在線程堆棧中傳遞的是該數(shù)據(jù)的地址,而不是整數(shù)值本身。下面是一個(gè)定義為通過引用接收整數(shù)值的方法的示例:

void FlipInt32(ref Int32 num){num = -num;}

這里,F(xiàn)lipInt32 方法獲取一個(gè) Int32 值的地址、訪問數(shù)據(jù)、對(duì)它求反,然后將求反過的值賦給原始變量。在以下代碼中,F(xiàn)lipInt32 方法會(huì)將調(diào)用程序的變量 x 的值從 10 更改為 -10:

Int32 x = 10;FlipInt32(ref x);

在托管代碼中可以重用這種能力,將指針傳遞給非托管代碼。例如,F(xiàn)ileEncryptionStatus API 函數(shù)以 32 位無符號(hào)位掩碼的形式返回文件加密狀態(tài)。該 API 按以下所示方式進(jìn)行記錄:

BOOL FileEncryptionStatus(LPCTSTR lpFileName,  // file nameLPDWORD lpStatus     // encryption status);

請(qǐng)注意,該函數(shù)并不使用它的返回值返回狀態(tài),而是返回一個(gè) Boolean值,指示調(diào)用是否成功。在成功的情況下,實(shí)際的狀態(tài)值是通過第二個(gè)參數(shù)返回的。它的工作方式是調(diào)用程序向該函數(shù)傳遞指向一個(gè) DWORD變量的指針,而該 API 函數(shù)用狀態(tài)值填充指向的內(nèi)存位置。以下代碼片段顯示了一個(gè)調(diào)用非托管 FileEncryptionStatus函數(shù)的可能外部方法定義:

[DllImport("Advapi32.dll", CharSet=CharSet.Auto)]static extern Boolean FileEncryptionStatus(String filename,out UInt32 status);

該定義使用 out 關(guān)鍵字來為 UInt32 狀態(tài)值指示 by-ref 參數(shù)。這里我也可以選擇 ref關(guān)鍵字,實(shí)際上在運(yùn)行時(shí)會(huì)產(chǎn)生相同的機(jī)器碼。out 關(guān)鍵字只是一個(gè) by-ref 參數(shù)的規(guī)范,它向 C#編譯器指示所傳遞的數(shù)據(jù)只在被調(diào)用的函數(shù)外部傳遞。相反,如果使用 ref 關(guān)鍵字,則編譯器會(huì)假定數(shù)據(jù)可以在被調(diào)用的函數(shù)的內(nèi)部和外部傳遞。

托管代碼中 out 和 ref 參數(shù)的另一個(gè)很好的方面是,地址作為 by-ref參數(shù)傳遞的變量可以是線程堆棧中的一個(gè)本地變量、一個(gè)類或結(jié)構(gòu)的元素,也可以是具有合適數(shù)據(jù)類型的數(shù)組中的一個(gè)元素引用。調(diào)用程序的這種靈活性使得by-ref 參數(shù)成為封送緩沖區(qū)指針以及單數(shù)值指針的一個(gè)很好的起點(diǎn)。只有在我發(fā)現(xiàn) ref 或 out參數(shù)不符合我的需要的情況下,我才會(huì)考慮將指針封送為更復(fù)雜的 CLR 類型(例如類或數(shù)組對(duì)象)。

如果您不熟悉 C 語法或者調(diào)用Windows API 函數(shù),有時(shí)很難知道一個(gè)方法參數(shù)是否需要指針。一個(gè)常見的指示符是看參數(shù)類型是否是以字母 P 或 LP 開頭的,例如LPDWORD 或 PINT。在這兩個(gè)例子中,LP 和 P 指示參數(shù)是一個(gè)指針,而它們指向的數(shù)據(jù)類型分別為 DWORD 或INT。然而,在有些情況下,可以直接使用 C 語言語法中的星號(hào) (*) 將 API 函數(shù)定義為指針。以下代碼片段展示了這方面的示例:

void TakesAPointer(DWORD* pNum);

可以看到,上述函數(shù)的唯一一個(gè)參數(shù)是指向 DWORD 變量的指針。

當(dāng)通過 P/Invoke 封送指針時(shí),ref和 out 只用于托管代碼中的值類型。當(dāng)一個(gè)參數(shù)的 CLR 類型使用 struct 關(guān)鍵字定義時(shí),可以認(rèn)為該參數(shù)是一個(gè)值類型。Out 和ref用于封送指向這些數(shù)據(jù)類型的指針,因?yàn)橥ǔV殿愋妥兞渴菍?duì)象或數(shù)據(jù),而在托管代碼中并沒有對(duì)值類型的引用。相反,當(dāng)封送引用類型對(duì)象時(shí),并不需要ref 和 out 關(guān)鍵字,因?yàn)樽兞恳呀?jīng)是對(duì)象的引用了。

如果您對(duì)引用類型和值類型之間的差別不是很熟悉,請(qǐng)查閱 2000 年 12 月 發(fā)行的 MSDN®Magazine,在 .NET 專欄的主題中可以找到更多信息。大多數(shù) CLR 類型都是引用類型;然而,除了 System.String 和System.Object,所有的基元類型(例如 System.Int32 和 System.Boolean)都是值類型。

封送不透明 (Opaque) 指針:一種特殊情況

有時(shí)在 Windows API 中,方法傳遞或返回的指針是不透明的,這意味著該指針值從技術(shù)角度講是一個(gè)指針,但代碼卻不直接使用它。相反,代碼將該指針返回給 Windows 以便隨后進(jìn)行重用。

一個(gè)非常常見的例子就是句柄的概念。在 Windows 中,內(nèi)部數(shù)據(jù)結(jié)構(gòu)(從文件到屏幕上的按鈕)在應(yīng)用程序代碼中都表示為句柄。句柄其實(shí)就是不透明的指針或有著指針寬度的數(shù)值,應(yīng)用程序用它來表示內(nèi)部的 OS 構(gòu)造。

少數(shù)情況下,API 函數(shù)也將不透明指針定義為 PVOID 或 LPVOID 類型。在 Windows API 的定義中,這些類型意思就是說該指針沒有類型。

當(dāng)一個(gè)不透明指針返回給您的應(yīng)用程序(或者您的應(yīng)用程序期望得到一個(gè)不透明指針)時(shí),您應(yīng)該將參數(shù)或返回值封送為 CLR 中的一種特殊類型 —System.IntPtr。當(dāng)您使用 IntPtr 類型時(shí),通常不使用 out 或 ref 參數(shù),因?yàn)?IntPtr意為直接持有指針。不過,如果您將一個(gè)指針封送為一個(gè)指針,則對(duì) IntPtr 使用 by-ref 參數(shù)是合適的。

在 CLR類型系統(tǒng)中,System.IntPtr 類型有一個(gè)特殊的屬性。不像系統(tǒng)中的其他基類型,IntPtr并沒有固定的大小。相反,它在運(yùn)行時(shí)的大小是依底層操作系統(tǒng)的正常指針大小而定的。這意味著在 32 位的 Windows 中,IntPtr變量的寬度是 32 位的,而在 64 位的 Windows 中,實(shí)時(shí)編譯器編譯的代碼會(huì)將 IntPtr 值看作 64位的值。當(dāng)在托管代碼和非托管代碼之間封送不透明指針時(shí),這種自動(dòng)調(diào)節(jié)大小的特點(diǎn)十分有用。

請(qǐng)記住,任何返回或接受句柄的 API 函數(shù)其實(shí)操作的就是不透明指針。您的代碼應(yīng)該將 Windows 中的句柄封送成 System.IntPtr 值。

您可以在托管代碼中將 IntPtr 值強(qiáng)制轉(zhuǎn)換為 32 位或 64 位的整數(shù)值,或?qū)⒑笳邚?qiáng)制轉(zhuǎn)換為前者。然而,當(dāng)使用 Windows API函數(shù)時(shí),因?yàn)橹羔槕?yīng)是不透明的,所以除了存儲(chǔ)和傳遞給外部方法外,不能將它們另做它用。這種“只限存儲(chǔ)和傳遞”規(guī)則的兩個(gè)特例是當(dāng)您需要向外部方法傳遞null 指針值和需要比較 IntPtr 值與 null 值的情況。為了做到這一點(diǎn),您不能將零強(qiáng)制轉(zhuǎn)換為 System.IntPtr,而應(yīng)該在IntPtr 類型上使用 Int32.Zero 靜態(tài)公共字段,以便獲得用于比較或賦值的 null 值。

封送文本

在編程時(shí)經(jīng)常要對(duì)文本數(shù)據(jù)進(jìn)行處理。文本為 interop 制造了一些麻煩,這有兩個(gè)原因。首先,底層操作系統(tǒng)可能使用 Unicode來表示字符串,也可能使用 ANSI。在極少數(shù)情況下,例如 MultiByteToWideChar API 函數(shù)的兩個(gè)參數(shù)在字符集上是不一致的。

第二個(gè)原因是,當(dāng)需要進(jìn)行 P/Invoke 時(shí),要處理文本還需要特別了解到 C 和 CLR 處理文本的方式是不同的。在 C中,字符串實(shí)際上只是一個(gè)字符值數(shù)組,通常以 null 作為結(jié)束符。大多數(shù) Windows API 函數(shù)是按照以下條件處理字符串的:對(duì)于ANSI,將其作為字符值數(shù)組;對(duì)于 Unicode,將其作為寬字符值數(shù)組。

幸運(yùn)的是,CLR 被設(shè)計(jì)得相當(dāng)靈活,當(dāng)封送文本時(shí)問題得以輕松解決,而不用在意 Windows API 函數(shù)期望從您的應(yīng)用程序得到的是什么。這里是一些需要記住的主要考慮事項(xiàng):

是您的應(yīng)用程序向 API 函數(shù)傳遞文本數(shù)據(jù),還是 API 函數(shù)向您的應(yīng)用程序返回字符串?dāng)?shù)據(jù)?或者二者兼有?

您的外部方法應(yīng)該使用什么托管類型?

API 函數(shù)期望得到的是什么格式的非托管字符串?

我們首先解答最后一個(gè)問題。大多數(shù) Windows API 函數(shù)都帶有 LPTSTR 或 LPCTSTR值。(從函數(shù)角度看)它們分別是可修改和不可修改的緩沖區(qū),包含以 null結(jié)束的字符數(shù)組。“C”代表常數(shù),意味著使用該參數(shù)信息不會(huì)傳遞到函數(shù)外部。LPTSTR 中的“T”表明該參數(shù)可以是 Unicode 或ANSI,取決于您選擇的字符集和底層操作系統(tǒng)的字符集。因?yàn)樵?Windows API 中大多數(shù)字符串參數(shù)都是這兩種類型之一,所以只要在DllImportAttribute 中選擇 CharSet.Auto,CLR 就按默認(rèn)的方式工作。

然而,有些 API函數(shù)或自定義的 DLL 函數(shù)采用不同的方式表示字符串。如果您要用到一個(gè)這樣的函數(shù),就可以采用 MarshalAsAttribute修飾外部方法的字符串參數(shù),并指明一種不同于默認(rèn) LPTSTR 的字符串格式。有關(guān) MarshalAsAttribute 的更多信息,請(qǐng)參閱位于MarshalAsAttribute Class 的 Platform SDK 文檔主題。

現(xiàn)在讓我們看一下字符串信息在您的代碼和非托管函數(shù)之間傳遞的方向。有兩種方式可以知道處理字符串時(shí)信息的傳遞方向。第一個(gè)也是最可靠的一個(gè)方法就是首先理解參數(shù)的用途。例如,您正調(diào)用一個(gè)參數(shù),它的名稱類似 CreateMutex 并帶有一個(gè)字符串,則可以想像該字符串信息是從應(yīng)用程序向 API函數(shù)傳遞的。同時(shí),如果您調(diào)用 GetUserName,則該函數(shù)的名稱表明字符串信息是從該函數(shù)向您的應(yīng)用程序傳遞的。

除了這種比較合理的方法外,第二種查找信息傳遞方向的方式就是查找 API 參數(shù)類型中的字母“C”。例如,GetUserName API 函數(shù)的第一個(gè)參數(shù)被定義為LPTSTR 類型,它代表一個(gè)指向 Unicode 或 ANSI 字符串緩沖區(qū)的長指針。但是 CreateMutex 的名稱參數(shù)被類型化為LTCTSTR。請(qǐng)注意,這里的類型定義是一樣的,但增加一個(gè)字母“C”來表明緩沖區(qū)為常數(shù),API 函數(shù)不能寫入。

一旦明確了文本參數(shù)是只用作輸入還是用作輸入/輸出,就可以確定使用哪種 CLR 類型作為參數(shù)類型。這里有一些規(guī)則。如果字符串參數(shù)只用作輸入,則使用 System.String 類型。在托管代碼中,字符串是不變的,適合用于不會(huì)被本機(jī) API 函數(shù)更改的緩沖區(qū)。

如果字符串參數(shù)可以用作輸入和/或輸出,則使用 System.StringBuilder 類型。StringBuilder類型是一個(gè)很有用的類庫類型,它可以幫助您有效地構(gòu)建字符串,也正好可以將緩沖區(qū)傳遞給本機(jī)函數(shù),由本機(jī)函數(shù)為您填充字符串?dāng)?shù)據(jù)。一旦函數(shù)調(diào)用返回,您只需要調(diào)用 StringBuilder 對(duì)象的 ToString 就可以得到一個(gè) String 對(duì)象。

GetShortPathName API 函數(shù)能很好地用于顯示什么時(shí)候使用 String、什么時(shí)候使用 StringBuilder,因?yàn)樗粠в腥齻€(gè)參數(shù):一個(gè)輸入字符串、一個(gè)輸出字符串和一個(gè)指明輸出緩沖區(qū)的字符長度的參數(shù)。

圖 3 所示為加注釋的非托管 GetShortPathName 函數(shù)文檔,它同時(shí)指出了輸入和輸出字符串參數(shù)。它引出了托管的外部方法定義,也如圖 3 所示。請(qǐng)注意第一個(gè)參數(shù)被封送為 System.String,因?yàn)樗且粋€(gè)只用作輸入的參數(shù)。第二個(gè)參數(shù)代表一個(gè)輸出緩沖區(qū),它使用了 System.StringBuilder。

小結(jié)

本月專欄所介紹的 P/Invoke 功能足夠調(diào)用 Windows 中的許多 API 函數(shù)。然而,如果您大量用到interop,則會(huì)最終發(fā)現(xiàn)自己封送了很復(fù)雜的數(shù)據(jù)結(jié)構(gòu),甚至可能需要在托管代碼中通過指針直接訪問內(nèi)存。實(shí)際上,本機(jī)代碼中的 interop可以是一個(gè)將細(xì)節(jié)和低級(jí)比特藏在里面的真正的潘多拉盒子。CLR、C# 和托管 C++ 提供了許多有用的功能;也許以后我會(huì)在本專欄介紹高級(jí)的P/Invoke 話題。

同時(shí),只要您覺得 .NET Framework 類庫無法播放您的聲音或者為您執(zhí)行其他一些功能,您可以知道如何向原始而優(yōu)秀的 Windows API 尋求一些幫助。

將您要發(fā)給 Jason 的問題和意見發(fā)送到 dot-net@microsoft.com。

Jason Clark 為 Microsoft 和 Wintellect (http://www.wintellect.com) 提供培訓(xùn)和咨詢,以前是 Windows NT 和 Windows 2000 Server 團(tuán)隊(duì)的開發(fā)人員。他與人合著了 Programming Server-side Applications for Microsoft Windows 2000 (Microsoft Press, 2000)。您可以通過 JClark@Wintellect.com 與 Jason 取得聯(lián)系。

本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊舉報(bào)。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
請(qǐng)問C#高手: [DllImport("kernel32.dll")]是什么意思??
委托與事件
總結(jié)4:有關(guān)簡述的問題
計(jì)算機(jī)世界網(wǎng)-用Visual C#調(diào)用Windows API函數(shù)
Alex's Blog : 透析回調(diào)函數(shù)
同步函數(shù)與異步函數(shù)
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號(hào)成功
后續(xù)可登錄賬號(hào)暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服