C#与非托管DLL进行交互

在做Unity开发时,经常会需要调用 C/C++ 编译的非托管库,包括目前常见的 Lua 库,比如 ToLua、XLua 都是通过 C 封装完接口,然后编译成各个平台的动态库,才能在Unity中进行使用,P/Invoke 是.Net 平台提供的非托管与托管通信的一种服务,通过它能够非常方便的完成非托管库的接入,当然这个过程中有很多需要注意的地方,希望通过阅读本文,能够少走一些弯路。

PInvoke介绍

PInvoke是.Net Framework提供的一项服务,它使得托管代码与非托管之间的交互变得非常简单,一般来说,只需要声名一个方法并指定System.Runtime.InteropServicesDllImportAttribute属性,就可以调用非托管方法。先看一个最简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Runtime.InteropServices;

public class Program
{

// 导入user32.dll中的一个函数
[DllImport("user32.dll")]
public static extern int MessageBox(IntPtr hWnd, String text, String caption, int options);

public static void Main(string[] args)
{
// 调用导入的非托管函数
MessageBox(IntPtr.Zero, "Command-line message box", "Attention!", 0);
}
}

然后我们分析以下上面的例子:

  • 首先引入System.Runtime.InteropServices,涉及本机交互的API多数都在此命名空间。
  • 为方法指定DllImport 特性。 此特性至关重要,因为它告诉CLR要加载非托管 DLL,以及如何加载DLL。
  • 定义了一个托管方法 MessageBox ,该方法的签名与非托管方法相匹配。 可以看到,声明中包含一个新关键字 extern,告诉运行时这是一个外部方法。调用该方法时,CLR在 DllImport 指定的 DLL 内查找该方法。

CLR通过PInvoke服务调用非托管函数:

PInvoke调用非托管函数流程:

  1. 定位包含非托管函数的DLL,并将DLL加载到内存当中。
  2. 定位非托管方法的地址。
  3. 参数传递以及函数返回值。

只有第一次调用非托管方法时,会定位和加载DLL

定位DLL存放地址

在指定库名称的时候,我们比较关心CLI内部如何去查找该库,这个过程是通过LoadLibrary来实现,不同的平台有不同的加载策略,对于Windows平台来说按照以下顺序进行查找:

1
2
3
4
5
6
7
[DllImport("kernel32.dll", CharSet = CharSet.Auto, BestFitMapping = false, SetLastError = true)]
private static extern IntPtr LoadLibrary(String fileName);

