C#與C++動(dòng)態(tài)鏈接庫(kù)數(shù)據(jù)傳遞
1 內(nèi)存對(duì)齊規(guī)則
- 結(jié)構(gòu)體的數(shù)據(jù)成員,第一個(gè)成員的偏移量為0,后面每個(gè)成員變量的地址必須從其大小的整數(shù)倍開始。
- 子結(jié)構(gòu)體中的第一個(gè)成員偏移量應(yīng)當(dāng)是子結(jié)構(gòu)體中最大成員的整數(shù)倍。
- 結(jié)構(gòu)體的總大小必須是其內(nèi)部最大成員的整數(shù)倍
示例
#include <iostream>
using namespace std;
struct Frame {
unsigned char id; // 0-1
int width; // 4-8
long long height; // 8-16
unsigned char* data; // 16-24 (x64) 16-20 (x86) 指針x86下是4字節(jié)
int size; // 24-28
};
struct Info {
char name[10];//0-10
double value; //16-24
Frame fr; // 24-56
};
int main() {
Frame frame;
Info info;
cout << sizeof(frame) << endl;//輸出32(x64) 輸出24(x86)
cout << sizeof(info) << endl;//輸出56(x64) 輸出48(x86)
}
在 C++ 中,我們可以使用 #pragma pack(n) 來(lái)修改對(duì)齊方式,控制結(jié)構(gòu)體中成員的排列方式。
-
n可以是 1、2、4、8、16 等,表示最大對(duì)齊數(shù)為 n 字節(jié)。 -
#pragma pack(1)表示取消對(duì)齊,所有成員緊密排列,沒有插入任何 padding。
使用 #pragma pack 有利于節(jié)省內(nèi)存空間,但會(huì)降低訪問(wèn)效率,甚至引發(fā)硬件異常(部分平臺(tái)不支持非對(duì)齊訪問(wèn))。
#pragma pack(push, 1)
struct Frame {
unsigned char id; //0-1
int width;//1-5
long long height;//5-13
unsigned char* data;//13-21(x64)
int size;//21-25
};
#pragma pack(pop)
此時(shí)Frame的大小變成了25。
2 C#結(jié)構(gòu)體
示例
using System.Runtime.InteropServices;
class Program
{
struct Frame
{
byte id; // 0-1
int width; // 4-8
long height; // 8-16
int size; // 16-20
};
static void Main()
{
Frame fr = new Frame();
int len = Marshal.SizeOf(fr);//24
Console.WriteLine("Size of Frame: " + len);
}
}
在 C# 中,結(jié)構(gòu)體的內(nèi)存布局控制是通過(guò)特性(Attributes)來(lái)實(shí)現(xiàn)的,特別是 [StructLayout] 和 [FieldOffset]。
? 示例一:順序布局 Sequential
[StructLayout(LayoutKind.Sequential)]
struct Frame //結(jié)構(gòu)體大小24字節(jié)
{
byte id; // 0-1
int width; // 4-8
long height; // 8-16
int size; // 16-20
}
? 示例二:精確偏移 Explicit
[StructLayout(LayoutKind.Explicit)]
struct Frame //結(jié)構(gòu)體大小48字節(jié)
{
[FieldOffset(0)] byte id; //0-1
[FieldOffset(10)] int width; //10-14
[FieldOffset(15)] long height; //15-23
[FieldOffset(40)] int size; //40-48
}
? 示例三:模擬 #pragma pack(1)
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct Frame //結(jié)構(gòu)體大小17字節(jié)
{
byte id; //0-1
int width; //1-5
long height; //5-13
int size; //13-17
}
3 調(diào)用約定
__cdecl 是 C/C++ 的默認(rèn)調(diào)用約定,參數(shù)從右向左入棧,調(diào)用者負(fù)責(zé)清理?xiàng)!KС挚勺儏?shù)函數(shù)(如 printf),跨平臺(tái)兼容性好。在 C# 調(diào)用時(shí)應(yīng)指定 CallingConvention.Cdecl,適合自己寫的 DLL。
__stdcall 是 Windows API 常用的調(diào)用約定,參數(shù)也是從右向左入棧,但由被調(diào)用者清理?xiàng)?臻g。函數(shù)名導(dǎo)出時(shí)會(huì)被修飾(如 _Func@8),不支持可變參數(shù)。C# 默認(rèn)用它,常用于調(diào)用系統(tǒng) DLL 或標(biāo)準(zhǔn)第三方庫(kù)。
4 C#與C++之間類型的對(duì)應(yīng)關(guān)系
|
C# 類型
|
C++ 對(duì)應(yīng)類型
|
?? 差異說(shuō)明
|
|---|---|---|
byte |
unsigned char |
? 完全匹配,8 位無(wú)符號(hào)整數(shù)(0~255)
|
sbyte |
signed char / char |
? 對(duì)應(yīng)有符號(hào) 8 位整數(shù)(-128~127)
|
short |
short |
? 都是 16 位有符號(hào)整數(shù)
|
ushort |
unsigned short |
? 都是 16 位無(wú)符號(hào)整數(shù)
|
int |
int |
? Windows 上都是 32 位有符號(hào)整數(shù)
|
uint |
unsigned int |
? 32 位無(wú)符號(hào)整數(shù)
|
long |
long long |
?? C++ 的
long 是 4 字節(jié),C# 的 long 是 8 字節(jié),建議對(duì)應(yīng) long long |
float |
float |
? 都是 32 位浮點(diǎn)數(shù)
|
double |
double |
? 都是 64 位浮點(diǎn)數(shù)
|
IntPtr |
void* / 指針類型 |
? 平臺(tái)相關(guān)的指針地址(x86 是 4 字節(jié),x64 是 8 字節(jié))
|
byte[] + [MarshalAs(...)] |
unsigned char buffer[固定大小] |
? 用于定長(zhǎng)字節(jié)數(shù)組傳遞,必須指定大小
|
4.1 基本數(shù)據(jù)類型傳遞
1. 通過(guò)值傳遞基本類型參數(shù) (Pass by Value):傳遞的是參數(shù)的值,函數(shù)內(nèi)部修改不會(huì)影響調(diào)用方
-
C++:
void Test_BasicData(unsigned char b, short s, int i, long long l, float f, double d); -
C#:
Test_BasicData(char b, short s, int i, long l, float f, double d);
-
C++:
void Test_BasicDataRef(char &b, short &s, int &i, long long &l, float &f, double &d); -
C#:
Test_BasicDataRef(ref char b, ref short s, ref int i, ref long l, ref float f, ref double d);
3. 通過(guò)指針傳遞基本類型參數(shù) (Pass by Pointer): C# 使用 ref 來(lái)傳遞指針,通過(guò) char*, short* 等 C++ 指針類型,C++ 可以直接修改傳入的變量。這種方式通常用于需要通過(guò) C++ 修改大量數(shù)據(jù)或處理復(fù)雜數(shù)據(jù)結(jié)構(gòu)的場(chǎng)景
-
C++:
void Test_BasicDataPoint(char* b, short* s, int* i, long long* l, float* f, double* d); -
C#:
Test_BasicDataPoint(ref char b, ref short s, ref int i, ref long l, ref float f, ref double d);
完整代碼
// =================== C# 文件:Program.cs ===================
using System;
using System.Runtime.InteropServices;
class Program
{
[DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void Test_BasicData(char b, short s, int i, long l, float f, double d);
[DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void Test_BasicDataRef(ref char b, ref short s, ref int i, ref long l, ref float f, ref double d);
[DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void Test_BasicDataPoint(ref char b, ref short s, ref int i, ref long l, ref float f, ref double d);
[DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int Add(int a, int b);
static void Main()
{
Test_BasicData('a', 1, 2, 3, 4.0f, 5.0);
char b = 'a';
short s = 1;
int i = 2;
long l = 3;
float f = 4.0f;
double d = 5.0;
Test_BasicDataRef(ref b, ref s, ref i, ref l, ref f, ref d);
Test_BasicDataPoint(ref b, ref s, ref i, ref l, ref f, ref d);
int n = Add(1, 3);
Console.WriteLine($"Add result: {n}");
Console.Read();
}
}
// =================== C++ 文件:mylib.cpp ===================
#include <windows.h>
#include <stdio.h>
#include <iostream>
using namespace std;
extern "C" __declspec(dllexport) int Add(int a, int b);
extern "C" __declspec(dllexport) void Test_BasicData(unsigned char b, short s, int i, long long l, float f, double d);
extern "C" __declspec(dllexport) void Test_BasicDataRef(char &b, short &s, int& i, long long& l, float& f, double& d);
extern "C" __declspec(dllexport) void Test_BasicDataPoint(char* b, short* s, int* i, long long* l, float* f, double* d);
int Add(int a, int b)
{
return a + b;
}
void Test_BasicData(unsigned char b, short s, int i, long long l, float f, double d)
{
cout << "Test_BasicData: " << b << ", " << s << ", " << i << ", " << l << ", " << f << ", " << d << endl;
}
void Test_BasicDataRef(char& b, short& s, int& i, long long& l, float& f, double& d)
{
b += 1; s += 2; i += 3; l += 4; f += 5; d += 6;
cout << "Test_BasicDataRef modified values.\n";
}
void Test_BasicDataPoint(char* b, short* s, int* i, long long* l, float* f, double* d)
{
*b += 11; *s += 22; *i += 33; *l += 44; *f += 55; *d += 66;
cout << "Test_BasicDataPoint modified values.\n";
}
[DllImport] 參數(shù)簡(jiǎn)要說(shuō)明
-
"mylib.dll":指定要加載的 DLL 文件名。 -
CallingConvention:指定調(diào)用約定,通常用Cdecl與 C++ 函數(shù)匹配。 -
EntryPoint:顯式指定要調(diào)用的函數(shù)名,用于解決名稱不一致的問(wèn)題。 -
CharSet:設(shè)置字符串編碼方式(如Ansi或Unicode)。 -
ExactSpelling:禁止根據(jù)CharSet自動(dòng)添加后綴(如 A/W)。
4.2 數(shù)組傳遞
-
C# 的數(shù)組可以直接傳給 C++,如
int[]對(duì)應(yīng)int*。因?yàn)?strong> C++ 無(wú)法知道數(shù)組長(zhǎng)度,需額外傳遞一個(gè)size參數(shù)表示元素?cái)?shù)量。內(nèi)存是順序連續(xù)的,C++ 端可以正常遍歷使用。
-
C++ 返回?cái)?shù)組時(shí)需使用靜態(tài)或堆內(nèi)存,不能返回局部變量。C# 端使用
IntPtr接收,再通過(guò)Marshal.Copy復(fù)制為托管數(shù)組。如果 C++ 返回的是堆內(nèi)存(動(dòng)態(tài)分配的內(nèi)存),C# 端必須調(diào)用 C++ 提供的釋放函數(shù),否則內(nèi)存會(huì)泄漏。
示例
// ============== C# 代碼 ==============
using System;
using System.Runtime.InteropServices;
class Program
{
[DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void Test_BasicDataArr(int[] arr1, int arr1Len, float[] arr2, int arr2Len);
[DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr Test_BasicDataRet();
[DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr Test_BasicDataRet1();
[DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void FreeFloatArray(IntPtr arr);
static void Main()
{
int[] intArr = new int[5] { 1, 2, 3, 4, 5 };
float[] floatArr = new float[5] { 11f, 22f, 33f, 44f, 55f };
// 調(diào)用傳數(shù)組函數(shù),需同時(shí)傳長(zhǎng)度
Test_BasicDataArr(intArr, intArr.Length, floatArr, floatArr.Length);
// 調(diào)用返回?cái)?shù)組函數(shù),假設(shè)返回5個(gè)整數(shù),C++ 返回靜態(tài)數(shù)組指針,C#不需要釋放
IntPtr retPtr = Test_BasicDataRet();
int[] retArr = new int[5];
Marshal.Copy(retPtr, retArr, 0, 5);
Console.WriteLine("Returned array from C++:");
foreach (var v in retArr)
Console.WriteLine(v);
// 調(diào)用返回?cái)?shù)組函數(shù),假設(shè)返回5個(gè)浮點(diǎn)數(shù),C++ 返回堆內(nèi)存指針,C#需要調(diào)用釋放函數(shù)釋放
IntPtr retFloatPtr = Test_BasicDataRet1();
float[] retFloatArr = new float[5];
Marshal.Copy(retFloatPtr, retFloatArr, 0, 5);
FreeFloatArray(retFloatPtr); // 釋放堆上的數(shù)組
foreach (var v in retFloatArr)
Console.WriteLine(v);
}
}
// ============== C++ 代碼 ==============
#include <iostream>
#include <cstring>
extern "C" __declspec(dllexport)
void Test_BasicDataArr(int* arr1, int arr1Len, float* arr2, int arr2Len)
{
std::cout << "Received int array from C#: ";
for (int i = 0; i < arr1Len; i++)
std::cout << arr1[i] << " ";
std::cout << std::endl;
std::cout << "Received float array C#: ";
for (int i = 0; i < arr2Len; i++)
std::cout << arr2[i] << " ";
std::cout << std::endl;
}
static int retArr[5] = { 100, 200, 300, 400, 500 };
extern "C" __declspec(dllexport)
int* Test_BasicDataRet()
{
return retArr;
}
extern "C" __declspec(dllexport)
float* Test_BasicDataRet1()
{
float* retArr1 = new float[5] { 1.1f, 2.2f, 3.3f, 4.4f, 5.5f };
return retArr1;
}
extern "C" __declspec(dllexport)
void FreeFloatArray(float* arr)
{
delete[] arr;
}
4.3 字符串傳遞
-
使用
string直接傳遞給const char*,會(huì)自動(dòng)在字符串末尾添加\0(null 終止符),C++ 端可以直接用printf輸出。默認(rèn)編碼是 ANSI,如需傳中文建議使用 UTF-8 編碼,并確保 C++ 控制臺(tái)設(shè)置正確編碼頁(yè)。
-
使用 UTF-8 編碼的
byte[],需要手動(dòng)在 C# 末尾添加一個(gè)0字節(jié)。C++ 依然用const char*接收,配合SetConsoleOutputCP(CP_UTF8)可正確打印中文。推薦用于跨平臺(tái)、支持中文、傳遞原始字符串?dāng)?shù)據(jù)等場(chǎng)景。
示例
// ============== C# 代碼 ==============
using System.Runtime.InteropServices;
class Program
{
// C++ 接收字符串(以 null 結(jié)尾的 ANSI 字符串)
[DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void Test_BasicDataString(string str);
// C++ 接收字節(jié)數(shù)組(需要自己確保字節(jié)數(shù)組以 \0 結(jié)尾)
[DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void Test_BasicDataByteArr(byte[] str);
static void Main()
{
string str = "你好";
// 方法1:直接傳遞 string,DllImport 自動(dòng)轉(zhuǎn)換為 null 結(jié)尾的 ANSI 字符串
Test_BasicDataString(str);
// 方法2:手動(dòng)將字符串轉(zhuǎn)換成 UTF-8 字節(jié)數(shù)組(不含尾部 \0),字節(jié)數(shù)組傳遞時(shí),自己加上 \0 結(jié)束符
byte[] utf8Bytes = System.Text.Encoding.UTF8.GetBytes(str);
byte[] utf8BytesWithNull = utf8Bytes.Concat(new byte[] { 0 }).ToArray();
Test_BasicDataByteArr(utf8BytesWithNull);
Console.ReadLine();
}
}
4.4 結(jié)構(gòu)體傳遞
方法一:手動(dòng)偏移
通過(guò)已知偏移,使用 Marshal.ReadInt32、ReadInt64、Marshal.Copy 等方法逐字段讀取結(jié)構(gòu)體內(nèi)容。
// ============== C++ 代碼 ==============
#include <string.h>
struct FrameInfo
{
char username[20]; //0-20
double pts; //24-32
};
struct Frame
{
int width; //0-4
int height; //4-8
int format; //8-12
int linesize[4]; //12-28
unsigned char* data[4]; //32-64 指針大小8,需要是8的倍數(shù)
FrameInfo* info; //64-72 指針大小8
};
Frame frame;
FrameInfo info;
extern "C" __declspec(dllexport) Frame* Test_Struct()
{
frame.width = 1920;
frame.height = 1080;
frame.format = 1;
for (size_t i = 0; i < 4; i++)
{
frame.linesize[i] = 100 * i;
frame.data[i] = new unsigned char[10];
for (size_t j = 0; j < 10; j++)
{
frame.data[i][j] = i;
}
}
info.pts = 12.5;
memset(info.username, 0, 20);
memcpy(info.username, "hello world", strlen("hello world"));
frame.info = &info;
return &frame;
}
// ============== C# 代碼 ==============
using System;
using System.Runtime.InteropServices;
class Program
{
// 聲明導(dǎo)入的本地方法,返回一個(gè)指向 Frame 結(jié)構(gòu)體的指針
[DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr Test_Struct();
static void Main()
{
// 調(diào)用本地方法,獲取 Frame 結(jié)構(gòu)體指針
IntPtr ptr = Test_Struct();
// 讀取 Frame 結(jié)構(gòu)體前3個(gè)int成員,偏移分別是0, 4, 8
int width = Marshal.ReadInt32(ptr, 0); // 讀取 width
int height = Marshal.ReadInt32(ptr, 4); // 讀取 height
int format = Marshal.ReadInt32(ptr, 8); // 讀取 format
// 創(chuàng)建整型數(shù)組接收 linesize[4],占16字節(jié)(4個(gè)int)
int[] linesize = new int[4];
// linesize數(shù)組在結(jié)構(gòu)體偏移12處
IntPtr linesizePtr = IntPtr.Add(ptr, 12);
// 從非托管內(nèi)存復(fù)制4個(gè)int到托管linesize數(shù)組
Marshal.Copy(linesizePtr, linesize, 0, 4);
// 創(chuàng)建IntPtr數(shù)組用于存放 data[4](4個(gè)指針)
IntPtr[] datas = new IntPtr[4];
// data數(shù)組在結(jié)構(gòu)體中偏移為12+16+4=32
IntPtr dataPtr = IntPtr.Add(ptr, 32);
// 從非托管內(nèi)存復(fù)制4個(gè)指針到datas數(shù)組
Marshal.Copy(dataPtr, datas, 0, 4);
// 遍歷4個(gè)指針,從每個(gè)地址拷貝10字節(jié)數(shù)據(jù)到托管byte數(shù)組
for (int i = 0; i < 4; i++)
{
byte[] temp = new byte[10]; // 臨時(shí)數(shù)組接收數(shù)據(jù)
Marshal.Copy(datas[i], temp, 0, 10);
// 這里可以對(duì)temp數(shù)組做后續(xù)處理
}
// 讀取 Frame 結(jié)構(gòu)體中 info 指針,偏移64處
IntPtr infoPtr = Marshal.ReadIntPtr(ptr, 64);
// 讀取 info 結(jié)構(gòu)體中 username 字符數(shù)組(20字節(jié))
byte[] username = new byte[20];
Marshal.Copy(infoPtr, username, 0, 20);
// 讀取 info 結(jié)構(gòu)體中 double pts,偏移24字節(jié)(20字節(jié)username + 4字節(jié)padding)
// 先讀64位整數(shù),再轉(zhuǎn)換成double
double pts = BitConverter.Int64BitsToDouble(Marshal.ReadInt64(infoPtr, 24));
// 保持控制臺(tái)窗口,方便查看輸出
Console.ReadLine();
}
方法二:結(jié)構(gòu)體映射
在 C# 中使用 [StructLayout] 正確定義結(jié)構(gòu)體布局,調(diào)用 Marshal.PtrToStructure<T> 一次性將內(nèi)存映射為結(jié)構(gòu)體對(duì)象。
// ============== C# 代碼 ==============
using System;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
struct FrameInfo
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
public byte[] username;
public double pts;
}
[StructLayout(LayoutKind.Sequential)]
struct Frame
{
public int width;
public int height;
public int format;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public int[] linesize;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public IntPtr[] data;
public IntPtr info;
}
class Program
{
// 聲明導(dǎo)入的本地方法,返回一個(gè)指向 Frame 結(jié)構(gòu)體的指針
[DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr Test_Struct();
static void Main()
{
// 調(diào)用本地方法,獲取 Frame 結(jié)構(gòu)體指針
IntPtr ptr = Test_Struct();
Frame frame = Marshal.PtrToStructure<Frame>(ptr);
FrameInfo info = Marshal.PtrToStructure<FrameInfo>(frame.info);
Console.WriteLine(info.pts);
}
}
?? 注意點(diǎn):定長(zhǎng)數(shù)組必須使用 [MarshalAs(ByValArray)] 顯式標(biāo)注
在進(jìn)行 C++ 與 C# 的結(jié)構(gòu)體互操作時(shí),C++ 中的定長(zhǎng)數(shù)組(如 char name[20])不能直接在 C# 中用數(shù)組表示,必須使用 [MarshalAs(UnmanagedType.ByValArray)] 顯式標(biāo)注,否則會(huì)導(dǎo)致內(nèi)存布局不一致或運(yùn)行時(shí)錯(cuò)誤。
?? 注意點(diǎn):結(jié)構(gòu)體使用 bool 字段時(shí)必須謹(jǐn)慎,尤其在有 Pack 設(shè)置時(shí)!
//=======================? C++ 結(jié)構(gòu)體=======================
// 編譯器默認(rèn)對(duì)齊,通常是 4 或 8 字節(jié)
struct MyStruct
{
bool flag1; // 0-1
bool flag2; // 1-2
int value; // 4-8
};
//=========================? C# 結(jié)構(gòu)體=========================
// C# 結(jié)構(gòu)體(默認(rèn)布局)
struct MyStruct {
public bool flag1;//0-4
public bool flag2;//4-8
public int value; //8-12
}
//C# 結(jié)構(gòu)體和 C++ 保持一致
struct BoolStruct
{
[MarshalAs(UnmanagedType.U1)]
public bool flag1; // 1 字節(jié)
[MarshalAs(UnmanagedType.U1)]
public bool flag2; // 1 字節(jié)
public int value; // 按 4 字節(jié)對(duì)齊,offset 4
}

浙公網(wǎng)安備 33010602011771號(hào)