Unity C#结合C++动态链接库编程
在使用Unity做应用开发过程中,可能会需要使用一些仅支持C++的库,或者因为底层使用C++具有更高的效率优化,而选择使用C#结合C++的方式开发。Untiy为此也提供了链接C++动态链接库的接口,方便开发者使用。
本人开发环境为:Window10,Visual Studio,Unity。
一、最简单的动态链接库使用
使用Visual Studio生成动态链接库的方法有很多,在此就不再赘述了。
1.使用VS创建一个工程TestDLL4Unity
工程包含以下文件:
- Interface.h
#pragma once
#include "DLL_EXPORTS.h"
extern "C" DLL_EXPORT int Add(int a, int b);//注意extern "c" 是必要的,当需要的函数较多时,使用extern "c" { fun1,fun2,...}即可
- Interface.cpp
#include "Interface.h"
DLL_EXPORT int Add(int a, int b)//注意这里的DLL_EXPORT是必要的
{
return a + b;
}
- DLL_EXPORTS.h
#pragma once
#ifndef DLL_EXPORTS//需要在预处理器添加DLL_EXPORTS,以使用__declspec(dllexport)分支
#define DLL_EXPORT __declspec(dllimport)
#else
#define DLL_EXPORT __declspec(dllexport)
#endif
在生成动态链接库时,同时生成x86和x64两个版本,这样我们就得到了两个版本的TestDLL4Unity.dll。
2.创建Unity工程TestDLL
在Assets下新建一个文件夹Plugins,并在其中建立两个子文件夹x86和x86_64:
分别将之前使用VS生成的x86和x64的TestDLL4Unity.dll放置到这两个文件夹(其他文件诸如*.lib,*.pdb并不需要)
在使用时,我在改Unity工程下建立了两个文件,Interface.cs和UpdateUI.cs。其中Interface.cs用来管理动态链接库接口的导入,UpdateUI.cs使用这些接口。
- Interface.cs
using System.Runtime.InteropServices;
public class Interface
{
[DllImport("TestDLL4Unity")]//链接动态链接库,TestDLL4Unity为库名,不需要加dll后缀
public static extern int Add(int a, int b);//在导入动态链接库后,需要重新定义一个static extern的同名同参函数作为新接口
}
- UseInterface.cs
using UnityEngine;
public class UseInterface : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
int a = Interface.Add(2, 3);
print(a);
}
}
3.运行结果
可以在Unity的Console中看到运行结果:
二、复杂数据的传递
简单的动态链接库很好理解和实现,但在实际的开发过程中 ,在C++程序中可能会使用到更多的数据类型,比如类,结构体,以及vector,list等各种容器等等。如果我们直接使用在接口上,会遇到Unity提示布局错误,无法正确链接。
出现这些问题的原因是C++和C#中各种类型,即使相似,也无法相互转换,如C++中的string和list等。因此我们需要在C++程序动态链接库导出接口之前,将需要导出的数据完全转换为最基本的数据类型,如int,float,char等。也就是说,我们需要人为的建立一个中间层,用来转换C++和C#中的各种复杂数据结构。
1.修改VS工程文件
- TestClass.h
#pragma once
#include <vector>
#include <glm/glm.hpp>
class CTestClass
{
public:
CTestClass() = default;
~CTestClass() = default;
const glm::vec3& getPos();
void setPos(const glm::vec3& vPos);
private:
glm::vec3 m_Pos;//glm::vec3包含3个float
};
- TestClass.cpp
#include "TestClass.h"
//************************************************************************************
//Function:
const glm::vec3& CTestClass::getPos()
{
return m_Pos;
}
//************************************************************************************
//Function:
void CTestClass::setPos(const glm::vec3& vPos)
{
m_Pos = vPos;
}
- Interface.h
#pragma once
#include "DLL_EXPORTS.h"
#include "TestClass.h"
struct SVector3//用来将glm::vec3解析为仅包含最基本的float结构
{
float x;
float y;
float z;
};
extern "C"
{
DLL_EXPORT int Add(int a, int b);
DLL_EXPORT CTestClass* createTestClass();
DLL_EXPORT SVector3 getPos(CTestClass* vTestClass);
DLL_EXPORT void setPos(CTestClass* vTestClass, const SVector3& vPos);
}
- Interface.cpp
#include "Interface.h"
//************************************************************************************
//Function:
DLL_EXPORT int Add(int a, int b)
{
return a + b;
}
//************************************************************************************
//Function:
DLL_EXPORT CTestClass* createTestClass()
{
return new CTestClass();
}
//************************************************************************************
//Function:
DLL_EXPORT SVector3 getPos(CTestClass* vTestClass)
{
glm::vec3 Pos = vTestClass->getPos();
SVector3 v;//数据转换
v.x = Pos.x;
v.y = Pos.y;
v.z = Pos.z;
return v;
}
//************************************************************************************
//Function:
DLL_EXPORT void setPos(CTestClass* vTestClass, const SVector3& vPos)
{
vTestClass->setPos(glm::vec3(vPos.x, vPos.y, vPos.z));//数据转换
}
- DLL_EXPORTS.h 同上未作改变
注:麻烦之处就在于每次更新底层,都需要重新替换Unity工程中的dll文件。
2.修改Unity工程文件
将新生成的dll文件分别替换给Unity工程。然后在Interface.cs中添加新的接口
- Interface.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;
using System;
public struct SVector3//需要定义相同的仅包含最基本类型的结构体
{
public float x;
public float y;
public float z;
};
public class Interface
{
[DllImport("TestDLL4Unity")]
public static extern int Add(int a, int b);
[DllImport("TestDLL4Unity")]
public static extern IntPtr createTestClass();//添加新的接口,其中IntPtr被包含在System中,可以用来保存指针
[DllImport("TestDLL4Unity")]
public static extern SVector3 getPos(IntPtr vTestClass);
[DllImport("TestDLL4Unity")]
public static extern void setPos(IntPtr vTestClass, SVector3 vPos);
}
- UseInterface.cs
using UnityEngine;
using System;
public class UseInterface : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
int a = Interface.Add(2, 3);
print(a);
IntPtr TestClass = Interface.createTestClass();
SVector3 Pos = new SVector3();
Pos.x = 1;
Pos.y = 0;
Pos.z = 2;
Interface.setPos(TestClass, Pos);
SVector3 PosGet = Interface.getPos(TestClass);
Vector3 p = new Vector3(PosGet.x, PosGet.y, PosGet.z);
print(p);
}
}
3.运行结果
三、字符串传递需要注意
在使用动态链接库时,需要注意的是,如果从Unity C#向底层C++经参数传递的数据为char*类型时,如果使用以上这种方法,会发现,只传递了首字符到底层,并没有将字符数组中所有字符传递过去。
解决方法就是,在链接动态链接库时,需要指定使用CharSet.Ansi。
[DllImport("VPLGenerator_C", CharSet = CharSet.Ansi)]
public static extern void transportCharArray(char[] vCharArray);