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

C# 呼叫非托管 C++ DLL:正確封送結構體陣列

     編輯:關於C

前言

在 C# 中透過 P/Invoke 呼叫非托管的 C++ DLL 時,經常會遇到參數為指標或指標的指標的情況。通常我們使用 IntPtr 來指向需傳遞參數的位址。但當導出函數的參數是一個結構體陣列時,例如:

extern "C" __declspec(dllexport) int GetClass(Class pClass[50]);

該如何正確使用 IntPtr 進行封送呢?本文將透過兩個完整的範例程式,示範如何處理這類非托管函數呼叫。

建立 C++ DLL 與導出函數

首先建立一個 C++ DLL 專案,定義 Student 與 Class 結構,並實作 GetClass 導出函數:

#include <Windows.h>
#include <stdio.h>

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;
}

此函數要求呼叫端傳入一個包含 50 個 Class 結構的陣列,每個 Class 內含 126 個 Student 結構。

C# 端定義對應結構體

在 C# 中必須定義記憶體配置完全相符的結構體,並使用 StructLayout 與 MarshalAs 屬性精確控制欄位對齊:

[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;
}

關鍵點: 陣列大小 (SizeConst) 必須與 C++ 端定義完全一致,否則會導致記憶體讀寫錯誤。

常見錯誤嘗試

許多開發者直覺會寫成:

Class myClass = new Class();
IntPtr ptr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Class)));
GetClass(ptr); // 錯誤:只傳遞單一結構體,非陣列
Marshal.FreeHGlobal(ptr);

或是嘗試直接對托管陣列取址:

Class[] myClass = new Class[50];
IntPtr ptr = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Class)) * 50);
// 直接將托管陣列指標傳遞給非托管程式碼 — 錯誤!
// 因為 Class 內含 Student 陣列 (引用型別),無法直接封送。

這兩種做法都無法正確處理巢狀結構體陣列。

正確解法:手動記憶體拷貝

核心概念是:先配置一塊非托管記憶體,呼叫 API 由非托管端填充資料,再逐筆將非托管記憶體讀回托管結構體陣列。

static void Main(string[] args) {
    // 1. 計算總大小並配置非托管記憶體
    int size = Marshal.SizeOf(typeof(Class)) * 50;
    IntPtr pBuff = Marshal.AllocHGlobal(size);

    // 2. 準備托管陣列接收資料
    Class[] pClass = new Class[50];

    // 3. 呼叫非托管函數 (傳入非托管緩衝區指標)
    GetClass(pBuff);

    // 4. 逐筆從非托管記憶體讀取結構體
    for (int i = 0; i < 50; i++) {
        // 計算第 i 個 Class 結構在緩衝區中的位移
        IntPtr pElement = new IntPtr(pBuff.ToInt64() + Marshal.SizeOf(typeof(Class)) * i);
        // 將非托管記憶體封送為托管結構體
        pClass[i] = (Class)Marshal.PtrToStructure(pElement, typeof(Class));
    }

    // 5. 釋放非托管記憶體
    Marshal.FreeHGlobal(pBuff);

    Console.ReadLine();
}

P/Invoke 簽章宣告

別忘了在 C# 中正確宣告外部方法:

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

總結

處理非托管結構體陣列的關鍵在於:

  1. 精確定義對應的托管結構體,確保記憶體配置一致。
  2. 配置連續的非托管記憶體區塊給非托管函數填充。
  3. 使用 Marshal.PtrToStructure 逐筆將資料從非托管記憶體封送回托管陣列。
  4. 記得釋放 AllocHGlobal 配置的記憶體。

有興趣的讀者可以自行測試,觀察輸出結果是否正確。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved