P/Invoke以指针形式传递结构体内的数组

在使用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);
}

留言

有想法?请给我们留言!您的留言不会直接显示在网站内。