[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool FreeLibrary(IntPtr hModule);
  1. 当前进程Exe文件所在目录。
  2. 当前目录,即Environment.CurrentDirectory
  3. Windows系统目录,一般是C/Windows/System32,可以通过GetSystemDirectory(kernel32.dll)或者Environment.GetFolderPath(Environment.SpecialFolder.System)函数获取此目录。
  4. Windows 16位系统目录,一般是C/Windows/System
  5. Windows目录,可以通过GetWindowsDirectory(kernel32.dll)或者Environment.GetEnvironmentVariable("windir")函数获取此目录。
  6. PATH系统变量列出的目录中。

知道要查找的目录之后,还需要知道动态库的名称,不同的平台对名称的处理策略不同。

  • Windows平台会对库名称附加.dll后缀。
  • Linux会使用lib前缀和.so后缀。
  • Mac OSX使用lib前缀和dylib后缀。

根据这些规则,在使用DllImport时,我们不应该指定前缀或者后缀,而是只是用Dll名称,例如:

1
2
[DllImport("MyLib", EntryPoint = "CreateEvent")]
public static extern IntPtr CreateEvent(IntPtr p, bool mreset, bool initstate, string name);

这样我们只需要为Windows平台提供MyLib.dll,Linux平台提供libMyLib.so,Mac OSX提供libMyLib.dylib文件,然后在不同的平台,CLI就能找到对应平台的库。

需要注意的是:如果提供的库名称中包含点号”.”,就不会附加后缀,例如”mylib-2.0.dll”如果指定”mylib-2.0”就不会附加后缀,从而引发DllNotFoundException。

定位非托管函数地址

在找到DLL之后,还需要找到函数在DLL的地址,该过程通过GetProcAddress来实现,该方法需要传递函数的名称,DllImport有两种方式可以获取函数名称,第一种通过EntryPoint指定;第二种是如果没有指定EntryPoint参数,则采用声名的托管函数名称。

1
2
3
4
//GetProcAddress包含在Kernel32.dll中,导入方式如下:  

[DllImport("kernel32.dll")]
private static extern IntPtr GetProcAddress(IntPtr hModule, String procName);

DllImportAttribute其他参数介绍

在使用DllImport导入非托管函数时,除了上面说介绍过的Dll名称,EntryPoint之外,还有一些比较重要的参数,其中包括CallingConventionCharSet

  • CallingConvention :指定了传递方法参数时的约定,必须和非托管保持一致,在Windows平台下该值默认是WinAPI,对应非托管是__stdcall
  • CharSet :改参数指定了向非托管封送方法名(入口名称)和字符串参数的编码格式,默认值是Ansi,即按照ASCII进行编码,CharSet.Unicode按照UTF-16进行编码。
  • BestFitMapping : 如果CharSet设置为CharSet.Ansi,封送处理器会采用近似匹配,将传递Unicode字符串映射到ASII编码,例如对于版权符号©会转换成字符’C’,对于没有映射的Unicode字符则转换成问号(?),默认为True。
  • ThrowOnUnmappableChar : 如果为True,当Unicode字符没有成功映射为ANSI字符时,会抛出异常,而非转换成问号“?”。

数据封送

托管内存和非托管内存是完全分开的,所以两者如果需要进行数据传输,就需要一个“信使”-互操作封送拆装器(Interop Mashaler)来传递,它负责将托管参数传递给非托管,并将函数返回值传递给托管,在调用一个含参的非托管方法时,封送的过程大致如下:

  1. CLR分配一块非托管内存。
  2. 将托管数据拷贝到刚分配的内存。
  3. 执行非托管方法,并将刚分配的非托管内存传递给函数。
  4. 将非托管内存拷贝回托管内存。

数据封送按照数据的流动方向可以分为两类:

  • 参数传递时:托管内存—>非托管内存
  • 函数返回值:非托管内存—>托管内存

无论数据如何传输,发送方的发送类型和接收方的接受类型,都必须具备相同的表示,即内存表示应该相同,例如:非托管函数形参为int类型,那么托管向该函数传递参数必须是32bit System.Int类型。

按照封送方式划分:

  • 按值封送
  • 按引用封送

Blittable类型

上面提到,封送的数据必须在发送方和接受方具备相同的表示,CLR大多数数据类型在托管和非托管内存中都有一个通用的表示,封送时不需要的特殊处理,这些类型被称为blittable类型,因为它们在托管和非托管代码之间传递时不需要转换。而与Blittable类型相对的是Non-Blittable类型,封送拆装器必须做一些特殊处理才能封送Non-Blittable类型。

P/Invoke的返回值必须是Blittable类型,不支持Non-Blittable类型的返回值。

Blittable类型包括以下几种:

  • byte
  • sbyte
  • short
  • ushort
  • int
  • uint
  • long
  • ulong
  • IntPtr
  • UIntPtr
  • float
  • double
  • 上述类型的一维数组
  • 只包含上述类型的结构体

内存复制与内存固定

封送数据时,互操作封送拆收器(interop marshaler)可以复制或固定正在封送的数据。复制将数据副本从非托管/托管移动到托管/非托管;锁定是保持对象内存在垃圾回收时不会重定位,封送拆收器通过固定减小复制的开销来提高性能。要封送数据的类型决定了是采用复制还是固定
下图显示从托管内存向非托管内存复制值类型和复制按引用传递的类型之间的差异。


如果函数参数时按值传递的,直接将数据内存拷贝到非托管栈上;如果函数参数是按引用传递的,则将引用对象的地址指针拷贝到非托管栈上。引用传递参数(ref标记)根据ref的类型选择按传值还是按引用传递。

封送字符串

什么时候回封送字符串:

  • 调用GetProcAddress时,封送方法名。
  • 调用非托管参数时,传递字符串类型参数。
  • 封送的结构体/类中包含字符串成员。

如何封送字符串? 有两种方式可以想非托管封送字符串:

  • System.String
  • System.Text.StringBuilder

两者的封送行为基本相同,封送拆装器会复制字符串,并将字符串转换为指定的编码格式,然后传输给非托管函数。两者非常重要区别在于string类型是不可变的,所以当调用结束时,不能将更改后的值复制回托管内存。
在封送时,可以使用MarshalAS()属性指定封送方式,见下表,其中StringBuilder只能允许LPStrLPWStrLPTStr

封送方式 内容
UnmanagedType.BStr 固定长度Unicode字符
UnmanagedType.AnsiBStr 固定长度的Ansi字符串
UnmanagedType.LPStr 指向以 null 终止的Ansi字符数组指针
UnmanagedType.LPWStr 指向以 null 终止的Unicode字符数组指针
UnmanagedType.LPTStr 指向以 null 终止的平台相关字符数组的指针
UnmanagedType.LPUTF8Str 指向UTF8编码的字符串的指针
UnmanagedType.ByValTStr 用于结构体内定长字符数组;数组的类型(Ansi、Unicode)由结构体的CharSet确定

非托管定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct StringInfoA
{
char * f1;
char f2[256];
};

struct StringInfoW
{
WCHAR * f1;
WCHAR f2[256];
BSTR f3;
};

struct StringInfoT
{
TCHAR * f1;
TCHAR f2[256];
};

托管Wrapper定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]  
struct StringInfoA
{
[MarshalAs(UnmanagedType.LPStr)] public String f1;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)] public String f2;
}

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
struct StringInfoW
{
[MarshalAs(UnmanagedType.LPWStr)] public String f1;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)] public String f2;
[MarshalAs(UnmanagedType.BStr)] public String f3;
}

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]
struct StringInfoT
{
[MarshalAs(UnmanagedType.LPTStr)] public String f1;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)] public String f2;
}

