C#与C++动态链接库数据传递

1 内存对齐规则

  • 结构体的数据成员,第一个成员的偏移量为0,后面每个成员变量的地址必须从其大小的整数倍开始。
  • 子结构体中的第一个成员偏移量应当是子结构体中最大成员的整数倍。
  • 结构体的总大小必须是其内部最大成员的整数倍

示例

#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字节
    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)修改对齐方式,控制结构体中成员的排列方式。

  • n 可以是 1、2、4、8、16 等,表示最大对齐数为 n 字节。

  • #pragma pack(1) 表示取消对齐,所有成员紧密排列,没有插入任何 padding。

使用 #pragma pack 有利于节省内存空间,但会降低访问效率,甚至引发硬件异常(部分平台不支持非对齐访问)。

#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)

此时Frame的大小变成了25。

2 C#结构体

示例

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# 中,结构体的内存布局控制是通过特性(Attributes)来实现的,特别是 [StructLayout][FieldOffset]

✅ 示例一:顺序布局 Sequential

[StructLayout(LayoutKind.Sequential)]
struct Frame //结构体大小24字节
{
    byte id;          // 0-1
    int width;        // 4-8
    long height;      // 8-16
    int size;         // 16-20
}

✅ 示例二:精确偏移 Explicit

[StructLayout(LayoutKind.Explicit)]
struct Frame //结构体大小48字节
{
    [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 //结构体大小17字节
{
    byte id;     //0-1
    int width;   //1-5
    long height; //5-13
    int size;    //13-17
}

3 调用约定

__cdecl 是 C/C++ 的默认调用约定,参数从右向左入栈,调用者负责清理栈。它支持可变参数函数(如 printf),跨平台兼容性好。在 C# 调用时应指定 CallingConvention.Cdecl,适合自己写的 DLL。

__stdcall 是 Windows API 常用的调用约定,参数也是从右向左入栈,但由被调用者清理栈空间。函数名导出时会被修饰(如 _Func@8),不支持可变参数。C# 默认用它,常用于调用系统 DLL 或标准第三方库。

4 C#与C++之间类型的对应关系

C# 类型
C++ 对应类型
⚠️ 差异说明
byte
unsigned char
✅ 完全匹配,8 位无符号整数(0~255)
sbyte
signed char / char
✅ 对应有符号 8 位整数(-128~127)
short
short
✅ 都是 16 位有符号整数
ushort
unsigned short
✅ 都是 16 位无符号整数
int
int
✅ Windows 上都是 32 位有符号整数
uint
unsigned int
✅ 32 位无符号整数
long
long long
⚠️ C++ 的 long 是 4 字节,C# 的 long 是 8 字节,建议对应 long long
float
float
✅ 都是 32 位浮点数
double
double
✅ 都是 64 位浮点数
IntPtr
void* / 指针类型
✅ 平台相关的指针地址(x86 是 4 字节,x64 是 8 字节)
byte[] + [MarshalAs(...)]
unsigned char buffer[固定大小]
✅ 用于定长字节数组传递,必须指定大小

4.1 基本数据类型传递

1. 通过值传递基本类型参数 (Pass by Value):传递的是参数的值,函数内部修改不会影响调用方

  • 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);

2. 通过引用传递基本类型参数 (Pass by Reference):传递参数的引用,函数内部修改会反映到调用方
  • 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. 通过指针传递基本类型参数 (Pass by Pointer): C# 使用 ref 来传递指针,通过 char*short* 等 C++ 指针类型,C++ 可以直接修改传入的变量。这种方式通常用于需要通过 C++ 修改大量数据或处理复杂数据结构的场景

  • 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] 参数简要说明

  • "mylib.dll":指定要加载的 DLL 文件名。

  • CallingConvention:指定调用约定,通常用 Cdecl 与 C++ 函数匹配。

  • EntryPoint:显式指定要调用的函数名,用于解决名称不一致的问题。

  • CharSet:设置字符串编码方式(如 AnsiUnicode)。

  • ExactSpelling:禁止根据 CharSet 自动添加后缀(如 A/W)。

4.2 数组传递

  • C# 的数组可以直接传给 C++,如 int[] 对应 int*。因为 C++ 无法知道数组长度,需额外传递一个 size 参数表示元素数量。内存是顺序连续的,C++ 端可以正常遍历使用。

  • C++ 返回数组时需使用静态或堆内存,不能返回局部变量。C# 端使用 IntPtr 接收,再通过 Marshal.Copy 复制为托管数组。如果 C++ 返回的是堆内存(动态分配的内存),C# 端必须调用 C++ 提供的释放函数,否则内存会泄漏。

示例

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

        // 调用传数组函数,需同时传长度
        Test_BasicDataArr(intArr, intArr.Length, floatArr, floatArr.Length);

        // 调用返回数组函数,假设返回5个整数,C++ 返回静态数组指针,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);

        // 调用返回数组函数,假设返回5个浮点数,C++ 返回堆内存指针,C#需要调用释放函数释放
        IntPtr retFloatPtr = Test_BasicDataRet1();
        float[] retFloatArr = new float[5];
        Marshal.Copy(retFloatPtr, retFloatArr, 0, 5);
        FreeFloatArray(retFloatPtr); // 释放堆上的数组
        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*,会自动在字符串末尾添加 \0(null 终止符),C++ 端可以直接用 printf 输出。默认编码是 ANSI,如需传中文建议使用 UTF-8 编码,并确保 C++ 控制台设置正确编码页。

  • 使用 UTF-8 编码的 byte[],需要手动在 C# 末尾添加一个 0 字节。C++ 依然用 const char* 接收,配合 SetConsoleOutputCP(CP_UTF8) 可正确打印中文。推荐用于跨平台、支持中文、传递原始字符串数据等场景。

示例

// ============== C# 代码 ==============
using System.Runtime.InteropServices;
class Program
{
    // C++ 接收字符串(以 null 结尾的 ANSI 字符串)
    [DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern void Test_BasicDataString(string str);

    // C++ 接收字节数组(需要自己确保字节数组以 \0 结尾)
    [DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern void Test_BasicDataByteArr(byte[] str);

    static void Main()
    {
        string str = "你好";

        // 方法1:直接传递 string,DllImport 自动转换为 null 结尾的 ANSI 字符串
        Test_BasicDataString(str);

        // 方法2:手动将字符串转换成 UTF-8 字节数组(不含尾部 \0),字节数组传递时,自己加上 \0 结束符
        byte[] utf8Bytes = System.Text.Encoding.UTF8.GetBytes(str);
        byte[] utf8BytesWithNull = utf8Bytes.Concat(new byte[] { 0 }).ToArray();
        Test_BasicDataByteArr(utf8BytesWithNull);

        Console.ReadLine();
    }
}

4.4 结构体传递

方法一:手动偏移

通过已知偏移,使用 Marshal.ReadInt32ReadInt64Marshal.Copy 等方法逐字段读取结构体内容。

// ============== 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的倍数
	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
{
    // 声明导入的本地方法,返回一个指向 Frame 结构体的指针
    [DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr Test_Struct();

    static void Main()
    {
        // 调用本地方法,获取 Frame 结构体指针
        IntPtr ptr = Test_Struct();

        // 读取 Frame 结构体前3个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

        // 创建整型数组接收 linesize[4],占16字节(4个int)
        int[] linesize = new int[4];
        // linesize数组在结构体偏移12处
        IntPtr linesizePtr = IntPtr.Add(ptr, 12);
        // 从非托管内存复制4个int到托管linesize数组
        Marshal.Copy(linesizePtr, linesize, 0, 4);

        // 创建IntPtr数组用于存放 data[4](4个指针)
        IntPtr[] datas = new IntPtr[4];
        // data数组在结构体中偏移为12+16+4=32
        IntPtr dataPtr = IntPtr.Add(ptr, 32);
        // 从非托管内存复制4个指针到datas数组
        Marshal.Copy(dataPtr, datas, 0, 4);

        // 遍历4个指针,从每个地址拷贝10字节数据到托管byte数组
        for (int i = 0; i < 4; i++)
        {
            byte[] temp = new byte[10]; // 临时数组接收数据
            Marshal.Copy(datas[i], temp, 0, 10);
            // 这里可以对temp数组做后续处理
        }

        // 读取 Frame 结构体中 info 指针,偏移64处
        IntPtr infoPtr = Marshal.ReadIntPtr(ptr, 64);

        // 读取 info 结构体中 username 字符数组(20字节)
        byte[] username = new byte[20];
        Marshal.Copy(infoPtr, username, 0, 20);

        // 读取 info 结构体中 double pts,偏移24字节(20字节username + 4字节padding)
        // 先读64位整数,再转换成double
        double pts = BitConverter.Int64BitsToDouble(Marshal.ReadInt64(infoPtr, 24));

        // 保持控制台窗口,方便查看输出
        Console.ReadLine();
    }

方法二:结构体映射

在 C# 中使用 [StructLayout] 正确定义结构体布局,调用 Marshal.PtrToStructure<T> 一次性将内存映射为结构体对象。

// ============== 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
{
    // 声明导入的本地方法,返回一个指向 Frame 结构体的指针
    [DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr Test_Struct();

    static void Main()
    {
        // 调用本地方法,获取 Frame 结构体指针
        IntPtr ptr = Test_Struct();

        Frame frame = Marshal.PtrToStructure<Frame>(ptr);
        FrameInfo info = Marshal.PtrToStructure<FrameInfo>(frame.info);
        Console.WriteLine(info.pts);
    }
}

⚠️ 注意点:定长数组必须使用 [MarshalAs(ByValArray)] 显式标注

在进行 C++ 与 C# 的结构体互操作时,C++ 中的定长数组(如 char name[20])不能直接在 C# 中用数组表示,必须使用 [MarshalAs(UnmanagedType.ByValArray)] 显式标注,否则会导致内存布局不一致或运行时错误。

⚠️ 注意点:结构体使用 bool 字段时必须谨慎,尤其在有 Pack 设置时!

//=======================✅ C++ 结构体=======================

// 编译器默认对齐,通常是 4 或 8 字节
struct MyStruct
{
    bool flag1;   // 0-1
    bool flag2;   // 1-2
    int value;    // 4-8
};

//=========================✅ C# 结构体=========================
// C# 结构体(默认布局)
struct MyStruct {
    public bool flag1;//0-4
    public bool flag2;//4-8
    public int value; //8-12
}

//C# 结构体和 C++ 保持一致
struct BoolStruct
{
    [MarshalAs(UnmanagedType.U1)]
    public bool flag1;  // 1 字节

    [MarshalAs(UnmanagedType.U1)]
    public bool flag2;  // 1 字节

    public int value;   // 按 4 字节对齐,offset 4
}

  

  

  

posted @ 2025-06-09 09:34  湾仔码农  阅读(122)  评论(0)    收藏  举报