tolua#代码简要分析
2017-04-16 23:02 风恋残雪 阅读(4460) 评论(1) 编辑 收藏 举报简介
tolua#是Unity静态绑定lua的一个解决方案,它通过C#提供的反射信息分析代码并生成包装的类。它是一个用来简化在C#中集成lua的插件,可以自动生成用于在lua中访问Unity的绑定代码,并把C#中的常量、变量、函数、属性、类以及枚举暴露给lua。它是从cstolua衍变而来。从它的名字可以看出,它是集成了原来的tolua代码通过二次封装写了一个C#与tolua(c)的一个中间层。
All problems in computer science can be solved by another level of indirection, except of course for the problem of too many indirections
基础
要想了解tolua#是如何集成的我们需要对C#的一些特性有一些了解,比如了解C#与原生代码交互的方式等。,我们假设读者已经对lua和tolua++有一个比较熟悉,我们略过lua与c或者C++的交互方式,主要介绍一些C#的特性,来帮助我们接下来分析tolua#的集成原理。
C#特性
Attribute
Attribute 是一种可由用户自由定义的修饰符(Modifier),可以用来修饰各种需要被修饰的目标。特性Attribute 的作用是添加元数据。元数据可以被工具支持,比如:编译器用元数据来辅助编译,调试器用元数据来调试程序。Unity以及tolua#中就会用Attribute来辅助做一些事情。
值类型与引用类型
只所以要提这两个概念,是因为很好得理解这两个概念有助于我们写出比较高效的C#代码。
我们知道,C#中的每一种类型要么是值类型,要么是引用类型。所以每个对象要么是值类型的实例,要么是引用类型的实例。
引用类型和值类型都继承自System.Object类。不同的是,几乎所有的引用类型都直接从System.Object继承,而值类型则继承其子类,即直接继承System.ValueType。
作为所有类型的基类,System.Object提供了一组方法,这些方法在所有类型中都能找到,其中包含toString方法及clone等方法。
System.ValueType直接继承System.Object,即System.ValueType本身是一个类类型,而不是值类型;System.ValueType没有添加任何成员,但覆盖了所继承的一些方法,使其更适合于值类型。例如,ValueType重写了Equals()方法,从而对值类型按照实例的值来比较,而不是引用地址来比较。
简单了解了值类型与引用类型那么我们下面来看下C#中的装箱和拆箱的概念。
装箱和拆箱
装箱和拆箱是值类型和引用类型之间相互转换是要执行的操作。
1. 装箱在值类型向引用类型转换时发生
2. 拆箱在引用类型向值类型转换时发生
1 object objValue = 4; 2 3 int value = (int)objValue;
上面的两行代码会执行一次装箱操作将整形数字常量4装箱成引用类型object变量objValue;然后又执行一次拆箱操作,将存储到堆上的引用变量objValue存储到局部整形值类型变量value中。
同样我们需要看下IL代码:
1 .locals init ( 2 3 [0] object objValue, 4 5 [1] int32 'value' 6 7 ) //上面IL声明两个局部变量object类型的objValue和int32类型的value变量 8 9 IL_0000: nop 10 11 IL_0001: ldc.i4.4 //将整型数字4压入栈 12 13 IL_0002: box [mscorlib]System.Int32 //执行IL box指令,在内存堆中申请System.Int32类型需要的堆空间 14 15 IL_0007: stloc.0 //弹出堆栈上的变量,将它存储到索引为0的局部变量中 16 17 IL_0008: ldloc.0//将索引为0的局部变量(即objValue变量)压入栈 18 19 IL_0009: unbox.any [mscorlib]System.Int32 //执行IL 拆箱指令unbox.any 将引用类型object转换成System.Int32类型 20 21 IL_000e: stloc.1 //将栈上的数据存储到索引为1的局部变量即value
拆箱操作的执行过程和装箱操作过程正好相反,是将存储在堆上的引用类型值转换为值类型并给值类型变量。
C#调用原生代码
因为tolua#底层库是使用的tolua(c语言编写),那么就需要通过C#来调用原生代码,我们从LuaDll.cs中摘取一段代码来演示如何从C#中调用原生代码。
1 Public class LuaDll 2 3 { 4 5 [DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)] 6 7 public static extern void lua_close(IntPtr luaState); 8 9 }
其中LUADLL对应的字符串就是tolua,在不同的平台上mono会去加载对应的tolua.dll或者tolua.so等文件并调用对应的函数。具体可以参考mono官方的教程。
tolua#集成
tolua#集成主要分为两部分,一部分是运行时需要的代码包括一些手写的和自动生成的绑定代码,另一部分是编辑器相关代码,主要提供代码生成、编译lua文件等操作,具体就是Unity编辑器中提供的功能。用mono打开整个tolua#的工程,文件结构大体如下所示:
Runtime
Source
Generate 这个文件里面是生成的绑定代码
LuaConst.cs 这个文件是一些lua路径等配置文件。
ToLua
BaseLua 一些基础类型的绑定代码
Core 提供的一些核心功能,包括封装的LuaFunction LuaTable LuaThread LuaState LuaEvent、调用tolua原生代码等等。
Examples 示例
Misc 杂项,目前有LuaClient LuaCoroutine(协程) LuaLooper(用于tick) LuaResLoader(用于加载lua文件)
Reflection 反射相关
Editor
Editor
Custom
CustomSettings.cs 自定义配置文件,用于定义哪些类作为静态类型、哪些类需要导出、哪些附加委托需要导出等。
ToLua
Editor
Extend 扩展一些类的方法。
ToLuaExport.cs 真正生成lua绑定的代码
ToLuaMenu.cs Lua菜单上功能对应的代码
ToLuaTree.cs 辅助树结构
Generate All 流程
了解了tolua#的大致文件结构,下面我们来看下tolua#的Generate All 这个功能来分析下它的生成过程。生成绑定代码主要放在ToLuaExport.cs里面,我们并不会对每一个函数进行细致的讲解,如果有什么不了解的地方,可以直接看它的代码。
GenLuaDelegates函数
生成委托绑定的代码,它会从CustomSettings.customDelegateList里面取出所有自定义导出的委托列表,然后把CustomSettings.customTypeList里面的所有类型中的委托根据一定规则加入到list中,最后调用ToLuaExport.GenDelegates()方法来生成委托绑定的代码,生成的代码在DelegateFactory.cs文件中。
由于该函数比较简单,我们这里不做展开,可以直接查看ToLuaExport.cs中的GenDelegates()并配合DelegateFactory.cs来查看。
GenerateClassWraps 函数
遍历所有需要导出的类,然后调用ToLuaExport.Generate()方法来生成类的绑定代码。
下面我们来看下ToLuaExport.Generate()方法,其基本流程如下所示:
从上面的流程图我们可以看到,整个过程还是比较清楚的,如果这个类是枚举类型,那么它会调用枚举导出的接口,而如果这个类型是一个普通的类,那么它就会调用上图所示的相应的流程将代码导出。至于结构体类型,目前应该是只支持一些特定的结构体,需要在lua中对应一份实现(Assets\ToLua\Lua目录中),当然它生成的代码也有一些依赖于tolua#的核心运行时,我们前面简单的讲解了如何在编辑器中生成绑定代码,接下来我们讲一下它的核心运行时。
tolua#的核心运行时
tolua#的运行代码包含SourceàGenerate下面的绑定代码以及ToLuaàBaseType代码以及Core下面的核心代码。接下来我们着重讲一下Core下面的几个主要类。
LuaAttribute.cs
我们前面基础知识部分已经讲过,它在tolua#生成绑定代码时做一些标示使用。
LuaBaseRef.cs
Lua中对象对应C#中对象的一个基类,主要作用是有一个reference指向lua里面的对象,引用计数判断两个对象是否相等等。
比如LuaFunction里面的reference是指向lua里面的一个闭包的,而LuaTable的reference是指向lua中的一个table的。
LuaDll.cs
这个类的主要作用就是实现了C#调用原生代码的功能,其中的原理我们也在上面的基础章节提到了如何在C#中调用原生代码,这里我们就不展开去讲了。
LuaState.cs
这里面是对真正的lua_State的封装,包括初始化lua路径,加载相应的lua文件,注册我们前面生成的绑定代码以及各种辅助函数。
ObjectTranslator.cs
接下来,我们着重说一下这个ObjectTranslator这个类,这个类代码不多,它存在的主要意义就是给lua中对C#对象的交互提供了基础,简单来说就是C#中的对象在传给lua时并不是直接把对象暴露给了lua,而是在这个OjbectTranslator里面注册并返回一个索引(可以理解为windows编程中的句柄),并把这个索引包装成一个userdata传递给lua,并且设置元表。具体可以查看tolua_pushnewudata代码,如下所示:
1 // tolua# 代码 2 3 static void PushUserData(IntPtr L, object o, int reference) 4 5 { 6 7 int index; 8 9 ObjectTranslator translator = ObjectTranslator.Get(L); 10 11 if (translator.Getudata(o, out index)) 12 13 { 14 15 if (LuaDLL.tolua_pushudata(L, index)) 16 17 { 18 19 return; 20 21 } 22 23 translator.Destroyudata(index); 24 25 } 26 27 index = translator.AddObject(o); 28 29 LuaDLL.tolua_pushnewudata(L, reference, index); 30 31 } 32 33 // tolua++ 代码 34 35 LUALIB_API void tolua_pushnewudata(lua_State *L, int metaRef, int index) 36 37 { 38 39 lua_getref(L, LUA_RIDX_UBOX); 40 41 tolua_newudata(L, index); 42 43 lua_getref(L, metaRef); 44 45 lua_setmetatable(L, -2); 46 47 lua_pushvalue(L, -1); 48 49 lua_rawseti(L, -3, index); 50 51 lua_remove(L, -2); 52 53 }
而在lua需要通过上面传到lua里面的对象调用C#的方法时,它会调用ToLua.CheckObject或者ToLua.ToObject从ObjectTranslator获取真正的C#对象。下面我们把ToLua.ToObject的代码做个示例:
1 public static object ToObject(IntPtr L, int stackPos) 2 3 { 4 5 int udata = LuaDLL.tolua_rawnetobj(L, stackPos); 6 7 if (udata != -1) 8 9 { 10 11 ObjectTranslator translator = ObjectTranslator.Get(L); 12 13 return translator.GetObject(udata); 14 15 } 16 17 return null; 18 19 }
总结
通过对tolua#的简单分析,我们对tolua#是怎么实现lua与Unity交互有了一个基础的认识,如果想对tolua#有一个比较深入的了解,那么就需要我们仔细去研究代码、示例以及用它来实际地去做些东西。
因为看的时间不是很多,所以理解上难免有错误,如果发现问题还请指正。前段时间也完整实现了一套虚幻4中的使用lua框架,希望有时间的话也能跟大家分享一下,当然如果你有兴趣了解,也可以留言,这样我会尽量抽时间来把实现的具体细节跟大家分享一下。
作者: 风恋残雪
出处: http://www.cnblogs.com/ghl_carmack
关于作者:专注游戏引擎,关注VR,对操作系统、编译原理有深厚兴趣!
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接,否则保留追究法律责任的权利。