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 。