在 C# 中透過 P/Invoke 呼叫非托管的 C++ DLL 時,經常會遇到參數為指標或指標的指標的情況。通常我們使用 IntPtr 來指向需傳遞參數的位址。但當導出函數的參數是一個結構體陣列時,例如:
extern "C" __declspec(dllexport) int GetClass(Class pClass[50]);
該如何正確使用 IntPtr 進行封送呢?本文將透過兩個完整的範例程式,示範如何處理這類非托管函數呼叫。
首先建立一個 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# 中必須定義記憶體配置完全相符的結構體,並使用 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();
}
別忘了在 C# 中正確宣告外部方法:
[DllImport("YourCppDll.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int GetClass(IntPtr pClass);
處理非托管結構體陣列的關鍵在於:
Marshal.PtrToStructure 逐筆將資料從非托管記憶體封送回托管陣列。AllocHGlobal 配置的記憶體。有興趣的讀者可以自行測試,觀察輸出結果是否正確。