一、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 。