C#互操作:不使用unsafe能否封送含二维数组的C结构体?
无需
unsafe的C#互操作方案处理C DLL的二维结构数组 当然有办法在不使用unsafe关键字的情况下处理这个二维结构数组!我们可以利用C#的封送特性和一些辅助技巧来实现,下面分两种情况给出方案——完全不修改C API和仅做最小改动C API的情况,你可以根据需求选择。
方案一:完全不修改现有C API
这个方案不需要改动任何C代码,仅通过C#的封送机制和Marshal类来完成数据转换。
首先重新定义C#端的结构和导入函数,完全避免unsafe:
public struct MyPodStruct { public double a; public double b; } [StructLayout(LayoutKind.Sequential)] private struct My2dArray { // 用IntPtr替代指针的指针,绕过unsafe语法 public IntPtr arr; public int n; public int m; } [DllImport("Foo.DLL", EntryPoint = "Init2d", CallingConvention = CallingConvention.Cdecl)] private static extern void Init2d(ref My2dArray s, int n, int m); [DllImport("Foo.DLL", EntryPoint = "Free2d", CallingConvention = CallingConvention.Cdecl)] private static extern void Free2d(ref My2dArray s); [DllImport("Foo.DLL", EntryPoint = "Fill2dResult", CallingConvention = CallingConvention.Cdecl)] private static extern void Fill2dResult(ref My2dArray result);
接下来是核心的数据转换逻辑,通过Marshal.Copy来读取原生内存中的指针数组和结构数据:
public static MyPodStruct[,] GetFill2dResult(int n, int m) { My2dArray nativeArray = new My2dArray(); MyPodStruct[,] managedArray = new MyPodStruct[n, m]; // 初始化并填充原生二维数组 Init2d(ref nativeArray, n, m); Fill2dResult(ref nativeArray); // 第一步:读取所有行的指针(原生的MyPodStruct**转成C#的IntPtr数组) IntPtr[] rowPointers = new IntPtr[n]; Marshal.Copy(nativeArray.arr, rowPointers, 0, n); // 第二步:遍历每一行,把原生结构数据复制到托管数组 for (int i = 0; i < n; i++) { // 一次性复制整行的MyPodStruct到一维托管数组 MyPodStruct[] rowData = new MyPodStruct[m]; Marshal.Copy(rowPointers[i], rowData, 0, m); // 将一维行数据映射到二维数组的对应位置 for (int j = 0; j < m; j++) { managedArray[i, j] = rowData[j]; } } // 务必释放原生内存,避免泄漏 Free2d(ref nativeArray); return managedArray; }
方案原理
我们把原生的MyPodStruct**当作一个指向IntPtr数组的指针,先通过Marshal.Copy把这些行指针读取到C#的IntPtr[]中。之后再针对每个行指针,用Marshal.Copy直接把整行的结构数据复制到托管数组里,全程不需要直接操作指针,完全规避了unsafe语法。
方案二:仅做最小改动C API(更高效)
如果你可以对C API做极其微小的改动(新增一个辅助函数,不修改原有逻辑),这个方案的效率会更高,尤其是处理大数组时。
新增C辅助函数
在原有的C代码中新增一个负责复制数组的函数,逻辑非常简单:
#include <string.h> void Copy2dArray(const My2dArray* src, MyPodStruct* dst, int n, int m) { for (int i = 0; i < n; i++) { // 把每一行的结构数据直接复制到目标缓冲区 memcpy(dst + i*m, src->arr[i], sizeof(MyPodStruct)*m); } }
C#端实现
导入新增的函数,然后简化转换逻辑:
// 新增的函数导入 [DllImport("Foo.DLL", EntryPoint = "Copy2dArray", CallingConvention = CallingConvention.Cdecl)] private static extern void Copy2dArray(ref My2dArray src, [Out] MyPodStruct[] dst, int n, int m); public static MyPodStruct[,] GetFill2dResultOptimized(int n, int m) { My2dArray nativeArray = new My2dArray(); // 先创建一维托管数组作为复制缓冲区 MyPodStruct[] tempArray = new MyPodStruct[n * m]; MyPodStruct[,] managedArray = new MyPodStruct[n, m]; Init2d(ref nativeArray, n, m); Fill2dResult(ref nativeArray); // 一次性把整个二维数组复制到托管缓冲区 Copy2dArray(ref nativeArray, tempArray, n, m); // 将一维缓冲区的数据映射到二维数组 for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { managedArray[i, j] = tempArray[i * m + j]; } } Free2d(ref nativeArray); return managedArray; }
方案优势
这个方案只新增了一个无侵入性的辅助函数,原有API的逻辑完全不变。通过原生代码完成内存复制,减少了C#端的中间步骤,效率比方案一更高,同时仍然不需要使用unsafe。
总结
- 如果完全不能修改C API,方案一完全满足需求,全程无
unsafe,逻辑清晰易维护。 - 如果允许做最小改动,方案二是更优选择,性能更好,代码也更简洁。
内容的提问来源于stack exchange,提问作者Ziv
相关产品推荐
相关产品推荐

