P/Invoke之C#呼叫動態連結庫DLL

2023-03-29 12:00:55

本編所涉及到的工具以及框架:
1、Visual Studio 2022
2、.net 6.0

P/Invok是什麼?

P/Invoke全稱為Platform Invoke(平臺呼叫),其實際上就是一種函數呼叫機制,通過P/Invoke就可以實現呼叫非託管Dll中的函數。

在開始之前,我們首先需要了解C#中有關託管與非託管的區別

託管(Collocation),即在程式執行時會自動釋放記憶體;
非託管,即在程式執行時不會自動釋放記憶體。

廢話不多說,直接實操

第一步:

  1. 開啟VS2022,新建一個C#控制檯應用

  2. 右擊解決方案,新增一個新建項,新建一個"動態連結庫(DLL)",新建完之後需要右擊當前專案--> 屬性 --> C/C++ --> 預編譯頭 --> 選擇"不使用編譯頭"

  3. 在新建的DLL中我們新建一個標頭檔案,用於編寫我們的方法定義,然後再次新建一個C++檔案,字尾以.c 結尾

第二步:

  1. 在我們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);
    
  2. 之後需要去實現標頭檔案中的方法,在Native.c中實現,具體實現如下:

    #include "Native.h" // 匯入頭部檔案
    #include "stdio.h"
    
    HEAD int Add(int a, int b)
    {
        return a+b;
    }
    
  3. 在這些步驟做完後,可以嘗試生成解決方案,檢查是否報錯,沒有報錯之後,將進入專案檔案中,檢查是否生成DLL (../x64/Debug)

第三步:

  1. 在這裡之後,就可以在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#中的程式碼塊做一些改動,便於維護

  1. 在改動中我們將用到NativeLibrary類中的一個方法,用於設定回撥,解析從程式集進行的本機庫匯入,並實現通過設定DLL的相對路徑進行載入,其方法如下:

    public static void SetDllImportResolver (System.Reflection.Assembly assembly, System.Runtime.InteropServices.DllImportResolver resolver);
    
  2. 在使用這個方法前,先檢視一下其引數

    a、assembly: 主要是獲取包含當前正在執行的程式碼的程式集(不過多講解)

    b、resolber: 此引數是我們要注重實現的,我們可以通過檢視他的元程式碼,發現其實現的是一個委託,因此我們對其進行實現。

    原始方法如下:

    public delegate IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath);
    
  3. 實現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的路徑時,只需要在這個方法中對其相對路徑進行修改即可。

  4. 更新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();
        }
    }
    
  5. 最後重新編譯,檢查其是否能順利編譯通過,最終我們的到的結果為:68

至此,我們就完成了一個簡單的C#呼叫動態連結庫的案例

  下面將通過一個具體範例,講述為什麼要這樣做?(本範例通過從效能方面進行對比)

  1. 在DLL中的標頭檔案中,加入如下程式碼:

    HEAD void CBubbleSort(int* array, int length);
    
  2. 在.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;
                }
            }
        }
    }
    
  3. 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($"\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($"\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;
        }
    }
    
  4. 執行結果:

    C#實現排序所耗時: 130ms
    C實現排序所耗時:3ms
    

    在實現本案例中,可能在編譯後,大家所看到的結果不是很出乎意料,但這只是一種案例,希望通過此案例的分析,能給大家帶來一些意想不到的收穫叭。

最後

簡單做一下總結叭,通過上述所描述的從第一步如何建立一個DLL到如何通過C#去呼叫的一個簡單範例,也應該能給正在查閱相關資料的你有所收穫,也希望能給在這方面有所研究的你有一些相關的啟發,同時也希望能給目前對這方面毫無瞭解的你有一個更進一步的學習。

作者:百寶門-劉忠帥

原文地址:https://blog.baibaomen.com/p-invoke之c呼叫動態連結庫dll/