首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 與 C++ 互操作:正確封送結構體陣列的實戰技巧

     編輯:關於C語言

問題場景

呼叫非託管 C++ DLL 時,若匯出函式參數為結構體陣列(如 extern "C" __declspec(dllexport) int GetClass(Class pClass[50])),直接對應 C# 陣列參數往往會導致記憶體存取違規或資料錯誤。本文詳細說明正確的封送策略。

C++ 端定義(DLL 導出)

typedef struct Student {
    char name[20];
    int age;
    double scores[32];
} Student;

typedef struct Class {
    int number;
    Student students[126];
} Class;

extern "C" __declspec(dllexport) int GetClass(Class pClass[50]) {
    for (int i = 0; i < 50; i++) {
        pClass[i].number = i;
        for (int j = 0; j < 126; j++) {
            memset(pClass[i].students[j].name, 0, 20);
            sprintf(pClass[i].students[j].name, "name_%d_%d", i, j);
            pClass[i].students[j].age = (j % 2 == 0) ? 15 : 20;
        }
    }
    return 0;
}

C# 端對應結構體定義

關鍵在於使用 StructLayout(LayoutKind.Sequential) 並精確標註陣列大小:

[StructLayout(LayoutKind.Sequential)]
struct Student {
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 20)]
    public string name;
    public int age;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
    public double[] scores;
}

[StructLayout(LayoutKind.Sequential)]
struct Class {
    public int number;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 126)]
    public Student[] students;
}

錯誤嘗試與正確做法

❌ 錯誤 1:單一結構體指標

Class myClass = new Class();
IntPtr ptr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Class)));
GetClass(ptr); // 參數型別不符,且只傳遞單一結構體
Marshal.FreeHGlobal(ptr);

❌ 錯誤 2:直接封送托管陣列

Class[] myClass = new Class[50];
IntPtr ptr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Class)) * 50);
GetClass(ptr); // 托管陣列在記憶體中非連續,且包含嵌套結構體無法直接封送
Marshal.FreeHGlobal(ptr);

✅ 正確做法:手動配置非託管記憶體塊 + 逐項封送

[DllImport("CppDll.dll", CallingConvention = CallingConvention.Cdecl)]
static extern int GetClass(IntPtr pClass);

static void Main() {
    int structSize = Marshal.SizeOf(typeof(Class));
    int totalSize = structSize * 50;

    // 1. 配置非託管記憶體塊
    IntPtr pBuff = Marshal.AllocHGlobal(totalSize);
    Class[] pClass = new Class[50];

    // 2. 呼叫非託管函式填充記憶體
    GetClass(pBuff);

    // 3. 逐項將非託管記憶體封送回托管結構體
    for (int i = 0; i < 50; i++) {
        IntPtr pItem = new IntPtr(pBuff.ToInt64() + structSize * i);
        pClass[i] = (Class)Marshal.PtrToStructure(pItem, typeof(Class));
    }

    // 4. 釋放非託管記憶體
    Marshal.FreeHGlobal(pBuff);

    // 驗證結果
    foreach (var c in pClass) {
        Console.WriteLine($"Class {c.number}: Student0={c.students[0].name}, Age={c.students[0].age}");
    }
    Console.ReadLine();
}

核心原理解析

  • 記憶體佈局一致性:LayoutKind.Sequential 確保欄位順序與 C++ 一致;ByValArray / ByValTStr 將陣列/字串內嵌在結構體內,而非指標參考。
  • 非託管記憶體塊:AllocHGlobal 分配連續記憶體,模擬 C++ 陣列在記憶體中的連續分佈。
  • 指標運算定位:透過 pBuff + i * structSize 計算每個元素起始位址,再用 PtrToStructure 逐一封送。
  • 嵌套結構體處理:Student 陣列內嵌於 Class 中,PtrToStructure 會遞歸處理內層陣列,前提是大小與佈局完全匹配。

效能與安全性提示

  • 大量資料時,考慮分批處理或使用 unsafe 指標直接存取以減少封送開銷。
  • 務必在 finally 區塊或 using 模式中釋放 AllocHGlobal 記憶體,避免洩漏。
  • 若 C++ 端使用 #pragma pack 改變對齊,C# 需對應 [StructLayout(LayoutKind.Sequential, Pack = n)]。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved