C# 调用 C/C++ DLL 的现代方法:从 DllImport 到 LibraryImport

本教程详细介绍了在 C# 中通过 P/Invoke 调用原生 C/C++ DLL 的方法,包括传统的 DllImport 和 .NET 7+ 的现代 LibraryImport,并演示了如何封送结构体。

在 .NET 开发中,我们经常需要与用 C/C++ 编写的本机(Native)代码库进行交互。平台调用(Platform Invoke,简称 P/Invoke)是 .NET 提供的一种强大机制,允许托管代码(如 C#)调用非托管函数(如 DLL 中的函数)。

本文将介绍两种主要的 P/Invoke 方法,并演示如何处理复杂的数据结构。

方法 1:传统 [DllImport]

[DllImport] 是自 .NET Framework 时代以来最经典的 P/Invoke 方式。它通过一个属性来声明对 DLL 中函数的引用。

1
2
3
// 使用 DllImport 属性指定 DLL 名称和函数入口点
[DllImport("ZrqHRV.dll", CallingConvention = CallingConvention.StdCall, EntryPoint = "func")]
public static extern int func(int devtype);

方法 2:现代 [LibraryImport] (适用于 .NET 7+)

从 .NET 7 开始,推荐使用 [LibraryImport] 属性。它利用源生成器(Source Generator)在编译时生成封送代码,相比 [DllImport] 的运行时生成,具有更高的性能和更好的调试体验。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// .NET 7+ 的新方法,性能更优
[LibraryImport("nativelib", EntryPoint = "to_lower", StringMarshalling = StringMarshalling.Utf16)]
internal static partial string ToLower(string str);

// 配合 MarshalAs 进行更精细的封送控制
[LibraryImport("nativelib", EntryPoint = "to_lower")]
[return: MarshalAs(UnmanagedType.LPWStr)]
internal static partial string ToLowerWithMarshalAs([MarshalAs(UnmanagedType.LPWStr)] string str);

// 指定调用约定 (Calling Convention)
[LibraryImport("nativelib", EntryPoint = "to_lower", StringMarshalling = StringMarshalling.Utf16)]
[UnmanagedCallConv(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvStdcall) })]
internal static partial string ToLowerWithCallConv(string str);

定义结构体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[StructLayout(LayoutKind.Sequential)]
public struct ParamsIn
{
    public int deviceId;                        //设备号
    public int packageSn;                       //包序号
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 200)]
    public int[] ecgList;           //心电波形,200个点
    public int ecgFlag;                     //心电导联告警标识 0:正常  1:脱落
    public int ecgFs = 200;                         //心电采样率:200
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 25)]
    public int[] chData;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 25)]
    public int[] abData;
    public int rspFlag;
    //public int hrThresholdHi;                   //心率高阈值120
    //public int hrThresholdLo;                   //心率低阈值:50
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 25)]
    public int[] xList;          //x轴数据
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 25)]
    public int[] yList;          //y轴数据
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 25)]
    public int[] zList;			//z轴数据

    public ParamsIn()
    {
    }
}

传递结构体并解析结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
ParamsIn paramsIn = new()
{
    ecgList = new int[200],
    deviceId = 1,
    packageSn = sn,
    ecgFlag = 0,
    ecgFs = 200,
    //hrThresholdLo = 120,
    //hrThresholdHi = 50
};
// 分配非托管内存
IntPtr pntIn = Marshal.AllocHGlobal(Marshal.SizeOf(paramsIn));
IntPtr pntOut = Marshal.AllocHGlobal(Marshal.SizeOf<ParamsOut>());
// 托管对象到非托管对象
Marshal.StructureToPtr(paramsIn, pntIn, false);
// 方法参数为指针类型(nint paramsIn, nint paramsOut)
bool b = NewProtocolLibrary.processSignal(pntIn, pntOut);
if (b)
{
    // 非托管对象到托管对象
    ParamsOut? paramsOut = (ParamsOut?)Marshal.PtrToStructure(pntOut, typeof(ParamsOut));
    if (paramsOut.HasValue)
    {
        heartRate = paramsOut.Value.hr;
        foreach (var item in paramsOut.Value.ecgfilterList)
        {
            channel.Writer.TryWrite((short)item);
        }
    }
}
//释放非托管内存
Marshal.FreeHGlobal(pntIn);
Marshal.FreeHGlobal(pntOut);
Built with Hugo
Theme Stack designed by Jimmy