内容简介:在使用P/Invoke(平台调用)从.NET(例如C#)调用C++本机函数时,可以选择将数组以指针的形式传递。但如果数组是结构体中的成员时,就只能通过SafeArray或者按值传递数组的方式传递了,无法用指针的形式传递。那么问题来了,如果要在P/Invoke中使用一个带有动态数组成员的结构体,又不想用SafeArray,该如何实现呢?为了解决这个问题,可以设计专用于传递动态数组的结构。这个结构需要满足以下要求:一个能通过P/Invoke传递的基本数组结构C#代码如下所示:
在使用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); }
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- C语言指针数组和数组指针
- 将数组和矩阵传递给函数,作为C中指针的指针和指针
- C 语言中的指针与数组
- 一个新细节,Go 1.17 将允许切片转换为数组指针
- 数据结构和算法面试题系列-C指针、数组和结构体
- NULL 指针、零指针、野指针
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。