在某些情况下,必须将定长字符缓冲区传递到要操作的非托管代码中。在这种情况下,只传递字符串将不起作用,因为被调用方不能修改所传递缓冲区的内容。 即使通过引用传递字符串,仍无法将缓冲区初始化为给定大小。

解决方法是将 StringBuilder 作为参数而非字符串进行传递。被调用方可以修改 StringBuilder,前提是不超过 StringBuilder 的容量。还可以初始化为固定长度。例如,如果将 StringBuilder 缓冲区初始化为 N 容量,则封送处理程序提供大小为 (N+ 1) 个字符的缓冲区。 +1 是因为非托管字符串以 null 结尾,而 StringBuilder 却不不是,所以需要 +1 字节存放null终结符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//c函数声名:
char* strncpy (char *dest, const char *src, size_t n);

//托管函数声名:
[DllImport("libc.so")]
private static extern void strncpy(StringBuilder dest,
string src, uint n);

private static void UseStrncpy()
{
StringBuilder sb = new StringBuilder(256);
strncpy(sb, "this is the source string", sb.Capacity);
Console.WriteLine(sb.ToString());
}
如果需要多次调用StringBuilder,在调用之前,需要调用EnsureCapacity,因为由于内存优化会造成Capacity可能缩小,从而造成不可预期的结果。

封送System.Boolean

System.Boolean是比较特殊的一个类型,当它作为类或者结构体的成员时,默认按UnmanagedType.Bool(int,0-false,非0-true)封送;当它作为函数的参数时,默认按UnmanagedType.VariantBool(short,0-false,1-true)封送。可以通过MarshaalAs显示指定封送行为,来替换默认的封送行为,可选参数如下:

参数 封送行为
UnmanagedType.Bool int,0-false,非0-true
UnmanagedType.VariantBool short,0-false,1-true
UnmanagedType.U1 byte,0-false,1-true

封送类和结构体

类对象的内存是在托管堆上分配并由垃圾回收器进行管理,因此不能将类对象进行按值封送,只能按引用进行封送。此外在使用类封送时,还有以下几点需要注意:

  1. 类默认的字段排序是按照 LayoutKind.Auto,这是一种不确定的排序方式,需要指定LayoutKind.Sequential 或者 LayoutKind.Exlict.
  2. 对于类类型,默认只能从托管内存拷贝到非托管内存,如果需要从非托管内存拷贝到托管内存,需要对参数指定 [Out] 特性,如果是需要双向进行拷贝,需要同时指定 [In] [Out] 特性

