You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 18:09:17