C与C#间复杂嵌套结构体的编组问题求助
问题分析与解决方案
核心问题
C结构体使用了二级指针(如struct DATATABLE **table),但C#直接用数组类型无法自动转换为对应的非托管指针结构,导致C代码访问时内存地址错误,触发System.AccessViolationException。此外还存在类型匹配、调用约定不匹配等问题:
- C#用
class对应C结构体时,Marshal行为与二级指针的内存布局不兼容 DATASEGMENT中的List<string> text与C的char** text类型不匹配- DllImport未指定调用约定,C默认
Cdecl与C#默认StdCall冲突
修正步骤
1. 调整C#结构体定义
将结构体改为值类型struct,并用IntPtr对应C的二级指针(手动处理内存布局):
[StructLayout(LayoutKind.Sequential)] public struct DATASET { public ulong n_tables; public ulong n_columns; public ulong n_segments; public ulong n_records; public IntPtr table; // 对应C的 struct DATATABLE** } [StructLayout(LayoutKind.Sequential)] public struct DATATABLE { public uint n_headers; public ulong n_columns; public ulong n_segments; public ulong n_records; public IntPtr segment; // 对应C的 struct DATASEGMENT** } [StructLayout(LayoutKind.Sequential)] public struct DATASEGMENT { public ulong n_rows; public ulong n_columns; public IntPtr data; // 对应C的 double** [MarshalAs(UnmanagedType.LPStr)] public string label; [MarshalAs(UnmanagedType.LPStr)] public string header; public IntPtr text; // 对应C的 char** }
2. 修正DllImport调用约定
添加CallingConvention = CallingConvention.Cdecl匹配C默认调用规则:
[DllImport("Name_of_dll.dll", CallingConvention = CallingConvention.Cdecl)] public static extern void print_dataset(ref DATASET D, int n_lines);
3. 手动构建非托管内存指针数组
C的二级指针需要指向一个存储结构体指针的数组,需手动分配内存并映射:
using System; using System.Runtime.InteropServices; class Program { // 结构体定义同上... [DllImport("Name_of_dll.dll", CallingConvention = CallingConvention.Cdecl)] public static extern void print_dataset(ref DATASET D, int n_lines); static void Main(string[] args) { // 1. 创建并填充DATATABLE实例 DATATABLE table1 = new DATATABLE { n_headers = 23, n_columns = 3, n_segments = 1, n_records = 9 }; // 为table1分配非托管内存 IntPtr table1Ptr = Marshal.AllocHGlobal(Marshal.SizeOf(table1)); Marshal.StructureToPtr(table1, table1Ptr, false); // 2. 创建DATATABLE指针数组(对应n_tables=1) int tableCount = 1; IntPtr tableArrayPtr = Marshal.AllocHGlobal(IntPtr.Size * tableCount); Marshal.WriteIntPtr(tableArrayPtr, 0, table1Ptr); // 3. 创建并填充DATASET实例 DATASET D = new DATASET { n_tables = (ulong)tableCount, n_columns = 3, n_segments = 1, n_records = 9, table = tableArrayPtr }; try { // 调用C函数 print_dataset(ref D, 2); } finally { // 必须手动释放非托管内存,避免泄漏 Marshal.FreeHGlobal(table1Ptr); Marshal.FreeHGlobal(tableArrayPtr); } } }
4. 扩展处理DATASEGMENT及内部字段(可选)
如果需要填充DATASEGMENT、double** data或char** text,逻辑与上述一致:
- 为每个
DATASEGMENT分配非托管内存,构建指针数组赋值给DATATABLE.segment - 对于
double** data:先分配每行的double数组内存,再创建存储行指针的数组 - 对于
char** text:将C#string[]转换为非托管char*数组,再存储指针数组地址
关键注意事项
- 内存管理:所有
Marshal.AllocHGlobal分配的内存必须手动释放,否则会造成内存泄漏 - 类型严格匹配:确保C与C#字段类型完全对应(如
uint对应unsigned int,ulong对应uint64_t) - 调用约定:必须在
DllImport中指定与C函数一致的调用约定
内容的提问来源于stack exchange,提问作者Lampen
相关产品推荐
相关产品推荐

