C#中調用Dll動態鏈接庫
起始
受限於語言的不同,我們有的時候可能會用別人提供的函數及方法
或者其他的什么原因、反正就是要調!!!
恰巧別人所使用的的語言跟自己又不是一樣的
這個時候想要調用別人的函數庫就需要借用一些別的東西了
今天我們要說的是“UnmanagedExports”
當前我所要實現的目的只是為某一QQ機器人編寫插件
但我又不喜歡某中文編程語言,編程習慣導致 233333
在這里我們還可以使用進程間UDP通信來解決這個問題(編寫插件的問題)
但是這種方法局限性比較大,操作起來又略顯繁瑣
所以今天介紹一下“UnmanagedExports”這個nuget包
經過
打開nuget包管理器,為你所在的項目的安裝上這個包,這里就不在復述了
之后便可以以類似下面的寫法來調用Dll
首先需要聲明需要調用的函數及其對用的Dll
[DllImport("user32.dll")]//DllImportAttribute
public static extern int MsgBox(int hWnd, String text, String caption, uint type);
這里告訴編譯器我們需要調用的Dll名稱及其對應的方法定義
使用“extern”關鍵字來標識這個方法是從外部引用
關於“DllImportAttribute”的屬性會在下面講到
DllImportAttribute詳解
DllImportAttribute是一個重要的角色,其主要作用是給CLR指示哪個Dll是需要調用的外部庫。
| 字段 | 說明 |
|---|---|
| BestFitMapping | 啟用或禁用最佳匹配映射。 |
| CallingConvention | 指定用於傳遞方法參數的調用約定。 默認值為 WinAPI,該值對應於基於 32 位 Intel 的平台的 __stdcall。 |
| CharSet | 控制名稱重整以及將字符串參數封送到函數中的方式。 默認值為 CharSet.Ansi。 |
| EntryPoint | 指定要調用的 DLL 入口點。 |
| ExactSpelling | 控制是否應修改入口點以對應於字符集。 對於不同的編程語言,默認值將有所不同。 |
| PreserveSig | 控制托管方法簽名是否應轉換成返回 HRESULT 並且返回值有一個附加的 [out, retval] 參數的非托管簽名。默認值為 true(不應轉換簽名)。 |
| SetLastError | 允許調用方使用 Marshal.GetLastWin32Error API 函數來確定執行該方法時是否發生了錯誤。 在 Visual Basic 中,默認值為 true;在 C# 和 C++ 中,默認值為 false。 |
| ThrowOnUnmappableChar | 控件引發的異常,將無法映射的 Unicode 字符轉換成一個 ANSI"?"字符。 |
除了指出所調用的Dll外,DllImportAttribute還包含了一些可選屬性
其中有以下幾個比較常用:
entrypoint
入口點,用於標識函數在Dll中的位置。
你可以將入口點映射到一個不用的名稱,這實際上就是將被調用的函數重命名。
這里也說明以下重命名Dll函數的可能原因
- 避免使用區分大小寫的API函數名
- 符合現行的命名標准
- 提供采用不同數據類型的函數(通過聲明同一Dll函數的多個版本)
- 簡化對包含ANSI和Unicode版本的API的使用
[DllImport("dllname", EntryPoint="MyFunctionname")]
[DllImport("dllname", EntryPoint="#123")]
指定入口點名稱時,您可以提供一個字符串來指示包含入口點的 DLL 的名稱,或者也可以按序號來標識入口點。序號以 # 符號為前綴,如 #1。(序號看不太明白,不用先)
下面來演示一下如何使用Entrypoint字段將我們自己的函數MessageBoxA映射(替換)為Dll庫中的MsgBox
[DllImport("user32.dll", EntryPoint="MessageBoxA")]
public static extern int MsgBox(int hWnd, String text, String caption, uint type);
CharSet(部分摘自MSDN)
charset字段控制字符串封送處理並確定平台調用在dll查找函數名的方式。
對於采用字符串參數的函數,有些 API 將導出它們的兩個版本:窄版本 (ANSI) 和寬版本 (Unicode)。例如,Win32 API 包含 MessageBox 函數的以下入口點名稱:
- MessageBoxA
提供單字節字符 ANSI 格式,其特征是在入口點名稱后附加一個“A”。對 MessageBoxA 的調用始終會以 ANSI 格式封送字符串,它常見於 Windows 95 和 Windows 98 平台。
- MessageBoxW
提供雙字節字符 Unicode 格式,其特征是在入口點名稱后附加一個“W”。對 MessageBoxW 的調用始終會以 Unicode 格式封送字符串,它常見於 Windows NT、Windows 2000 和 Windows XP 平台。
CharSet 字段接受以下值:
CharSet.Ansi(默認值)
- 字符串封送處理
平台調用將字符串從托管格式 (Unicode) 封送為 ANSI 格式。
- 名稱匹配
在 ExactSpelling 字段為 true(它是 Visual Basic 2005 中的默認值)時,平台調用將只搜索您指定的名稱。例如,如果指定MessageBox,則平台調用將搜索 MessageBox,如果它找不到完全相同的拼寫則失敗。
當 ExactSpelling 字段為 false(它是 C++ 和 C# 中的默認值)時,平台調用將首先搜索未處理的別名 (MessageBox),如果找不到未處理的別名,則將搜索已處理的名稱 (MessageBoxA)。請注意,ANSI 名稱匹配行為與 Unicode 名稱匹配行為不同。
CharSet.Unicode
- 字符串封送處理
平台調用會將字符串從托管格式 (Unicode) 復制為 Unicode 格式。
- 名稱匹配
當 ExactSpelling 字段為 true(它是 Visual Basic 2005 中的默認值)時,平台調用將只搜索您指定的名稱。例如,如果指定MessageBox,則平台調用將搜索 MessageBox,如果它找不到完全相同的拼寫則失敗。
當 ExactSpelling 字段為 false(它是 C++ 和 C# 中的默認值)時,平台調用將首先搜索已處理的名稱 (MessageBoxW),如果找不到已處理的名稱,則將搜索未處理的別名 (MessageBox)。請注意,Unicode 名稱匹配行為與 ANSI 名稱匹配行為不同。
CharSet.Auto
- 平台調用在運行時根據目標平台在 ANSI 和 Unicode 格式之間進行選擇。( 針對目標操作系統適當地自動封送字符串。在 Windows NT、Windows 2000、Windows XP 和 Windows Server2003 系列上默認值為 System.Runtime.InteropServices.CharSet.Unicode;在 Windows 98
和 Windows Me 上默認值為 System.Runtime.InteropServices.CharSet.Ansi。)
下面的示例演示用於指定字符集的 MessageBox 函數的三個托管定義。在第一個定義中,通過省略,使 CharSet 字段默認為 ANSI 字符集。
[DllImport("user32.dll")]
public static extern int MessageBoxA(int hWnd, String text, String caption, uint type);
[DllImport("user32.dll", CharSet=CharSet.Unicode)]
public static extern int MessageBoxW(int hWnd, String text, String caption, uint type);
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern int MessageBox(int hWnd, String text, String caption, uint type);
CharSet.Ansi 和 CharSet.Unicode 的名稱匹配規則大不相同。對於 Ansi 來說,如果將 EntryPoint 設置為“MyMethod”且它存在的話,則返回“MyMethod”。如果 DLL 中沒有“MyMethod”,但存在“MyMethodA”,則返回“MyMethodA”。對於 Unicode 來說則正好相反。如果將 EntryPoint 設置為“MyMethod”且它存在的話,則返回“MyMethodW”。如果 DLL 中不存在“MyMethodW”,但存在“MyMethod”,則返回“MyMethod”。如果使用的是 Auto,則匹配規則與平台有關(在 Windows NT 上為 Unicode,在 Windows 98 上為 Ansi)。如果 ExactSpelling 設置為 true,則只有當 DLL 中存在“MyMethod”時才返回“MyMethod”。
如果 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 兩種版本。
以上內容采取直譯,大意就是那樣
這里我們一般不設置,即使用Auto即可。
各位Dalao有見解的話歡迎補充說明。
SetLastError(摘自MSDN)
SetLastError 錯誤處理非常重要,但我們在編程時經常會遺忘,或者直接偷懶而導致程序容錯性差。
對於該函數,我們可以使用 GetLastError 來查找擴展的錯誤信息,則應該在外部方法的 DllImportAttribute 中將 SetLastError 屬性設置為 true。
這會導致 CLR 在每次調用外部方法之后緩存由 API 函數設置的錯誤。
然后,在包裝方法中,可以通過調用類庫的 System.Runtime.InteropServices.Marshal 類型中定義的 Marshal.GetLastWin32Error方法來獲取緩存的錯誤值。
我的建議是檢查這些期望來自 API 函數的錯誤值,並為這些值引發一個可感知的異常。
對於其他所有失敗情況(包括根本就沒意料到的失敗情況),則引發在 System.ComponentModel 命名空間中定義的 Win32Exception,並將 Marshal.GetLastWin32Error返回的值傳遞給它。
CallingConvention
該字段的值有以下幾個:
-
CallingConvention.Cdecl : 調用方清理堆棧。它使您能夠調用具有 varargs 的函數(如printf)。
-
CallingConvention.StdCall : 被調用方清理堆棧。它是從托管代碼調用非托管函數的默認約定。
-
CallingConvention 字段的默認值為 Winapi,而后者又默認為 StdCall 約定。
這里不做詳解,用到的地方不多,大多是時候默認。
這里給一個例子
[DllExport("about", CallingConvention = CallingConvention.Cdecl)]
public static void about()
{
}
這里我們需要自己實現該函數
ExactSpelling
ExactSpelling 指示是否應修改非托管 DLL 中的入口點的名稱,以與 CharSet 字段中指定的 CharSet 值相對應。
如果為 true,則當 DllImportAttribute.CharSet 字段設置為 CharSet 的 Ansi 值時,向方法名稱中追加字母 A,當 DllImportAttribute.CharSet 字段設置為 CharSet 的 Unicode 值時,向方法的名稱中追加字母 W。
此字段的默認值是 false。
定義明確的情況下,不刻意使用該字段。
個人認為會把自己繞進去
結果
MarkDown真是太好使了!