与类不同,结构内存是分配在线程栈上,并且默认字段排序是LayoutKind.Sequential,因此结构体不用添加任何特性,就可以封送给非托管。另外如果结构体分配在线程栈上,并且结构成员只包含blittable类型,那么在封送时是固定的,可以直接封送给非托管,而不需要进行内存副本拷贝。一旦包含了Non-Blittable类型,该优化将不起作用。

P/Invoke函数参数声明准则

非托管的签名 托管声明:非间接-struct MyType 托管声明:一级间接-class MyType
DoWork(MyType x) 要求零级间接 DoWork(MyType x) 不可能实现,因为clas已经包含一级间接
DoWork(MyType* x) 要求一级间接 DoWork(ref MyType x) DoWork(MyType x)
DoWork(MyType** x) 要求二级间接 不可能,因为不能出现ref ref DoWork(ref MyType x)

类和结构体作为返回值

前面说过,P/Invoke的返回值只能是Blittable类型,所以类不能作为传值返回值,如果想要类作为返回值,非托管的返回值必须是指向结构体的指针。

结构体可以作为非托管的传值返回值,不能以传引用的方式返回结构体,如果需要返回结构体的引用,需要返回结构的指针(IntPtr),然后调用Marshal.PtrToStructure将IntPtr转换到结构体。

封送数组

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
37
38
39
40
41
42
43
44
45
#pragma once

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#ifdef PINVOKELIB_EXPORTS
#define PINVOKELIB_API __declspec(dllexport)
#else
#define PINVOKELIB_API __declspec(dllimport)
#endif

const int COL_DIM = 5;

typedef struct _MYPOINT
{
int x;
int y;
} MYPOINT;

typedef struct _MYPERSON
{
char* first;
char* last;
} MYPERSON;

#ifdef __cplusplus
extern "C"
{
#endif

PINVOKELIB_API int TestArrayOfInts(int* pArray, int size);

PINVOKELIB_API int TestRefArrayOfInts(int** ppArray, int* pSize);

PINVOKELIB_API int TestMatrixOfInts(int pMatrix[][COL_DIM], int row);

PINVOKELIB_API int TestArrayOfStrings(char* ppStrArray[], int size);

PINVOKELIB_API int TestArrayOfStructs(MYPOINT* pPointArray, int size);

PINVOKELIB_API int TestArrayOfStructs2(MYPERSON* pPersonArray, int size);

#ifdef __cplusplus
}
#endif

托管导出方法:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential)]
public struct MyPoint
{
public int x;
public int y;
public MyPoint(int x, int y)
{
this.x = x;
this.y = y;
}
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct MyPerson
{
public String first;
public String last;
public MyPerson(String first, String last)
{
this.first = first;
this.last = last;
}
}

public class LibWrap
{
// 按值封送int[],不能改变数组的大小,通过添加Out属性,非托管内存会复制到托管内存
[DllImport("..\\LIB\\PinvokeLib.dll")]
public static extern int TestArrayOfInts([In, Out] int[] array, int size);

// 按引用封送int[],非托管端可以改变数组的大小,但是改变后的内存不能返回给托管堆
// 因为拆装器不知道改变后的数组大小,托管端可以返回的size大小和数组指针,手动进行获取改变后的值
[DllImport("..\\LIB\\PinvokeLib.dll")]
public static extern int TestRefArrayOfInts(ref IntPtr array, ref int size);


// 按值封送二维数组
[DllImport("..\\LIB\\PinvokeLib.dll")]
public static extern int TestMatrixOfInts([In, Out] int[,] pMatrix, int row);


// 按值封送字符串数组
[DllImport("..\\LIB\\PinvokeLib.dll")]
public static extern int TestArrayOfStrings([In, Out] String[] stringArray, int size);

// 封送结构体数组
[DllImport("..\\LIB\\PinvokeLib.dll")]
public static extern int TestArrayOfStructs([In, Out] MyPoint[] pointArray, int size);


// 封送包含字符串的结构体数组
[DllImport("..\\LIB\\PinvokeLib.dll")]
public static extern int TestArrayOfStructs2([In, Out]MyPerson[] personArray, int size);
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;


class Program
{
static void Main(string[] args)
{
// 测试按值封送int[]
int[] array1 = new int[10];
Console.WriteLine("Integer array passed ByVal before call:");
for (int i = 0; i < array1.Length; i++)
{
array1[i] = i;
Console.Write(" " + array1[i]);
}
int sum1 = LibWrap.TestArrayOfInts(array1, array1.Length);
Console.WriteLine("\nSum of elements:" + sum1);
Console.WriteLine("\nInteger array passed ByVal after call:");

foreach (int i in array1)
{
Console.Write(" " + i);
}

// 测试按引用封送int[]
int[] array2 = new int[10];
int size = array2.Length;
Console.WriteLine("\n\nInteger array passed ByRef before call:");
for (int i = 0; i < array2.Length; i++)
{
array2[i] = i;
Console.Write(" " + array2[i]);
}
IntPtr buffer = Marshal.AllocCoTaskMem(Marshal.SizeOf(size)
* array2.Length);
Marshal.Copy(array2, 0, buffer, array2.Length);

int sum2 = LibWrap.TestRefArrayOfInts(ref buffer, ref size);
Console.WriteLine("\nSum of elements:" + sum2);
if (size > 0)
{
int[] arrayRes = new int[size];
Marshal.Copy(buffer, arrayRes, 0, size);
Marshal.FreeCoTaskMem(buffer);
Console.WriteLine("\nInteger array passed ByRef after call:");
foreach (int i in arrayRes)
{
Console.Write(" " + i);
}
}
else
Console.WriteLine("\nArray after call is empty");

// 多维数组封送
const int DIM = 5;
int[,] matrix = new int[DIM, DIM];

Console.WriteLine("\n\nMatrix before call:");
for (int i = 0; i < DIM; i++)
{
for (int j = 0; j < DIM; j++)
{
matrix[i, j] = j;
Console.Write(" " + matrix[i, j]);
}
Console.WriteLine("");
}
int sum3 = LibWrap.TestMatrixOfInts(matrix, DIM);
Console.WriteLine("\nSum of elements:" + sum3);
Console.WriteLine("\nMatrix after call:");
for (int i = 0; i < DIM; i++)
{
for (int j = 0; j < DIM; j++)
{
Console.Write(" " + matrix[i, j]);
}
Console.WriteLine("");
}

// 按值封送
String[] strArray = { "one", "two", "three", "four", "five" };
Console.WriteLine("\n\nString array before call:");
foreach (String s in strArray)
Console.Write(" " + s);
int lenSum = LibWrap.TestArrayOfStrings(strArray, strArray.Length);
Console.WriteLine("\nSum of string lengths:" + lenSum);
Console.WriteLine("\nString array after call:");
foreach (String s in strArray)
{
Console.Write(" " + s);
}

// 按值封送结构体数组
MyPoint[] points = { new MyPoint(1, 1), new MyPoint(2, 2), new MyPoint(3, 3) };
Console.WriteLine("\n\nPoints array before call:");
foreach (MyPoint p in points)
Console.WriteLine("x = {0}, y = {1}", p.x, p.y);
int allSum = LibWrap.TestArrayOfStructs(points, points.Length);
Console.WriteLine("\nSum of points:" + allSum);
Console.WriteLine("\nPoints array after call:");
foreach (MyPoint p in points)
Console.WriteLine("x = {0}, y = {1}", p.x, p.y);

// 按值封送结构体数组
MyPerson[] persons = { new MyPerson( "Kim", "Akers" ),
new MyPerson( "Adam", "Barr" ), new MyPerson( "Jo", "Brown" )};

Console.WriteLine("\n\nPersons array before call:");
foreach (MyPerson pe in persons)
Console.WriteLine("first = {0}, last = {1}", pe.first, pe.last);
int namesSum = LibWrap.TestArrayOfStructs2(persons, persons.Length);
Console.WriteLine("\nSum of name lengths:" + namesSum);
Console.WriteLine("\n\nPersons array after call:");
foreach (MyPerson pe in persons)
Console.WriteLine("first = {0}, last = {1}", pe.first, pe.last);
}
}

输出结果:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
Integer array passed ByVal before call:
0 1 2 3 4 5 6 7 8 9
Sum of elements:45

Integer array passed ByVal after call:
100 101 102 103 104 105 106 107 108 109 //可以看出,数组的值已经改变了,并且返回到了托管内存

Integer array passed ByRef before call:
0 1 2 3 4 5 6 7 8 9
Sum of elements:45

Integer array passed ByRef after call:
100 101 102 103 104 //数组的值和大小都改变了

Matrix before call:
0 1 2 3 4
0 1 2 3 4
0 1 2 3 4
0 1 2 3 4
0 1 2 3 4

Sum of elements:50

Matrix after call:
100 101 102 103 104
100 101 102 103 104
100 101 102 103 104
100 101 102 103 104
100 101 102 103 104


String array before call:
one two three four five
Sum of string lengths:19

String array after call:
123456789 123456789 123456789 123456789 123456789

Points array before call:
x = 1, y = 1
x = 2, y = 2
x = 3, y = 3

Sum of points:12

Points array after call:
x = 1, y = 0
x = 2, y = 0
x = 3, y = 0


Persons array before call:
first = Kim, last = Akers
first = Adam, last = Barr
first = Jo, last = Brown

Sum of name lengths:35


Persons array after call:
first = Kim, last = McAkers
first = Adam, last = McBarr
first = Jo, last = McBrown

封送委托

当非托管参数时函数指针或者回调指针时,托管端需要向非托管封送委托数据。如果传递过去的回调函数,是在非托管调用内完成调用,CLR会避免对委托进行回收。
但是如果非托管函数将委托保存起来,等调用结束之后才调用,那么就需要手动防止对委托的垃圾回收,直到调用结束之后才能够回收。


非托管定义:

1
2
3
4
5
typedef bool (CALLBACK *FPTR)(int i);
typedef bool (CALLBACK *FPTR2)(char* str);

void TestCallBack(FPTR pf, int value);
void TestCallBack2(FPTR2 pf2, char* value);

托管声名:

1
2
3
4
5
6
7
8
9
10
11
12
public delegate bool FPtr(int value);
public delegate bool FPtr2(string value);

public class LibWrap
{
// Declares managed prototypes for unmanaged functions.
[DllImport("..\\LIB\\PinvokeLib.dll")]
public static extern void TestCallBack(FPtr cb, int value);

[DllImport("..\\LIB\\PinvokeLib.dll")]
public static extern void TestCallBack2(FPtr2 cb2, String value);
}

输出结果:

1
2
3
4
5
6
7
8
9
Received value: 99
Passing to callback...
Callback called with param: 99
Callback returned true.

Received value: abc
Passing to callback...
Callback called with param: abc
Callback2 returned true.

非托管与GC

CLR的垃圾回收器只是负责托管堆上的垃圾回收,对于非托管内存垃圾回收并不能进行正确回收,这就可能引起意想不到的问题,考虑下面的代码:

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
class C
{
// 指向非托管物体的句柄
IntPtr _handle;

// 对h执行一些操作
[DllImport ("...")]
static extern void OperateOnHandle (IntPtr h);

// 释放h资源
[DllImport ("...")]
static extern void DeleteHandle (IntPtr h);

~C()
{
DeleteHandle(_handle);
}

public void m()
{
OperateOnHandle(_handle);
}
}

class Program
{
void Main()
{
C c = new C();
c.m();
}
}

当在Main中调用c.m()方法,方法调用完成之后,c不再存在引用,此时垃圾回收器可以对其进行回收,但是m()中执行了非托管方法,非托管仍然引用着c的成员对象,这是GC不可能知道的。如果正在执行OperateOnHandle非托管函数的时候,c对象被垃圾回收器回收(虽然不太可能),同时DeleteHandle被垃圾回收线程调用,这样就会引发进程错误,可能导致程序崩溃。
如何解决这个问题,可以采用HandleRef.aspx)类来替换IntPtr,来避免非托管函数完成前对托管对象进行垃圾回收。修改后的代码如下:

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
class C
{
// 指向非托管物体的句柄
HandleRef _handle;

// 对h执行一些操作
[DllImport ("...")]
static extern void OperateOnHandle (HandleRef h);

// 释放h资源
[DllImport ("...")]
static extern void DeleteHandle (HandleRef h);

[DllImport ("...")]
static extern IntPtr CreateHandle ();

public C()
{
IntPtr h = CreateHandle();

//可以看到,HandleRef同时引用实例对象和一个指针对象
_handle = new HandleRef(this,h);
}

~C()
{
DeleteHandle(_handle);
}

public void m()
{
OperateOnHandle(_handle);
}
}

释放非托管资源

GC只能回收托管堆的资源,不能对非托管资源进行回收,因此对于非托管资源,需要进行手动回收,我们可以通过释放模式(即IDisposable接口)进行非托管内存的回收,下面是一种利用SafeHandle实现的释放模式,也是比较推荐的一种模式。

CLR对SafeHandle做了一些特殊的处理,在进行封送的时候会自动处理SafeHandle,处理方式跟它出现的位置有关系:

  • 作为传出参数时,SafeHandle.handle会被封送。
  • 作为返回值时,会自动生成SafeHandle实例,并将返回的IntPtr赋值给SafeHandle.handle。
  • 使用ref SafeHandle时,传出的值会被忽略(必须设置为IntPtr.Zero),返回值会被赋值过来。
  • 作为结构体的成员函数,SafeHandle.handle会被封送。
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
using System;
using System.Runtime.InteropServices;


public static class WinAPI
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "CreateEvent")]
public static extern IntPtr CreateEvent(IntPtr p, bool mreset, bool initstate, string name);


[DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "CreateEvent")]
public static extern SafeHandle CreateEventSafe(IntPtr p, bool mreset, bool initstate, string name);

[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool CloseHandle(IntPtr hObject);
}

public class DisposeBase : IDisposable
{
//使用SafeHandle句柄控制非托管资源
private SafeHandle nativeRes;

public DisposeBase()
{
//示例:创建非托管资源
nativeRes = WinAPI.CreateEventSafe(IntPtr.Zero, false, false, null);
}

/// <summary>
/// 释放方法,需要使用者手动调用
/// </summary>
public void Dispose()
{
//释放资源
Dispose(false);

//阻止GC调用终结器
GC.SuppressFinalize(this);
}

//如果使用IntPtr句柄引用非托管资源,则必须要实现终结期,避免用户没有显式调用Dispose方法,造成内存泄漏
//由于本实例采用SafeHandle,SafeHandle内部实现了终结器,它会帮我们进行资源释放,所以这里就不需要终结器
/*
~DisposeBase()
{
Dispose(true);
}
*/

/// <summary>
/// 资源释放逻辑
/// </summary>
/// <param name="disposing">true表示用户手动调用,false表示由GC调用</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
//清理托管资源,或者其他实现了Dispose模式的非托管资源
nativeRes.Dispose();
}

//释放非托管资源
}
}

非托管数据类型与托管类型对应表

非托管类型与托管类型对应关系:

Wtypes.h 中的非托管类型 非托管 C 语言类型 托管类名称 描述
HANDLE *void System.IntPtr 在 32 位 Windows 操作系统上为 32 位、在 64 位 Windows 操作系统上为 64 位。
BYTE unsigned char System.Byte 8 位
short short System.Int16 16 位
WORD unsigned short System.UInt16 16 位
INT int System.Int32 32 位
UINT unsigned int System.UInt32 32 位
LONG long System.Int32 32 位
BOOL long Byte 32 位
DWORD unsigned long System.UInt32 32 位
ULONG unsigned long System.UInt32 32 位
CHAR char System.Char 使用 ANSI 修饰。
WCHAR wchar_t System.Char 使用 Unicode 修饰。
LPSTR char\ System.String 或 System.Text.StringBuilder 使用 ANSI 修饰。
LPCSTR Const char\ System.String 或 System.Text.StringBuilder 使用 ANSI 修饰。
LPWSTR wchar_t* System.String 或 System.Text.StringBuilder 使用 Unicode 修饰。
LPCWSTR Const wchar_t* System.String 或 System.Text.StringBuilder 使用 Unicode 修饰。
FLOAT Float System.Single 32 位
DOUBLE 双精度 System.Double 64 位
Tips: Win32Api的导入,可以在[pinvoke.net](https://www.pinvoke.net) 找到相应的实现,以及相应的使用示例。

参考文章:
Mono Doc
MSDN.aspx)