Unity中C#调用C++封装的dll,接收并传递结构体数组指针(类和结构体)

项目所需,需要在unity中调用C++封装的dll,需要传递结构体数组。C++中封装的函数的参数为结构体指针。在网上找了一下博客,然后结合MSDN上的C#编程参考手册,找到了方法。

DLL的创建

这里创建了一个.h文件,声明了很简单两个结构体和一个类,类中还声明了一个Vector6D的数组,没有使用更复杂的结构体。

//StructTest.h
#ifndef _STRUCTTEST_H_
#define _STRUCTTEST_H_
#include 
#include 
using namespace std;

//Vector3D结构体、指向Vector3D的指针、包含Vector3D结构体的结构体:Vector6D
typedef struct
{
	int v_X;
	int v_Y;
	int v_Z;
} Vector3D;
typedef struct
{
	Vector3D pos_X;
	Vector3D pos_Y;
	Vector3D pos_Z;
} Vector6D;
class StructTest
{
public:
	Vector3D v3;
	Vector6D v6;
	Vector6D v6_arr[3];
	StructTest();
	~StructTest();
	void setVector3D(int x, int y, int z);
	void setVector6D(Vector3D *v_3);        //传入的是Vector3D的指针,其实是一个数组
	void setVector6D_Arr(Vector6D *v_6);    //这里也一样
	bool saveVector();                      //这里将数据写入文件,可以比较直观的看到反馈
};
#endif // !_STRUCTTEST_H_

这里是对上面的定义(看着多,其实很简单)

//StructTest.cpp
#include "StructTest.h"
StructTest::StructTest()
{
	//初始化v3
	v3.v_X = rand() % 6;
	v3.v_Y = rand() % 6;
	v3.v_Z = rand() % 6;
	//初始化v6
	Vector3D temp3D;
	temp3D.v_X = rand() % 11 + 10;
	temp3D.v_Y = rand() % 11 + 10;
	temp3D.v_Z = rand() % 11 + 10;
	v6.pos_X = temp3D;
	temp3D.v_X = rand() % 11 + 10;
	temp3D.v_Y = rand() % 11 + 10;
	temp3D.v_Z = rand() % 11 + 10;
	v6.pos_Y = temp3D;
	temp3D.v_X = rand() % 11 + 10;
	temp3D.v_Y = rand() % 11 + 10;
	temp3D.v_Z = rand() % 11 + 10;
	v6.pos_Z = temp3D;
	//初始化v6_arr(数组)
	for (int i = 0; i < 3; i++)
	{
		Vector3D temp3D;
		Vector6D temp6D;
		temp3D.v_X = rand() % 11 + 20;
		temp3D.v_Y = rand() % 11 + 20;
		temp3D.v_Z = rand() % 11 + 20;
		temp6D.pos_X = temp3D;
		temp3D.v_X = rand() % 11 + 20;
		temp3D.v_Y = rand() % 11 + 20;
		temp3D.v_Z = rand() % 11 + 20;
		temp6D.pos_Y = temp3D;
		temp3D.v_X = rand() % 11 + 20;
		temp3D.v_Y = rand() % 11 + 20;
		temp3D.v_Z = rand() % 11 + 20;
		temp6D.pos_Z = temp3D;

		v6_arr[i] = temp6D;	
	}
}
StructTest::~StructTest()
{
}
void StructTest::setVector3D(int x, int y, int z)
{
	v3.v_X = x;
	v3.v_Y = y;
	v3.v_Z = z;
}
void StructTest::setVector6D(Vector3D *v_3)   //传Vector3D v_3[3]
{
	v6.pos_X = v_3[0];
	v6.pos_Y = v_3[1];
	v6.pos_Z = v_3[2];
}
void StructTest::setVector6D_Arr(Vector6D *v_6)   //传Vector6D v_6[3]
{
	v6_arr[0] = v_6[0];
	v6_arr[1] = v_6[1];
	v6_arr[2] = v_6[2];
}
bool StructTest::saveVector()
{
	ofstream ofs;
	ofs.open("Struct.txt", ios::app);  //用追加的方式打开文件,其他方式也没问题
	if (!ofs.is_open())
	{
		return false;
	}
	ofs << "This is Vector3D:" << endl;
	ofs << v3.v_X << endl;
	ofs << v3.v_Y << endl;
	ofs << v3.v_Z << endl;

	ofs << "This is Vector6D:" << endl;
	ofs << v6.pos_X.v_X << endl;
	ofs << v6.pos_X.v_Y << endl;
	ofs << v6.pos_X.v_Z << endl;
	ofs << v6.pos_Y.v_X << endl;
	ofs << v6.pos_Y.v_Y << endl;
	ofs << v6.pos_Y.v_Z << endl;
	ofs << v6.pos_Z.v_X << endl;
	ofs << v6.pos_Z.v_Y << endl;
	ofs << v6.pos_Z.v_Z << endl;

	ofs << "This is Vector6D Array:" << endl;
	for (int i = 0; i < 3; i++)
	{
		ofs << v6_arr[i].pos_X.v_X << endl;
		ofs << v6_arr[i].pos_X.v_Y << endl;
		ofs << v6_arr[i].pos_X.v_Z << endl;
		ofs << v6_arr[i].pos_Y.v_X << endl;
		ofs << v6_arr[i].pos_Y.v_Y << endl;
		ofs << v6_arr[i].pos_Y.v_Z << endl;
		ofs << v6_arr[i].pos_Z.v_X << endl;
		ofs << v6_arr[i].pos_Z.v_Y << endl;
		ofs << v6_arr[i].pos_Z.v_Z << endl;
	}
	ofs << "-----END-----" << endl;
	ofs.close();
	return true;
}

然后建立一个.cpp文件,用于定义Dll的封装函数。而且这里涉及到类的封装,所以这里采用“拆解”的方法去封装需要调用的类,就是把类的方法在类外用封装函数来调用。然后生成即可,注意我这里由于要放在Unity中,所以采用的是配置是:Release,平台是x64。这里生成和调用的配置与平台要一致。

#include "StructTest.h"
#define DLLEXPORT _declspec(dllexport)  //这个是关键,用此标识符声明的函数是表示从dll导出
//新建函数,使用这些函数来代替类的函数,但是类还是那个类
//将类的方法拆开来,用别的函数来调用,但是具体的实现还是类本身定义的方法实现

StructTest* st = nullptr;               //在dll中实例化一个类的对象
extern "C"                              //这个也是关键,我理解的是表示用C语言的方式编译
{
	//声明      为了规范,其实应该还需要声明一个函数用来释放指针和内存
	DLLEXPORT bool GenerateStruct();
	DLLEXPORT void setVector3D(int x, int y, int z);
	DLLEXPORT void setVector6D(Vector3D* v_3);
	DLLEXPORT void setVector6D_Arr(Vector6D* v_6);
	DLLEXPORT bool saveVector();
	//定义,可以放在extern “C”外
	DLLEXPORT bool GenerateStruct()
	{
		st = new StructTest();
		if (rs == nullptr)
		{
			return false;
		}
		return true;
	}
	DLLEXPORT void setVector3D(int x, int y, int z)
	{
		st->setVector3D(x, y, z);
	}
	DLLEXPORT void setVector6D(Vector3D* v_3)
	{
		st->setVector6D(v_3);
	}
	DLLEXPORT void setVector6D_Arr(Vector6D* v_6)
	{
		st->setVector6D_Arr(v_6);
	}
	DLLEXPORT bool saveVector()
	{
		return st->saveVector();
	}
}

Unity中C#脚本的创建

然后是Unity中,在Assets下创建一个目录“Scripts”用于存放C#脚本;再创建一个目录“Plugins”用于存放.dll文件,将刚刚生成的dll文件直接拖进来即可。
Unity中C#调用C++封装的dll,接收并传递结构体数组指针(类和结构体)_第1张图片
然后再这个C#文件TestDll_03.cs中:因为C#属于托管语言,C++是非托管,两者在交互时涉及一些底层的东西,尤其时对待字符类型数据的时候,涉及到编码方式之类的,所以要在托管内存和非托管内存之间进行交互需要用到一个类:Marshall,具体详细使用可以参考官方文档。
这几篇博客对我帮助很大:C#引用c++DLL结构体数组注意事项(数据发送与接收时)
C#调用C++DLL传递结构体数组的终极解决方案
C#结构体指针的定义及使用详解

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;   //这个是关键,想使用DLL就需要引进来
using System;

//使用函数来代替类的相关方法,类还是那个类
public class TestDll_03 : MonoBehaviour
{
    [StructLayout(LayoutKind.Sequential)]  //这个是关键,在官方文档中为StructLayoutAttribute,作用是允许你控制内存中类或结构体的数据字段的物理布局。
    public struct Vector3D
    {
        public int x, y, z;
    }
    [StructLayout(LayoutKind.Explicit)]  //这个的具体使用可以参考官方文档,不同参数表示不同的对齐方式
    public struct Vector6D               //当结构体含有字符串,字符数组等类型时,这里特别关键,采用不同的参数,来严格设置内存布局
    {
       [FieldOffset(0)] public Vector3D pos_X;
       [FieldOffset(12)] public Vector3D pos_Y;
       [FieldOffset(24)] public Vector3D pos_Z;
    }
    //声明需要从dll中导入的函数,我的dll名称为:TestStructure_03_02。这个DllImport的使用也有很多参数,在官方文档中可以查到
    [DllImport("TestStructure_03_02")]
    public static extern bool GenerateStruct();
    [DllImport("TestStructure_03_02")]
    public static extern void setVector3D(int x, int y, int z);
    [DllImport("TestStructure_03_02")]
    public static extern void setVector6D(IntPtr v_3);     //注意,我这里采用的时IntPtr,好像用普通指针*的时候说什么unsafe啥的我不太懂。
    [DllImport("TestStructure_03_02")]
    public static extern void setVector6D_Arr(IntPtr v_6);
    [DllImport("TestStructure_03_02")]
    public static extern bool saveVector();
    
    private bool st;                //用来表示创建一个StructTest是否成功
    
