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 。

posted @ 2022-03-25 11:33  永恒月华  阅读(1456)  评论(0编辑  收藏  举报