Unity c#与c++混合编程
首先,Unity是基于Mono也就是.Net的运行环境的,所以它肯定支持C#;然后,Unity团队自行开发了一种Boo的语言;后面可能考虑到用户的接受程度的问题,又开发了类似JS的一种语言,但那绝对不是JS,勉强可以称之为UnityScript。这三种语言的代码最后都会被编译执行,而且可以互相访问。
在Unity游戏的开发当中,我们的游戏项目变得越来越复杂,以至于有些项目功能必须通过依赖库来进行实现。
比如,我们在手游开发中用到的GVoice、FMOD或者是其他的插件,都是通过调用Native dll(C/C++)来实现一些复杂的功能。《王者荣耀》核心代码libGameCore.so 也是用c++完成的。
那么我们应该如何使用C#来对C++进行调用呢。
了解C#的人都知道,C#是运行在CLR之上被托管的,而C++则并没有被托管。
目前.Net平台中托管环境调用非托管环境有三种方法:
- P/Invoke
- C++ Interop
- COM Interop
这三种方法当中,C++ Interop是针对托管C++使用的方法(说实话C++/CLI感觉没啥前途),COM Interop则是针对Window软件开发而采用的方式。所以我们只剩下一种解决方案:也就是PInvoke来进行托管环境与非托管环境的互操作。不过由于PInvoke本身内容也并不少,所以在这里我也就简单介绍一下其使用的方式,更详细的内容可以去查看官方文档,或者是下一个《精通.Net互操作》的pdf来阅读就可以了。
新手入门
如何做到c# 能调用到c++代码呢,下面我们通过一个小的Demo展示。
- 在Unity 新建一个c# 代码,里面内容如下:
public class TestCPP : MonoBehaviour {
#if UNITY_IPHONE || UNITY_XBOX360
[DllImport("__Internal")]
#else
[DllImport("GameCore")]
#endif
public static extern int iAdd(int x, int y);
public void OnGUI()
{
GUILayout.BeginVertical();
if (GUILayout.Button("Cal-Add"))
{
int i = iAdd(8, 7);
Debug.Log("8+7=" + i);
}
GUILayout.EndVertical();
}
}
Testcpp.cs挂在当前场景随意一个gameobject下,然后在vs中设置新建一个c++ 空项目,属性设置如下图:
配置类型:动态库(.dll),平台设置为x64
c++代码的实现如下:
#ifndef __Test__
#define __Test__
extern "C"
{
__declspec(dllexport) int iAdd(int a,int b)
{
return a+b;
}
};
#endif
导出dll,然后copy到unity项目Plugins/x86_x64目录下,点击unity运行按钮,点击GUI Add按钮,这时你可以看到一行log在console串口中,就证明你的c++调用成功了。类似下图:
断点调试
在你的c++ 调试(D)->附加到进程(P)… ,在弹出的窗口作如下选择:
点击附加,你就可以设置断点了。
如果你的vs工程是由cmakelist生成的, 且断点不能再unity中命中的话,可以检查c/c++里的调式信息格式选项有没有设置成下图所示的选项:
复杂的数据封送
指针传递
extern "C"
{
__declspec(dllexport) int iSub(int* a, int* b)
{
return *a - *b;
}
}
c#主要通过IntPtr去处理的
#if UNITY_IPHONE || UNITY_XBOX360
[DllImport("__Internal")]
#else
[DllImport("Core")]
#endif
public static extern int iSub(IntPtr x, IntPtr y);
public void OnGUI
{
if (GUI.Button(new Rect(20, 120, 100, 60), "Sub"))
{
int a = 8, b = 2;
IntPtr p1 = Marshal.AllocCoTaskMem(Marshal.SizeOf(a));
Marshal.StructureToPtr(a, p1, false);
IntPtr p2 = Marshal.AllocCoTaskMem(Marshal.SizeOf(b));
Marshal.StructureToPtr(b, p2, false);
int rst = iSub(p1, p2);
Debug.Log(a + "-" + b + "=" + rst);
}
}
c++反调用c# 使用指针函数(c++)
typedef bool(*SharpCALLBACK)(unsigned char,const char*);
__declspec(dllexport) void iInitCallbackCommand(SharpCALLBACK cb)
{
callback = cb;
}
public delegate void CppDelegate(byte type, IntPtr p);
#if UNITY_IPHONE || UNITY_XBOX360
[DllImport("__Internal")]
#else
[DllImport("GameCore")]
#endif
public static extern void iInitCallbackCommand(CppDelegate cb);
public static void Init()
{
iInitCallbackCommand(OnInitCallback);
}
[MonoPInvokeCallback(typeof(CppDelegate))]
static void OnInitCallback(byte t, IntPtr ptr)
{
string command = Marshal.PtrToStringAnsi(ptr);
XDebug.CLog(command);
}
传送结构体
struct Row
{
uint itemid;
char itemname[MaxStringSize];
int equippos;
int profession;
};
extern "C"
{
__declspec(dllexport) void iGetFashionListRow(Row* row);
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct RowData {
uint itemid;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
string itemname;
int equippos;
int profession;
}
#if UNITY_IPHONE || UNITY_XBOX360
[DllImport("__Internal")]
#else
[DllImport("GameCore")]
#endif
static extern void iGetFashionListRowByID(int idx, ref RowData row);
面向对象
通过上面测试我们可以看到出c#里都是静态函数,c++都写在extern c中,需要自己去获取对应的实例,这些都是面向过程的。而c++天然是面对对象的OO,那我们如何实现c#里去new一个c++对象,c# new的对象又是如何析构的呢?
C++互操作也有自动生成接口代码工具,这就是Swig。Swig可以根据不同的语言生成各个语言与C++交互的接口,包括C#、Java、Python、Ruby等等,你可以在swig官网下载最新版本的swig.
在c++项目里添加swig文件夹,添加Core.i ,类似图中所示:
右击Core.i, 设置项类型为自定义生成工具
点击右下角应用,刷新之后,在自定义工具设置命令行和输出,如图:
命令行配置如下:
这段代码的意思就是调用swig,-c++设置源语言为c++ -csharp代表输出语言为C#,最终的-outdir代表的是C#接口的输出目录,而最后的参数代表的是.cxx文件的输出目录。
然后我们对工程进行编译:
发现会生成C#文件以及.cxx文件,这个时候,我们将.cxx文件包含到工程当中, 如果没有包含到工程中,c#调用c++时候将会找不到方法,切记。
我们新建一个c++的类,其中实现代码如下:
#include "Invork.h"
int Invork:: Mul(int a,int b)
{
return a * b;
}
int Invork::Div(int a,int b)
{
if( b == 0 )
{
return 0;
}
else
{
return a/b;
}
}
编译生成之后会发现我们的c#目录会多出三个文件,如下图所示:
在c#里我们这可这样调用c++的东西:
如果最终你得到如图所示的日志,恭喜你,所有的流程都跑通了。
在c#里new c++里的对象,记得一定要Dispose(),否则的话,会造成内存泄漏,带来不必要的麻烦。上面的代码如果没有显示调用ins.Dispose() 可是可以的,ins在托管销毁的时候,swig已帮助我们在析构函数中调用Dispose(),但这样释放的不及时,并不是函数退出的时候就会释放。在生成的Invoik类的析构函数自动调用了Disposse()。最后说说swig的缺点,swig并不会帮助我们生成UNITY_IPHONE ios平台下外链调用,即DllImport 因为ios平台我们平常使用的都是静态库, 而不像Windows和Android平台一样是动态库。
Swig常用语法
详细的文档请参考官方文档,这里只做简单的一些介绍。
我在项目中写了一个简单的模板,用到了一些Swig的常用功能:
module
module代表的是当前.i模板所在的模块,相对应的,该.i文件也会生成相应的接口文件,命名就与%module声明的一样。所以该语法一般用在模板的开头。
include
就像C/C++一样,include会将需要生成接口的文件进行生成。是必不可少的语法。
大括号帮助我们在.cxx中加入一些代码,例如我们最常用的#include,这样我们才可以让.cxx调用到相应的代码。
使用C++/STL
我们可以通过包含各种swig所包含的.i文件来帮助我们实现STL库。
例如%incude “std_string.i”、%include “std_vector.i”
使用这样的定义方式,Swig会为我们生成一个名为BoolVector的类型而不是未知类型。我们可以在目标语言中创建C++中的STL并且与C++中的Vector进行互操作。
需要注意的是,如果我们使用自定义类型而非基本类型或者使用指针作为模板类型,我们则需要事先导出自定义类型的定义,否则就会得到SWIGTYPE_p_类型名这样定义作为类型模板的Vector定义,这往往不是我们想要的。
使用指针
定义指针的方法如下:
通过这个定义我们Swig会为我们生成指针相对应的类,Swig再会生成类似于SWIGTYPE_p_bool这样的未定义类型,而是直接使用BoolPointer,并且我们能够自己在目标语言中申请内存,并且自己对内存进行管理。
使用数组
定义数组的方法如下。
通过这种方式我们可以导出相应的数组类型。我们可以在目标语言中创建C++中的数组,并且与C++中的数组进行互操作。
关于本节使用的代码都已上传到github,欢迎下载