本編所涉及到的工具以及架構:
1、Visual Studio 2022
2、.net 6.0
P/Invok是什麼?
P/Invoke全稱為Platform Invoke(平台調用),其實際上就是一種函數調用機制,通過P/Invoke就可以實作調用非托管Dll中的函數。
在開始之前,我們首先需要了解C#中有關托管與非托管的差別
托管(Collocation),即在程式運作時會自動釋放記憶體;
非托管,即在程式運作時不會自動釋放記憶體。
廢話不多說,直接實操
第一步:
- 打開VS2022,建立一個C#控制台應用
- 右擊解決方案,添加一個建立項,建立一個”動态連結庫(DLL)”,建立完之後需要右擊目前項目–> 屬性 –> C/C++ –> 預編譯頭 –> 選擇”不使用編譯頭”
- 在建立的DLL中我們建立一個頭檔案,用于編寫我們的方法定義,然後再次建立一個C++檔案,字尾以.c 結尾
第二步:
- 在我們DLL中的頭檔案(Native.h)中定義相關的Test方法,具體代碼如下: #pragma once // 定義一些宏 #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN #endif #define CallingConvention _cdecl // 判斷使用者是否有輸入,進而定義區分使用dllimport還是dllexport #ifdef DLL_IMPORT #define HEAD EXTERN __declspec(dllimport) #else #define HEAD EXTERN __declspec(dllexport) #endif HEAD int CallingConvention Sum(int a, int b);
- 之後需要去實作頭檔案中的方法,在Native.c中實作,具體實作如下: #include "Native.h" // 導入頭部檔案 #include "stdio.h" HEAD int Add(int a, int b) { return a+b; }
- 在這些步驟做完後,可以嘗試生成解決方案,檢查是否報錯,沒有報錯之後,将進入項目檔案中,檢查是否生成DLL (../x64/Debug)
第三步:
- 在這裡之後,就可以在C#中去嘗試調用剛剛所聲明的方法,以便驗證是否調用DLL成功,其具體實作如下:
using System.Runtime.InteropServices;
class Program
{
[DllImport(@"C:\My_project\C#_Call_C\CSharp_P_Invoke_Dll\x64\Debug\NativeDll.dll")]
public static extern int Add(int a, int b);
public static void Main(string[] args)
{
int sum = Add(23, 45);
Console.WriteLine(sum);
Console.ReadKey();
}
}
運作結果為:68,證明我們成功調用了DLL動态鍊庫
C#中通過P/Invoke調用DLL動态鍊庫的流程
通過上述一個簡單的例子,我們大緻了解到了在C#中通過P/Invoke調用DLL動态鍊庫的流程,接下我們将對C#中的代碼塊做一些改動,便于維護
- 在改動中我們将用到NativeLibrary類中的一個方法,用于設定回調,解析從程式集進行的本機庫導入,并實作通過設定DLL的相對路徑進行加載,其方法如下: public static void SetDllImportResolver (System.Reflection.Assembly assembly, System.Runtime.InteropServices.DllImportResolver resolver);
-
在使用這個方法前,先檢視一下其參數 a、assembly: 主要是擷取包含目前正在執行的代碼的程式集(不過多講解)
b、resolber: 此參數是我們要注重實作的,我們可以通過檢視他的元代碼,發現其實作的是一個委托,是以我們對其進行實作。
原始方法如下: public delegate IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath);
- 實作resolver方法: const string NativeLib = "NativeDll.dll"; static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { string dll = Path.Combine(new DirectoryInfo(Environment.CurrentDirectory).Parent.Parent.Parent.Parent.ToString(), "x64","Release", "NativeDll.dll"); // 此處為Dll的路徑 //Console.WriteLine(dll); return libraryName switch { NativeLib => NativeLibrary.Load(dll, assembly, searchPath), _ => IntPtr.Zero }; } 該方法主要是用于區分在加載DLL時不一定隻能是設定絕對路徑,也可以使用相對路徑對其加載,本區域代碼是通過使用委托去實作加載相對路徑對其DLL加載,這樣做的好處是,便于以後需要更改DLL的路徑時,隻需要在這個方法中對其相對路徑進行修改即可。
- 更新C#中的代碼,其代碼如下: using System.Reflection; using System.Runtime.InteropServices; class Program { const string NativeLib = "NativeDll.dll"; [DllImport(NativeLib)] public static extern int Add(int a, int b); static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { string dll = Path.Combine(new DirectoryInfo(Environment.CurrentDirectory).Parent.Parent.Parent.Parent.ToString(), "x64","Release", "NativeDll.dll"); Console.WriteLine(dll); return libraryName switch { NativeLib => NativeLibrary.Load(dll, assembly, searchPath), _ => IntPtr.Zero }; } public static void Main(string[] args) { NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), DllImportResolver); int sum = Add(23, 45); Console.WriteLine(sum); Console.ReadKey(); } }
- 最後重新編譯,檢查其是否能順利編譯通過,最終我們的到的結果為:68
至此,我們就完成了一個簡單的C#調用動态連結庫的案例
下面将通過一個具體執行個體,講述為什麼要這樣做?(本執行個體通過從性能方面進行對比)
- 在DLL中的頭檔案中,加入如下代碼: HEAD void CBubbleSort(int* array, int length);
- 在.c檔案中加入如下代碼: HEAD void CBubbleSort(int* array, int length) { int temp = 0; for (int i = 0; i < length; i++) { for (int j = i + 1; j < length; j++) { if (array[i] > array[j]) { temp = array[i]; array[i] = array[j]; array[j] = temp; } } } }
-
C#中的代碼修改:
using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; class Program { const string NativeLib = "NativeDll.dll";[DllImport(NativeLib)] public unsafe static extern void CBubbleSort(int* arr, int length); static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { string dll = Path.Combine(new DirectoryInfo(Environment.CurrentDirectory).Parent.Parent.Parent.Parent.ToString(), "x64", "Release", "NativeDll.dll"); //Console.WriteLine(dll); return libraryName switch { NativeLib => NativeLibrary.Load(dll, assembly, searchPath), _ => IntPtr.Zero }; } public unsafe static void Main(string[] args) { int num = 1000; int[] arr = new int[num]; int[] cSharpResult = new int[num]; //随機生成num數量個(0-10000)的數字 Random random = new Random(); for (int i = 0; i < arr.Length; i++) { arr[i] = random.Next(10000); } //利用冒泡排序對其數組進行排序 Stopwatch sw = Stopwatch.StartNew(); Array.Copy(arr, cSharpResult, arr.Length); cSharpResult = BubbleSort(cSharpResult); Console.WriteLine(#34;\n C#實作排序所耗時:{sw.ElapsedMilliseconds}ms\n"); // 調用Dll中的冒泡排序算法 NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), DllImportResolver); fixed (int* ptr = &arr[0]) { sw.Restart(); CBubbleSort(ptr, arr.Length); } Console.WriteLine(#34;\n C實作排序所耗時:{sw.ElapsedMilliseconds}ms"); Console.ReadKey(); } //冒泡排序算法 public static int[] BubbleSort(int[] array) { int temp = 0; for (int i = 0; i < array.Length; i++) { for (int j = i + 1; j < array.Length; j++) { if (array[i] > array[j]) { temp = array[i]; array[i] = array[j]; array[j] = temp; } } } return array; }}
- 執行結果: C#實作排序所耗時: 130ms C實作排序所耗時:3ms 在實作本案例中,可能在編譯後,大家所看到的結果不是很出乎意料,但這隻是一種案例,希望通過此案例的分析,能給大家帶來一些意想不到的收獲叭。
最後
簡單做一下總結叭,通過上述所描述的從第一步如何建立一個DLL到如何通過C#去調用的一個簡單執行個體,也應該能給正在查閱相關資料的你有所收獲,也希望能給在這方面有所研究的你有一些相關的啟發,同時也希望能給目前對這方面毫無了解的你有一個更進一步的學習。