在使用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>接口之间的互相转换,可以通过下面给出的FromIEnumerableCast成员函数实现。为了防止原始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);
}