在使用P/Invoke(平台调用)从.NET(例如C#)调用C++本机函数时,可以选择将数组以指针的形式传递。但如果数组是结构体中的成员时,就只能通过SafeArray或者按值传递数组的方式传递了,无法用指针的形式传递。那么问题来了,如果要在P/Invoke中使用一个带有动态数组成员的结构体,又不想用SafeArray,该如何实现呢?
为了解决这个问题,可以设计专用于传递动态数组的结构。这个结构需要满足以下要求:
- 能作为其他对象的成员在.NET和本机函数之间传递
- 能从.NET的IEnumerable接口转换,或者转换到C#数组
- 正确实现.NET的IDisposable模式,使得:
- 内存空间能够通过Dispose函数被显式释放
- 在忘记调用Dispose的情况下由终结器释放内存
- 不会尝试在.NET端释放在本机端创建的对象
- 与C++标准库中的其他集合有相似的接口
- 正确实现C++的析构函数,使得:
- 析构函数调用时,通过C++端分配的内存空间能够正确释放,数组成员的析构函数能正确调用
一个能通过P/Invoke传递的基本数组结构C#代码如下所示:
[StructLayout(LayoutKind.Sequential)]
public class UnmanagedArray
{
private readonly IntPtr data;
private readonly int length;
}
要实现与.NET IEnumerable<T>接口之间的互相转换,可以通过下面给出的FromIEnumerable和Cast成员函数实现。为了防止原始IEnumerable<T>中的元素被垃圾回收导致意外情况(例如将IEnumerable<UnmanagedArray>转换为UnmanagedArray时,垃圾回收和终结器会将原始数据中分配的本机内存释放),在一个静态字典中存储该原始IEnumerable<T>。
/// <summary>
/// 用于保留转换为UnmanagedArray的原始对象,以防止其值被垃圾回收。
/// </summary>
private static Dictionary<IntPtr, object> originalVals = new Dictionary<IntPtr, object>();
/// <summary>
/// 该构造函数只用于将非托管对象转换为托管对象时由Marshal函数调用。
/// 使用该构造函数的实例不会自动释放内存空间。
/// </summary>
private UnmanagedArray()
{
data = IntPtr.Zero;
length = 0;
}
/// <summary>
/// 以预先分配的地址空间初始化<see cref="UnmanagedArray"/>类
/// </summary>
/// <param name="data">数组的第一个元素的指针</param>
/// <param name="length">数组的长度</param>
private UnmanagedArray(IntPtr data, int length)
{
this.data = data;
this.length = length;
}
public static UnmanagedArray FromIEnumerable<T>(IEnumerable<T> originalData)
{
int size;
if (typeof(T) == typeof(string))
{
size = Marshal.SizeOf<IntPtr>();
}
else
{
size = Marshal.SizeOf<T>();
}
int length = originalData.Count();
var data = Marshal.AllocHGlobal(size * length);
int i = 0;
if (typeof(T) == typeof(string))
{
foreach (T item in originalData)
{
Marshal.WriteIntPtr(data + (size * i), Marshal.StringToHGlobalUni((string)(object)item));
++i;
}
}
else
{
foreach (var item in originalData)
{
Marshal.StructureToPtr(item, data + (size * i), false);
++i;
}
}
originalVals[data] = originalData;
return new UnmanagedArray(data, length, typeof(T));
}
public IEnumerable<T> Cast<T>()
{
for (int i = 0; i < length; ++i)
{
yield return Marshal.PtrToStructure<T>(data + (i * Marshal.SizeOf<T>()));
}
}
要实现.NET的IDisposable模式,首先令UnmanagedArray类实现IDisposable接口,并增加Dispose函数和终结器。由于P/Invoke功能不支持泛型,在此通过一个静态的类型字典来存储每个实例的类型。同时,构造函数需要进行修改以防止终结器尝试释放C++分配的内存。完整的C#中的UnmanagedArray类如下所示:
[StructLayout(LayoutKind.Sequential)]
public class UnmanagedArray : IDisposable
{
private static Dictionary<IntPtr, Type> typeMap = new Dictionary<IntPtr, Type>();
/// <summary>
/// 用于保留转换为UnmanagedArray的原始对象,以防止其值被垃圾回收。
/// </summary>
private static Dictionary<IntPtr, object> originalVals = new Dictionary<IntPtr, object>();
private readonly IntPtr data;
private readonly int length;
/// <summary>
/// 该构造函数只用于将非托管对象转换为托管对象时由Marshal函数调用。
/// 使用该构造函数的实例不会自动释放内存空间。
/// </summary>
private UnmanagedArray()
{
data = IntPtr.Zero;
length = 0;
GC.SuppressFinalize(this);
}
/// <summary>
/// 以预先分配的地址空间初始化<see cref="UnmanagedArray"/>类
/// </summary>
/// <param name="data">数组的第一个元素的指针</param>
/// <param name="length">数组的长度</param>
/// <param name="type">数组元素的类型</param>
private UnmanagedArray(IntPtr data, int length, Type type)
{
this.data = data;
this.length = length;
typeMap[data] = type;
}
~UnmanagedArray()
{
Dispose(false);
}
public static UnmanagedArray FromIEnumerable<T>(IEnumerable<T> originalData)
{
int size;
if (typeof(T) == typeof(string))
{
size = Marshal.SizeOf<IntPtr>();
}
else
{
size = Marshal.SizeOf<T>();
}
int length = originalData.Count();
var data = Marshal.AllocHGlobal(size * length);
int i = 0;
if (typeof(T) == typeof(string))
{
foreach (T item in originalData)
{
Marshal.WriteIntPtr(data + (size * i), Marshal.StringToHGlobalUni((string)(object)item));
++i;
}
}
else
{
foreach (var item in originalData)
{
Marshal.StructureToPtr(item, data + (size * i), false);
++i;
}
}
originalVals[data] = originalData;
return new UnmanagedArray(data, length, typeof(T));
}
public IEnumerable<T> Cast<T>()
{
for (int i = 0; i < length; ++i)
{
yield return Marshal.PtrToStructure<T>(data + (i * Marshal.SizeOf<T>()));
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (typeMap.ContainsKey(data))
{
var type = typeMap[data];
typeMap.Remove(data);
originalVals.Remove(data);
if (type == typeof(string))
{
int size = Marshal.SizeOf<IntPtr>();
for (int i = 0; i < length; ++i)
{
Marshal.ZeroFreeGlobalAllocUnicode(
Marshal.ReadIntPtr(data + (size * i)));
}
}
else
{
int size = Marshal.SizeOf(type);
for (int i = 0; i < length; ++i)
{
Marshal.DestroyStructure(data + (size * i), type);
}
}
Marshal.FreeHGlobal(data);
}
}
}
在C++中对应的类代码如下。由于在使用P/Invoke时,.NET端的类对象会以指针的形式传递到C++代码中,所以C++侧的析构函数不会被调用,因此不必对析构函数做特殊处理。
template<typename T>
class Array
{
private:
T *m_data;
int m_length;
public:
Array()
: m_data(nullptr), m_length(0)
{}
Array(int _length)
: m_data(new T[_length]), m_length(_length)
{}
~Array()
{
delete[] m_data;
}
}
添加了复制构造函数、赋值操作符重载、迭代器接口和索引接口的C++类如下所示:
template<typename T>
class Array
{
private:
T *m_data;
int m_length;
public:
Array()
: m_data(nullptr), m_length(0)
{}
Array(int _length)
: m_data(new T[_length]), m_length(_length)
{}
~Array()
{
delete[] m_data;
}
Array(const Array<T> &array)
: m_data(new T[array.m_length]), m_length(array.m_length)
{
for (int i = 0; i < m_length; ++i)
{
m_data[i] = array.m_data[i];
}
}
Array<T> &operator=(const Array<T> &array)
{
if (this != &array)
{
delete[] m_data;
m_data = new T[array.m_length];
m_length = array.m_length;
for (int i = 0; i < m_length; ++i)
{
m_data[i] = array.m_data[i];
}
}
return *this;
}
Array<T> &operator=(Array<T> &&array)
{
if (this != &array)
{
delete[] m_data;
m_data = array.m_data;
m_length = array.m_length;
array.m_data = nullptr;
array.m_length = 0;
}
return *this;
}
const T &operator[](size_t index) const
{
return m_data[index];
}
T &operator[](size_t index)
{
return m_data[index];
}
T *data()
{
return m_data;
}
T *begin()
{
return m_data;
}
T *end()
{
return m_data + m_length;
}
const T *begin() const
{
return m_data;
}
const T *end() const
{
return m_data + m_length;
}
int length() const
{
return m_length
}
};
一个C++函数和对应的P/Invoke声明示例如下所示。由于.NET端不能正确释放C++端分配的内存,所以需要通过P/Invoke调用C++函数来释放空间。
class InputParam {
public:
Array<int> ints;
Array<int> doubles;
};
class OutputParam {
public:
Array<int> ints;
Array<double> doubles;
};
extern "C" __declspec(dllexport) void Foo(const InputParam &inputParam, const OutputParam *&outputParam);
extern "C" __declspec(dllexport) void FreeOutput(const OutputParam *paramToFree);
[StructLayout(LayoutKind.Sequential)]
public class InputParam {
UnmanagedArray ints;
UnmanagedArray doubles;
}
[StructLayout(LayoutKind.Sequential)]
public class OutputParam {
UnmanagedArray ints;
UnmanagedArray doubles;
}
public static extern void Foo(
InputParam inputParam,
[Out] out IntPtr outputParam);
public static extern void FreeOutput(
IntPtr outputParam);
public void ExampleCall()
{
InputParam a = ...;
Foo(a, out IntPtr bPtr);
var b = Marshal.PtrToStructure<OutputParam>(bPtr);
FreeOutput(bPtr);
}
留言
有想法?请给我们留言!您的留言不会直接显示在网站内。