C# 程序動態調用 C/C++ 動態庫函數


 一、C/C++ 動態庫函數封裝過程


添加 Visual C++ 的【動態鏈接庫】項目,於全局作用域(基本上就是隨便找個空白地方)定義導出函數。

導出函數的原型加上前綴 extern "C" __declspec(dllexport) ,方便起見可以定義一個宏:

#define DLL_EXPORT extern "C" __declspec(dllexport)

比如定義了如下一個函數:

DLL_EXPORT VOID ExchangeAddr(PHANDLE pp1, PHANDLE pp2)
{
	HANDLE p0 = *pp1;
	*pp1 = *pp2;
	*pp2 = p0;
	p0 = NULL;
}

其中 VIOD = void ,PHANDLE = void ** ,HANDLE = void * 。

這個函數顧名思義執行了地址交換的功能,傳入了兩個二級指針的地址,交換的是二級指針數據區的數據,也就是它們所指向的一級指針地址。

定義完成之后編譯這個項目,得到對應的 dll 文件。默認生成路徑應該是解決方案文件夾的 Debug 或 Release 文件夾下。

這個項目命名為 Rank2Pointer ,相應地生成動態庫文件名為 Rank2Pointer.dll 。

找到 .dll 文件之后復制到對應的 C# 項目工作目錄下即可,默認是項目文件夾的 bin / Debug or Release 文件夾下。


二、C# 程序調庫方法


1. 調庫方法總覽

目前本人所知的方法有三種:靜態加載,委托動態加載,反射動態加載。

靜態加載代碼量小,但過程不可控且不可卸載;

委托動態加載代碼量大,但過程可控,卸載方便;

反射動態加載代碼量適中,過程可控,托管式卸載。

個人推薦委托動態加載方式,下文將着重介紹此方法。


2. 鏈接准備——引入 Kernel32.dll

在類中聲明 Kernel32 的接口函數,實際上是相當於靜態地加載了這個程序集,利用了 Win32API 提供的方法。

可以像這樣:

using System.Runtime.InteropServices;
...
    public class CKernel32
    {
        [DllImport("kernel32.dll", EntryPoint = "LoadLibrary")]
        static extern public int LoadLibrary(
            [MarshalAs(UnmanagedType.LPStr)] string lpLibFileName);

        [DllImport("kernel32.dll", EntryPoint = "GetProcAddress")]
        static extern public IntPtr GetProcAddress(int hModule,
            [MarshalAs(UnmanagedType.LPStr)] string lpProcName);

        [DllImport("kernel32.dll", EntryPoint = "FreeLibrary")]
        static extern public bool FreeLibrary(int hModule);
    }

方便起見,自定義函數名就與 API 內函數名保持一致了。

LoadLibrary 加載指定名稱的程序集,返回的 int 值是程序集的句柄 hModule 。

將 hModule 和接口函數名字符串傳入 GetProcAddress ,就得到了程序集中指定名稱的接口函數指針。

FreeLibrary 通過相應的 hModule 卸載程序集。


3. 鏈接准備——定義委托類

實例化后的委托 delegate 對象相當於函數指針,為此需要先定義對應的委托類,對應於動態庫函數 ExchangeAddr :

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate int Delegate_ExchangeAddr(ref IntPtr pp1, ref IntPtr pp2);

特性 UnmanagedFunctionPointer 的必填項 CallingConvention 指定了函數調用方法,C/C++ 默認是cdecl ,而 C# 默認是 stdcall 。

對 C/C++ 的二級指針參數 void ** ,C# 需要使用指針類 IntPtr 加上 ref 關鍵字。


4. 鏈接過程——加載動態庫

加載目標動態庫利用的是 Kernel32.dll 中的方法 LoadLibrary ,我們已經定義了 CKernel32 類即可直接調用:

    int hModule = CKernel32.LoadLibrary("Rank2Pointer.dll");
    if (hModule == 0)
    {
        // error handle
        ...
    }

傳入的參數是目標加載程序集的路徑,確保已經把動態庫文件拷貝到正確的位置。

系統將為程序集分配一個句柄值,如果為零則代表加載失敗,可能沒有找到,或者文件格式不正確。


5. 鏈接過程——得到接口函數指針

成功加載 Rank2Pointer.dll 之后,就可以拿着這個 hModule 來找接口函數了。利用 Kernel32.dll 方法 GetProcAddress :

    IntPtr intPtr = CKernel32.GetProcAddress(hModule, "ExchangeAddr");
    if (intPtr == IntPtr.Zero)
    {
        // error handle
        ...
    }

參數 1 是 hModule ,參數 2 填入需要導出的函數名稱。如果返回了零則代表沒能找到目標函數。


6. 鏈接過程——關聯委托對象

拿到 intPtr 這個函數指針,由 Marshal.GetDelegateForFunctionPointer 鏈接 C# 與 C++ 的函數:

    var ExchangeAddr = Marshal.GetDelegateForFunctionPointer(intPtr,
        typeof(Delegate_ExchangeAddr)) as Delegate_ExchangeAddr;
    if (ExchangeAddr == null)
    {
        // error handle
        ...
    }

