C# 与 C/C++ 的交互

什么是平台调用 (P/Invoke)

  • P/Invoke 是可用于从托管代码访问非托管库中的结构、回调和函数的一种技术。

托管代码与非托管的区别

    托管代码和非托管代码的主要区别是内存管理方式和对计算机资源的访问方式。托管代码通常运行在托管环境中,如 mono 或 java 虚拟机等,这些环境提供了垃圾回收器(GC)等工具来管理内存。在托管环境中,程序员通常不需要手动分配和释放内存,因为这些任务由运行时系统自动完成。托管代码通常具有更高的安全性和可移植性,因为它们运行在虚拟机中,而不是直接在操作系统上运行。
    相比之下,非托管代码通常是直接在操作系统上运行的,这意味着程序员需要手动分配和释放内存来避免内存泄漏和其他内存相关的问题。非托管代码可以更直接地访问计算机硬件和操作系统资源,因此它们通常具有更高的性能和更好的控制性。
  • P/Invoke API 包含在以下两个命名空间中:SystemSystem.Runtime.InteropServices。 使用这两个命名空间可提供用于如何与 native 通信的工具。

  • P/Invoke 从功能上来说,只支持函数调用,在被导出的函数前面一定要添加extern "C"来指明导出函数的时候使用C语言方式编译和连接,这样保证函数定义的名字和导出的名字相同,否则如果默认按C++方式导出,那个函数的名字就会变得乱七八糟,我们的程序就无法找到入口点了。

互调的基本原理

首先,我们需要了解 数据类型 的概念

在大多数编译型语言中定义变量的时候都需要先指定其数据类型,所谓数据类型,是语言约定的一个便于记忆的名称(int,long,float),究其本质就是在指明了这个数据在内存中到底是占用了几个字节,程序在运行的时候,首先找到这个数据的地址,然后再按照该类型的长度,读取响应的内存,然后再处理。

基于这个原理,编程语言直接就可以进行互调了,对于不同语言之间的互调,只要将改数据的指针(内存地址)传递给另一个语言,在另一个语言中根据通信协议将指针所指向的数据存储入长度对应的数据类型即可,当然要满足以下条件:

  1. 对于像 java,C# 这样有运行时虚拟机变成语言来讲,由于虚拟内存会让堆内存来回转移,因此在进行互调的时候,需要保证被调用的数据所在内存一定要固定,不能被转移。
  2. 有一些编程语言支持指针,有一些语言不支持指针(如Java),这个问题并不重要,所谓指针,其实就是一个内存地址,对于32位OS的指针是一个32位整数,而对于64位机OS的指针是一个64位整数。因为大多数语言中都有整型数,所以可以利用整型来接收指针。

Native 库的加载

举例被加载库的名称为: nativelib.so/nativelib.dll

  • 在 windows 平台运行时,将按以下顺序搜寻 dll:
    1. nativelib
    2. nativelib.dll
C# 复制代码
[DllImport("nativelib")]
[DllImport("nativelib.dll")]
static extern int ExportedFunction();

基本数据类型传递

C# 关键字 .Net类型 C/C++
byte System.Byte uint8_t
sbyte System.SByte int8_t
short System.Int16 int16_t
ushort System.UInt16 uint16_t
int System.Int32 int32_t
uint System.UInt32 uint32_t
long System.Int64 int64_t
ulong System.UInt64 uint64_t
char System.Char char
nint System.IntPtr intptr_t
bool System.Boolean Win32 BOOL 类型
decimal System.Decimal COM DECIMAL 结构

调用流程

基础数据类型调用

C++ 复制代码
#define EXPORTAPI __declspec(dllexport)

extern "C" EXPORTAPI int add(int a, int b)
{
	return a + b;
}
extern "C" EXPORTAPI void writeString(char* content)
{
	cout << content << endl;
}
  1. 第一行代码中定义了一个 EXPORTAPI 的宏,对应的内容是 __declspec(dllexport) 意思是将后面修饰的内容定义为 Dll 中要导出的内容 ,当然也剋以不使用这个宏,直接将 __declspec(dllexport) 写在要导出的函数前。
  2. 第二行中的 extern "C" ,表示该函数编译和连接时使用 C 语言的方式以保证函数名称不变,由于 C++ 支持函数重载,因此编译器会将函数的参数类型也加到编译后的代码中。例如函数 void fun(int,int),编译后的可能是 _fun_int_int(不同的编译器可能不同,但都采取了类似的机制),extern 是 C/C++ 语言中表明函数和全局变量的作用范围的关键字,这个关键字告诉编译器,其声明的函数和变量可以在本模块或其他模块中使用。
C# 复制代码
    [DllImport("TestCPPDll.dll",EntryPoint = "add")]
    extern static int add(int a, int b);

    [DllImport("TestCPPDll.dll")]
    extern static void writeString(string content);

    public static void Add()
    {
        int result = add(1, 2);
        Console.WriteLine($"CallLibraryAdd result :{result}");
    }

    public static void WriteString(string str)
    {
        writeString(str);
    }

DllImport 中第一个参数表示 Dll 文件的位置,第二个参数 EntryPoint 用来指明对应的 C/C++ 中的函数名称是什么, extern 关键字表明该方法是一个外部调用,这个方法声明后,就可以像调用一个普通的静态方法一样去使用了。

指针的传递

C++ 复制代码
extern "C" EXPORTAPI void addInt(int* i)
{
	*i += 1;
	cout << *i << endl;
}

//传入一个整型数组的指针以及数组长度,遍历每一个元素并且输出
extern "C" EXPORTAPI void sendIntArray(int* firstElement, int arraylength)
{
	cout << "C# send int array to CPP" << endl;
	int* currentPointer = firstElement;
	for (int i = 0; i < arraylength; i++)
	{
		cout << currentPointer[i];
	}

	cout << endl;
}

在第一个方法中参数为一个 int 类型的指针,并将其所指向的内容 +1

第二个方法传入一个整型数组的指针以及数组长度,遍历数组的每一个元素并且输出

C# 复制代码
    #region 指针的传递
    [DllImport("TestCPPDll.dll")]
    extern unsafe static void addInt(int* i);

    [DllImport("TestCPPDll.dll")]
    extern unsafe static void sendIntArray(int* firstElement, int arraylength);

    [DllImport("TestCPPDll.dll", EntryPoint = "getArrayFromCPP")]
    extern unsafe static IntPtr getArrayFromCPPUseInpPtr(IntPtr count);

    [DllImport("TestCPPDll.dll", EntryPoint = "getArrayFromCPP")]
    extern unsafe static IntPtr getArrayFromCPPUseRef(ref int count);



    // 调用 C++ 中的 AddInt 方法
    public static void AddInt()
    {
        int i = 10;
        unsafe
        {
            addInt(&i);
        }
    }

    //调用 C++ 中的 sendIntArray 方法将 C# 中的数组数据传递到 C++ 中,并在 C++ 中输出
    public static void AddIntArry()
    {
        int[] csArry = new int[10];
        for (int iArr = 0; iArr < 10; iArr++)
        {
            csArry[iArr] = iArr;
        }

        unsafe
        {
            fixed (int* pCSArray = &csArry[0])
            {
                sendIntArray(pCSArray, 10);
            }
        }
    }

    //调用C++中的GetArrayFromCPP方法获取一个C++中建立的数组,使用 InpPrt 类型传参,IntPtr 与指针可以互相强转
    public static void GetArrayFromCPPUseInpPtr()
    {

        Console.WriteLine("CPP send int array to C# user IntPtr");
        int count = 0;
        GCHandle gch = GCHandle.Alloc(count, GCHandleType.Pinned);
        IntPtr countInptr = gch.AddrOfPinnedObject();
        IntPtr pArrayPointer = getArrayFromCPPUseInpPtr(countInptr);
        count = Marshal.PtrToStructure<int>(countInptr);
        int[] array = new int[count];
        for (int i = 0; i < count; i++)
        {
            IntPtr intPtr = IntPtr.Add(pArrayPointer, i * Marshal.SizeOf<int>());
            array[i] = Marshal.PtrToStructure<int>(intPtr);
            Console.Write(array[i]);
        }

        Console.WriteLine();
    }

    //调用C++中的GetArrayFromCPP方法获取一个C++中建立的数组, 使用 ref 会出现防御性拷贝,造成 GC
    public static void GetArrayFromCPPUseRef()
    {
        Console.WriteLine("CPP send int array to C# user ref");
        int count = 0;
        IntPtr pArrayPointer = getArrayFromCPPUseRef(ref count);
        int[] array = new int[count];
        for (int i = 0; i < count; i++)
        {
            IntPtr intPtr = IntPtr.Add(pArrayPointer, i * Marshal.SizeOf<int>());
            array[i] = Marshal.PtrToStructure<int>(intPtr);
            Console.Write(array[i]);
        }

        Console.WriteLine();
    }
    #endregion

函数指针的传递

C# 中并没有函数指针的概念,但是可以使用 委托 来代替函数指针

在 C/C++ 运行的某一时刻调用 C# 的对应函数, 这个时候就需要将一个 C# 中已经指向某一个函数的函数指针(委托)传递给 C++

C++ 复制代码
//定义一个函数指针
typedef void (*OnCSCallBack)(int value);

OnCSCallBack onCSCallBack;

//用于设置函数指针的方法
extern "C" EXPORTAPI void setCallback(OnCSCallBack callback)
{
	onCSCallBack = callback;
	cout << "set CPP callback success" << endl;
	cout << endl;
}

//对 C# 中传递过来的委托进行调用
extern "C" EXPORTAPI void testCPPInvoke() {
	int value = 999;
	onCSCallBack(value);
}
C# 复制代码
    public delegate void OnCSDelegate(int value);

    [DllImport("TestCPPDll.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)]
    extern static void setCallback(OnCSDelegate action);

    [DllImport("TestCPPDll.dll")]
    extern static void testCPPInvoke();

    //测试过程中发现 .Net 中 deletegate 可以传递。action<int> 不行。
    //而在 Unity 中 将 Action 方法添加[MonoPInvokeCallback(typeof(Action<int>))] 标签后可以. MonoPInvokeCallback 标签在 AOT 命名空间下
    public static void SetCPPCallBack()
    {
        OnCSDelegate onCSCallBack = OnCSCallback;
        setCallback(onCSCallBack);
    }

    public static void OnCSCallback(int value)
    {
        Console.WriteLine($"CPP call CSharp founction value:{value}");
    }

结构体的传递

传递结构体的想法和传递一个int类型数据类似,struct中的数据是在内存中顺序排列的

  • C#中结构体字段类型与C++结构体中的字段类型相兼容
  • C#结构中的字段顺序与C++结构体中的字段顺序相同,要保证该功能,需要将C#结构体标记为[StructLayout( LayoutKind.Sequential)]
C++ 复制代码
typedef struct Vector3_
{
	float x;
	float y;
	float z;
} Vector3;

// 输出从 C# 侧传入的结构体内容
extern "C" EXPORTAPI void sendStructFormCSToCPP(Vector3 vector3)
{
	cout << "get vector3 int cpp,x:";
	cout << vector3.x;
	cout << ",y:";
	cout << vector3.y;
	cout << ",z:";
	cout << vector3.z << endl;
	cout << endl;
}

//包含固定长度数组的结构体
typedef struct ContainArray_
{
	Vector3 vectorArray[5];

	float valueArray[2];
}ContainArray;

//不固定数组长度的结构体
typedef struct ContainArrayHasCount_
{
	int arrayCount;
	float* floatArray;
	int vectorArrayCount;
	Vector3* vectorArray;
}ContainArrayHasCount;

//输出从 C# 侧传入数组固定长度的结构体信息
extern "C" EXPORTAPI void sendContainArrayStructFormCSToCPP(ContainArray containArray)
{
	int vectorArrayCount = sizeof(containArray.vectorArray) / sizeof(*containArray.vectorArray);
	for (size_t i = 0; i < vectorArrayCount; i++)
	{
		cout << "vectorArray i: " << i << ",x:" << containArray.vectorArray[i].x << ",y:" << containArray.vectorArray[i].y << ",z:" << containArray.vectorArray[i].z << endl;
	}
	int valueArrayCount = sizeof(containArray.valueArray) / sizeof(*containArray.valueArray);

	for (size_t i = 0; i < valueArrayCount; i++)
	{
		cout << "valueArray i: " + i << ", value: " << containArray.valueArray[i] << endl;
	}

	cout << endl;
}

//输出运行时决定数组长度的结构体信息
extern "C" EXPORTAPI void sendContainArrayHasCountStructFormCSToCPP(ContainArrayHasCount containArrayHasCount)
{
	for (size_t i = 0; i < containArrayHasCount.arrayCount; i++)
	{
		cout << "containArrayHasCount i: " << i << ",ArrayValue: " << containArrayHasCount.floatArray[i] << endl;
	}

	for (size_t i = 0; i < containArrayHasCount.vectorArrayCount; i++)
	{
		cout << "vectorArray i: " << i << ",x:" << containArrayHasCount.vectorArray[i].x << ",y:" << containArrayHasCount.vectorArray[i].y << ",z:" << containArrayHasCount.vectorArray[i].z << endl;
	}
}

//返回 C++ 侧创建的结构体指针
extern "C" EXPORTAPI ContainArrayHasCount * getArrayStructFormCPP()
{
	ContainArrayHasCount* containArrayHasCount = new ContainArrayHasCount();
	containArrayHasCount->arrayCount = 3;
	containArrayHasCount->floatArray = new float[3];
	for (size_t i = 0; i < containArrayHasCount->arrayCount; i++)
	{
		containArrayHasCount->floatArray[i] = i;
	}

	containArrayHasCount->vectorArrayCount = 4;
	containArrayHasCount->vectorArray = new Vector3[4];
	for (size_t i = 0; i < containArrayHasCount->vectorArrayCount; i++)
	{
		containArrayHasCount->vectorArray[i].x = i;
		containArrayHasCount->vectorArray[i].y = i;
		containArrayHasCount->vectorArray[i].z = i;
	}

	return containArrayHasCount;
}
C# 复制代码
    [StructLayout(LayoutKind.Sequential)]
    public struct Vector3
    {
        public Vector3(float x, float y, float z)
        {
            X = x;
            Y = y;
            Z = z;
        }

        public float X, Y, Z;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct ContainArray
    {
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
        public Vector3[] vectorArray;

        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
        public float[] valueArray;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct ContainArrayHasCount
    {
        public int floatArrayCount;
        public IntPtr floatArray;
        public int vectorArrayCount;
        public IntPtr vectorArray;
    }

    [DllImport("TestCPPDll")]
    extern static void sendStructFormCSToCPP(Vector3 vector3);

    [DllImport("TestCPPDll")]
    extern static void sendContainArrayStructFormCSToCPP(ContainArray containArrayStruct);

    [DllImport("TestCPPDll")]
    extern static void sendContainArrayHasCountStructFormCSToCPP(ContainArrayHasCount containArrayHasCount);

    [DllImport("TestCPPDll")]
    extern static IntPtr getArrayStructFormCPP();

    // 结构体作为参数传递给 C++ 方法
    public static void SendStructFormCSToCPP()
    {
        Vector3 vector3 = new Vector3();
        vector3.X = 1;
        vector3.Y = 2;
        vector3.Z = 3;
        sendStructFormCSToCPP(vector3);
    }

    // 传入包含指定长度数组的结构体
    public static void SendContainArrayStructFormCSToCPP()
    {
        ContainArray containArrayStruct = new ContainArray();
        containArrayStruct.vectorArray = new Vector3[5];
        for (int i = 0; i < 5; i++)
        {
            containArrayStruct.vectorArray[i].X = i;
            containArrayStruct.vectorArray[i].Y = i;
            containArrayStruct.vectorArray[i].Z = i;
        }
        containArrayStruct.valueArray = new float[2];
        containArrayStruct.valueArray[0] = 1.234f;
        containArrayStruct.valueArray[1] = 5.678f;
        sendContainArrayStructFormCSToCPP(containArrayStruct);
    }

    // 传入运行时决定数组大小的结构体
    public static void SendContainArrayHasCountStructFormCSToCPP()
    {
        ContainArrayHasCount containArrayHasCount = new ContainArrayHasCount();
        float[] floatArray = new float[] { 1, 22, 333, 4444, 55555 };
        Vector3[] vectorArray = new Vector3[] { new Vector3(123, 123, 123), new Vector3(456, 456, 456), new Vector3(789, 789, 789) };
        containArrayHasCount.floatArrayCount = floatArray.Length;
        containArrayHasCount.vectorArrayCount = vectorArray.Length;
        GCHandle floatArrayHandle =  GCHandle.Alloc(floatArray,GCHandleType.Pinned);
        containArrayHasCount.floatArray = floatArrayHandle.AddrOfPinnedObject();
        GCHandle vectorArrayHandle = GCHandle.Alloc(vectorArray,GCHandleType.Pinned);
        containArrayHasCount.vectorArray = vectorArrayHandle.AddrOfPinnedObject();
        sendContainArrayHasCountStructFormCSToCPP(containArrayHasCount);
        floatArrayHandle.Free();
        vectorArrayHandle.Free();
    }

    // 接收从 C++ 返回的结构体
    public static void GetArrayStructFormCPP()
    {
        unsafe
        {
            IntPtr structIntPtr = getArrayStructFormCPP();
            ContainArrayHasCount containArrayHasCount = Marshal.PtrToStructure<ContainArrayHasCount>(structIntPtr);
            for (int i = 0; i < containArrayHasCount.floatArrayCount; i++)
            {
                IntPtr floatIntPtr = IntPtr.Add(containArrayHasCount.floatArray,i*Marshal.SizeOf<float>());
                float value = Marshal.PtrToStructure<float>(floatIntPtr);
                Console.WriteLine($"floatArray {i}:{value}");
            }

            for (int i = 0; i < containArrayHasCount.vectorArrayCount; i++)
            {
                IntPtr vectorIntPtr = IntPtr.Add(containArrayHasCount.vectorArray,i*Marshal.SizeOf<Vector3>());
                Vector3 vector = Marshal.PtrToStructure<Vector3>(vectorIntPtr);
                Console.WriteLine($"vectorArray {i}:{vector.X},{vector.Y},{vector.Z}");
            }

            Console.WriteLine($"floatArrayCount: {containArrayHasCount.floatArrayCount}");
            Console.WriteLine($"vectorArrayCount: {containArrayHasCount.vectorArrayCount}");
        }
    }
相关推荐
QAQ小菜鸟16 分钟前
一、初识C语言(1)
c语言
何曾参静谧31 分钟前
「C/C++」C/C++ 之 变量作用域详解
c语言·开发语言·c++
互联网打工人no135 分钟前
每日一题——第一百二十一题
c语言
AI街潜水的八角41 分钟前
基于C++的决策树C4.5机器学习算法(不调包)
c++·算法·决策树·机器学习
JSU_曾是此间年少1 小时前
数据结构——线性表与链表
数据结构·c++·算法
朱一头zcy2 小时前
C语言复习第9章 字符串/字符/内存函数
c语言
此生只爱蛋2 小时前
【手撕排序2】快速排序
c语言·c++·算法·排序算法
何曾参静谧2 小时前
「C/C++」C/C++ 指针篇 之 指针运算
c语言·开发语言·c++
Envyᥫᩣ3 小时前
C#语言:从入门到精通
开发语言·c#
lulu_gh_yu3 小时前
数据结构之排序补充
c语言·开发语言·数据结构·c++·学习·算法·排序算法