    void Start()                    //进行初始化
    {
        //初始化数组
        //定义数组元素个数
        int count = 3;
        //计算两个结构体的大小
        int size_3D = Marshal.SizeOf(typeof(Vector3D));
        int size_6D = Marshal.SizeOf(typeof(Vector6D));
        //用Marshal分配两个结构体数组空间(托管->非托管)
        //这里也是很关键的地方:Marshal类:提供了一个方法集合,这些方法用于分配非托管内存、
        //复制非托管内存块、将托管类型转换为非托管类型,
        //此外还提供了在与非托管代码交互时使用的其他杂项方法
        IntPtr ptr_3DArray = Marshal.AllocHGlobal(count * size_3D);
        IntPtr ptr_6DArray = Marshal.AllocHGlobal(count * size_6D);
        //创建两个结构体数组
        Vector3D[] v3Array = new Vector3D[count];
        Vector6D[] v6Array = new Vector6D[count];
        
        //初始化v3Array
        for(int i = 0; i < count; i++)
        {
            v3Array[i].x = 120 + (i + 1) * 5;
            v3Array[i].y = 130 + (i + 1) * 5;
            v3Array[i].z = 140 + (i + 1) * 5;
        }
        //初始化v6Array
        for (int i = 0; i < count; i++)
        {
            Vector3D[] temp3D = new Vector3D[count];
            for(int j = 0; j < count; j++)
            {
                temp3D[j].x = 150 + (j + i + 1) * 5;
                temp3D[j].y = 160 + (j + i + 1) * 5;
                temp3D[j].z = 170 + (j + i + 1) * 5;
            }
            v6Array[i].pos_X = temp3D[0];
            v6Array[i].pos_Y = temp3D[1];
            v6Array[i].pos_Z = temp3D[2];
        }

        //将初始化完毕的结构体数组v3Array传入开辟好的非托管内存块中(ptr_3DArray指向的)
        for (int i = 0; i < count; i++)
        {
            //这里要特别注意,因为是一块连续的内存空间,但是指针指向的只是开头,所以指针也要按照结构体大小递增dddd
            IntPtr tempPtr = (IntPtr)((UInt32)ptr_3DArray + i * size_3D);
            Marshal.StructureToPtr(v3Array[i], tempPtr, false);  //这个StructureToPtr很关键!作用是:将数据从托管对象(C#)封送到非托管内存块(C++)中,与之相对的是PtrToStructure。
        }
        //将初始化完毕的结构体数组v6Array传入开辟好的非托管内存块中(ptr_6DArray指向的)
        for (int i = 0; i < count; i++)
        {
            IntPtr tempPtr = (IntPtr)((UInt32)ptr_6DArray + i * size_6D);
            Marshal.StructureToPtr(v6Array[i], tempPtr, false);
        }

        //创建StructTest类,并进行初始化(DLL中进行的)
        this.st = GenerateStruct();
        if (st)
        {
            Debug.Log("StructTest创建成功");
        }
        else
        {
            Debug.Log("StructTest创建失败");
        }
        //保存数据至Struct.txt中
        bool save = saveVector();
        if (save)
        {
            Debug.Log("StructTest初始化数据保存成功");
        }
        else
        {
            Debug.Log("StructTest初始化数据保存失败");
        }
       
        //设置Vector3D,只是简单的传递int型数据
        setVector3D(120, 130, 140);
        //设置Vector6D,传递的是Vector3D结构体类型的数组的指针,这个指针现在指向的是Marshall开辟出来的非托管内存,存储着Vector3D结构体类型的数组
        setVector6D(ptr_3DArray);
        //设置Vector6D_Arr,传递的是Vector6D结构体类型的数组的指针,这个指针现在指向的是Marshall开辟出来的非托管内存,存储着Vector6D结构体类型的数组
        setVector6D_Arr(ptr_6DArray);
        save = saveVector();
        if (save)
        {
            Debug.Log("StructTest修改后数据保存成功");
        }
        else
        {
            Debug.Log("StructTest修改后数据保存失败");
        }
		//记得释放内存啊
        Marshal.FreeHGlobal(ptr_6DArray);
        Marshal.FreeHGlobal(ptr_3DArray);
    }
    void Update()
    {
        
    }
}
//这里还提一嘴,我这里只涉及到从C#中传结构体数组指针到DLL中去,还有一种是接受DLL中的结构体数组,这里不同的就是要使用与上面StructureToPtr相对的PtrToStructure,作用是:将数据从非托管内存块封送到托管对象。

//定义完结构体VGAStat entries后,就可将接收到的C#结构体指针(用Marshall分配出来的)转换为定义的结构体对象。
VGAStat entries = (VGAStat)Marshal.PtrToStructure(iptr, typeof(VGAStat));  
//iptr为接收到的非托管的结构体指针。

脚本创建完后,将其拖到一个object上,然后点击运行即可。最后可以在Unity项目的文件夹下发现Struct.txt文件:
Unity中C#调用C++封装的dll,接收并传递结构体数组指针(类和结构体)_第2张图片

你可能感兴趣的:(c++,unity,c#,dll)