是非常容易出問題的一個環節,因為要求 C# 委托的形式與 C/C++ 函數原型高度匹配。

如果不匹配則返回空指針,這時候就需要修改委托類的定義。


7. 鏈接完畢——使用委托對象

GetDelegateForFunctionPointer 方法成功之后,就可以使用委托對象了,就像調用函數一樣地使用:

    var ptr1 = new IntPtr();
    var ptr2 = new IntPtr();
    ptr1 = Marshal.AllocHGlobal(1);
    ptr2 = Marshal.AllocHGlobal(1);
    Marshal.WriteByte(ptr1, 254);
    Marshal.WriteByte(ptr2, 1);
    Console.WriteLine("ptr1: " + Marshal.ReadByte(ptr1).ToString() +
                      ", ptr2: " + Marshal.ReadByte(ptr2).ToString());
    Console.WriteLine("Execute exchangeAddr function");
    ExchangeAddr(ref ptr1, ref ptr2);
    Console.WriteLine("ptr1: " + Marshal.ReadByte(ptr1).ToString() +
                      ", ptr2: " + Marshal.ReadByte(ptr2).ToString());
    Marshal.FreeHGlobal(ptr1);
    Marshal.FreeHGlobal(ptr2);

輸出結果:

ptr1: 254, ptr2: 1
Execute exchangeAddr function
ptr1: 1, ptr2: 254


8. 鏈接完畢——卸載程序集

如果有需要的話(一般來說不用),可用 Kernel32.dll 的 FreeLibrary 方法卸載程序集:

    if (CKernel32.FreeLibrary(hModule) == false)
    {
        // error handle
        ...
    }

參數傳遞的是程序集句柄 hModule ,在內存相對緊張的情況下考慮卸載。


三、互操作數據類型


1. 基本數據類型

Unmanaged type in Windows APIs Unmanaged C language type Managed type Description
VOID void System.Void Applied to a function that does not return a value.
HANDLE void * System.IntPtr or System.UIntPtr 32 bits on 32-bit Windows operating systems, 64 bits on 64-bit Windows operating systems.
BYTE unsigned char System.Byte 8 bits
SHORT short System.Int16 16 bits
WORD unsigned short System.UInt16 16 bits
INT int System.Int32 32 bits
UINT unsigned int System.UInt32 32 bits
LONG long System.Int32 32 bits
BOOL long System.Boolean or System.Int32 32 bits
DWORD unsigned long System.UInt32 32 bits
ULONG unsigned long System.UInt32 32 bits
CHAR char System.Char Decorate with ANSI.
WCHAR wchar_t System.Char Decorate with Unicode.
LPSTR char * System.String or System.Text.StringBuilder Decorate with ANSI.
LPCSTR const char * System.String or System.Text.StringBuilder Decorate with ANSI.
LPWSTR wchar_t * System.String or System.Text.StringBuilder Decorate with Unicode.
LPCWSTR const wchar_t * System.String or System.Text.StringBuilder Decorate with Unicode.
FLOAT float System.Single 32 bits
DOUBLE double System.Double 64 bits

有興趣可深入研究,鏈接在此 Marshalling Data with Platform Invoke 。

關注 LPSTR 、LPCSTR 、LPWSTR 、LPCWSTR ,這些經典 C 風格字符串可以簡單地通過參數傳遞,在 C# 程序中以 string 來對應即可。


2. 數組類型

在 C/C++ 中,數組名與指針同樣使用,但在 C# 程序中用 IntPtr 來操作定長數組卻並不可取。

也需要同樣地用數組(System.Array)來對應:

    [MarshalAs(UnmanagedType.ByValArray, SizeConst = ARR_SIZE)]
    public uint[] Arr;

注意到特性 MarshalAs ,必填項 UnmanagedType 指定為 ByValArray 的情況下,必須指定數組大小 SizeConst 。

值得一提的是 C/C++ 中的多維數組,在 C# 程序中仍然需要一維數組來對應:

// array defined in cpp:
// DWORD Arr[D_1_SIZE][D_2_SIZE][D_3_SIZE];
    [MarshalAs(UnmanagedType.ByValArray,
        SizeConst = D_1_SIZE * D_2_SIZE * D_3_SIZE)]
    public uint[] Arr;

更多數組操作范例 Marshalling Different Types of Arrays


3. 結構類型

傳遞結構類型的參數必須定義出對應類型的結構。

如果是結構體指針作為參數傳遞,直接加上 ref 關鍵字即可,麻煩的是結構體中的結構體指針:

// struct defined in cpp:
// typedef struct
// {
//     STRCPTRINSTRC *pStrc;
// }STRCPTRINSTRC;
    public struct StrcPtrInStrc
    {
        public IntPtr pStrc;
    }
    ...
    StrcPtrInStrc strc;
    IntPtr buffer = Marshal.AllocCoTaskMem(Marshal.SizeOf(strc));
    Marshal.StructureToPtr(strc, buffer, false);
    strc.pStrc = buffer;

更多結構類型操作范例 Marshalling Classes, Structures, and Unions 。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM