In .NET development, it’s often necessary to interact with native code libraries written in C/C++. Platform Invoke (P/Invoke) is a powerful feature provided by .NET that allows managed code (like C#) to call unmanaged functions, such as those in a DLL.
This guide covers the two primary P/Invoke methods and demonstrates how to handle complex data structures.
Method 1: Traditional [DllImport]
[DllImport]
has been the classic way to perform P/Invoke since the .NET Framework era. It uses an attribute to declare a reference to a function within a DLL.
1
2
3
|
// Use the DllImport attribute to specify the DLL name and function entry point
[DllImport("ZrqHRV.dll", CallingConvention = CallingConvention.StdCall, EntryPoint = "func")]
public static extern int func(int devtype);
|
Method 2: Modern [LibraryImport]
(for .NET 7+)
Starting with .NET 7, the recommended approach is to use the [LibraryImport]
attribute. It leverages a source generator to create marshalling code at compile time, which offers better performance and an improved debugging experience compared to the runtime generation of [DllImport]
.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// The new, more performant method in .NET 7+
[LibraryImport("nativelib", EntryPoint = "to_lower", StringMarshalling = StringMarshalling.Utf16)]
internal static partial string ToLower(string str);
// Use with MarshalAs for finer-grained marshalling control
[LibraryImport("nativelib", EntryPoint = "to_lower")]
[return: MarshalAs(UnmanagedType.LPWStr)]
internal static partial string ToLowerWithMarshalAs([MarshalAs(UnmanagedType.LPWStr)] string str);
// Specify a 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);
|
Data Marshalling: Defining Structs
When you need to pass complex data, you typically define a C# struct
that matches the memory layout of the C/C++ struct.
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
|
// Use StructLayout to ensure memory layout compatibility with C/C++
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct ParamsIn
{
public int deviceId; // Device ID
public int packageSn; // Package serial number
// Use MarshalAs to define a fixed-size array
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 200)]
public int[] ecgList; // ECG waveform, 200 points
public int ecgFlag; // ECG lead alarm flag: 0 for normal, 1 for off
public int ecgFs; // ECG sample rate
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 25)]
public int[] chData;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 25)]
public int[] abData;
public int rspFlag;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 25)]
public int[] xList; // X-axis data
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 25)]
public int[] yList; // Y-axis data
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 25)]
public int[] zList; // Z-axis data
}
|
Practical Call: Passing Struct Pointers and Parsing Results
The following code demonstrates how to pass a managed struct
instance to an unmanaged function that expects a pointer.
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
34
35
36
|
// 1. Initialize the managed struct
ParamsIn paramsIn = new()
{
ecgList = new int[200],
deviceId = 1,
packageSn = 123,
ecgFlag = 0,
ecgFs = 200,
};
// 2. Allocate unmanaged memory for input and output parameters
IntPtr pntIn = Marshal.AllocHGlobal(Marshal.SizeOf(paramsIn));
IntPtr pntOut = Marshal.AllocHGlobal(Marshal.SizeOf<ParamsOut>());
try
{
// 3. Copy the managed struct data to the unmanaged memory block
Marshal.StructureToPtr(paramsIn, pntIn, false);
// 4. Call the unmanaged function, passing the pointers
// Assuming the function signature is: bool ProcessSignal(nint paramsIn, nint paramsOut)
bool success = NativeLibrary.ProcessSignal(pntIn, pntOut);
if (success)
{
// 5. Copy the result from unmanaged memory back to a new managed struct
ParamsOut? paramsOut = Marshal.PtrToStructure<ParamsOut>(pntOut);
// ... process the data in paramsOut here ...
}
}
finally
{
// 6. Always free the allocated unmanaged memory to prevent memory leaks
Marshal.FreeHGlobal(pntIn);
Marshal.FreeHGlobal(pntOut);
}
|
Important: Memory allocated with Marshal.AllocHGlobal
must be manually freed using Marshal.FreeHGlobal
. It is highly recommended to place the deallocation code in a finally
block to ensure it executes even if an exception occurs.