現在是更深入地進行探討的時候了。在對Managed 程式碼進行 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 調用時,也會面臨其他的挑戰 — 處理Managed 程式碼中 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。