Lua

Lua热更新框架差异

方案选型考量方向

Posted by Bob on July 30, 2018

闲谈国产游戏脚本渊源

端游时代一款《天龙八部》就支撑了整个畅游公司。它引擎改造于orge+cegui。当c++代码工程随着项目业务日益庞大时,编译速度就实在很令人发狂。而C++对于开发人员要求还是蛮高的,驾驭不好给项目稳定性带来很大风险。再则为了更好分离引擎和业务逻辑,所以引入Lua做脚本语言作了部分业务逻辑开发。包括《魔兽世界》、《大话西游2》也引入Lua脚本。

早期页游大多基于Js+html+css开发,以Mop的《猫游记》最为代表,然而这种开发模式在浏览器兼容性和脚本执行效率、Dom效率等诸多问题上难以支撑重量产品。而后Flash As3发布,高效的脚本执行效率和统一的浏览器标准迎来页游黄金时代,mmorpg明星产品《神仙道》,SNS游戏《偷菜》风靡大江南北。这二种页游开发模式都是基于web部署,就无需考虑引入其他脚本做热更模块。

移动浪潮迎来手游时代,对于iOS游戏而言,app store的审核周期漫长无比,再加上IOS不允许动态下发可执行代码(不支持JIT的硬件环境)。就必须引入脚本做bug修复和一些需要及时发布的业务模块。而Android应用发布平台混乱,本身设计机制上就可以相对自由的替换so、dex、dll达到热更目的。在游戏App领域Cocos引擎,引入js和lua热更方案。其他传统App热更就百花齐放,如被封杀闹得沸沸扬扬的JsPatch。还有大量线上环境验证过的阿里AndFix和微信Tinker

手游时代还有一个不能忽略的是Html5游戏,国外引擎有Three.jsPixi.jsPlayCanvas,这些国外引擎工具链不太完善,设计理念并不太符合做国产游戏开发模式。而国内做的比较完善的引擎有Cocos2d-JSEgretLayabox。这些主流的引擎还是运行在JavaScript上。渲染模式部分采用Canvas模式。也有采用webGL(JS版的OpenGL)这种性能更好的模式,而且浏览器对它支持也越来越普及。由于手机浏览器性能差异、接口缺陷、SDK接入、系统级调用等诸多问题。大多数H5游戏发布还是绑在特定的runtime上,如发布在微信小游戏平台(WebGL)、发布原生包。正因本质还是web机制,所以做热更还是很容易的。

最后说说WebAssembly,它是一种二进制格式的类汇编代码,可以被浏览器加载和并进一步编译成可执行的机器码,从而在浏览器运行。它还可以作为高级语言的编译目标,理论上任何语言都可以编译为 WebAssembly。它接近 native code 比 JS 快这是显然的,作为浏览器四大巨头google、apple、firefox、microsoft合作共谋的产物,前途一片光明。目前支持的游戏引擎有 Egret。而Unity在发布WebGl时也可以选择Linker Target为WebAssembly。

常见的Lua for Unity3d框架

Lua Interface是一个用于Lua语言和Microsoft .NET的公共语言运行时(CLR)之间集成的库,LuaInterface的作者后来开发出跨平台升级版本NLua。最早一批热更新框架Ulua(首发时间2014年3月,后期停止维护。目前另有人保留了一个分支版本ulua2)就是基于Lua Interface开发的,Tolua(git首发时间2014年的9月,最早叫CsTolua)又是从ulua演化来的,再后来的Slua(2015年初)、Xlua(2015年3月)可以说都是前辈思想的变种者。

还有ILRuntime采用C#热更,以后单做分析。

性能测评差异

这些Lua框架目前本质上都是静态绑定代码里调用Lua CAPI来实现C#、Lua、C交互,理论上没有巨大性能差异,但网上性能测评都是体现出了不少差异。差异主要体现在下面二个方面:

  • Lua 的 C API 的调用频次。
  • C#、Lua、C、C++之间数据(值类型、引用类型)传递的实现方式和选择,以及数据交换产生的GC问题。

分析过程需对Lua虚拟机Lua虚拟栈概念有一定了解。

早期Lua框架调用C#采用反射实现(因为Lua Interface就是反射实现),会有性能损耗(几个框架测评相互嘲讽的点),目前大多数框架都是采用Wrap方式生成胶水代码,因此这个性能差异基本抹平了。

这些测评用例基本测试方向是对Unity独有类型Vector2, Vector3,Vector4,Quaternion)使用、对象读写、相互函数调用、复杂类型传递等,下面就这些情况的实现方式分别讨论。

Xlua v2.1.12
Unity独有类型使用

1.构造Vector3默认的映射方案是Vector3 -> userdata,userdata(size=12)比table(64位空table size ~= 80)更省内存,但操作字段比table性能稍低。 因经过了C#直接操作内存,所以性能略有损失。其他Unity类型Color、Quaternion等都是采用这种方案。

核心Lua CAPI调用过程如下:

lua_tonumber:lua虚拟栈3次出栈拿到xyz
xlua_pushstruct:构造userdata
xlua_pack_float3: 写入到数据到userdata

下列代码是完整调用过程:

--lua端
CS.UnityEngine.Vector3(1998, 2010, 1984)
// UnityEngineVector3Wrap.cs Vector3构造函数
static int __CreateInstance(RealStatePtr L){
    ...
    float _x = (float)LuaAPI.lua_tonumber(L, 2);
    float _y = (float)LuaAPI.lua_tonumber(L, 3);
    float _z = (float)LuaAPI.lua_tonumber(L, 4);
                        
    UnityEngine.Vector3 gen_ret = new UnityEngine.Vector3(_x, _y, _z);
    translator.PushUnityEngineVector3(L, gen_ret);
    ...
}

//WrapPusher.cs  userdata
 void PushUnityEngineVector3(RealStatePtr L, UnityEngine.Vector3 val){
    IntPtr buff = LuaAPI.xlua_pushstruct(L, 12, UnityEngineVector3_TypeID);
    CopyByValue.Pack(buff, 0, val)
}

//PackUnPack.cs userdata
bool Pack(IntPtr buff, int offset, UnityEngine.Vector3 field){
    if(!LuaAPI.xlua_pack_float3(buff, offset, field.x, field.y, field.z)){
        return false;
    }     
    return true;
}
/*xlua.c*/
typedef struct {
	int fake_id;
    unsigned int len;
	char data[1];
} CSharpStruct;

LUA_API void *xlua_pushstruct(lua_State *L, unsigned int size, int meta_ref) {
	CSharpStruct *css = (CSharpStruct *)lua_newuserdata(L, size + sizeof(int) + sizeof(unsigned int));
	css->fake_id = -1;
	css->len = size;
    lua_rawgeti(L, LUA_REGISTRYINDEX, meta_ref);
	lua_setmetatable(L, -2);
	return css;
}

LUALIB_API int xlua_pack_float3(void *p, int offset, float f1, float f2, float f3) {
	CSharpStruct *css = (CSharpStruct *)p;
	if (css->fake_id != -1 || css->len < offset + sizeof(float) * 3) {
		return 0;
	} else {
		float *pos = (float *)(&(css->data[0]) + offset);
		pos[0] = f1;
		pos[1] = f2;
		pos[2] = f3;
		return 1;
	}
}

2.lua里写入一个值到C# Vector3字段x,代码看出操作了userdata

核心Lua CAPI调用过程如下:

lua_tonumber:lua虚拟栈1次出栈拿到x值
Vector3.x = value:更新x值
lua_touserdata:取userdata
xlua_pack_float3: 更新到数据到userdata(实际xyz都写入了)

下列代码是完整调用过程:

static int _s_set_x(RealStatePtr L){
    ...   
    UnityEngine.Vector3 gen_to_be_invoked;
    translator.Get(L, 1, out gen_to_be_invoked);
    gen_to_be_invoked.x = (float)LuaAPI.lua_tonumber(L, 2);      
    translator.UpdateUnityEngineVector3(L, 1, gen_to_be_invoked);
    ...  
}

//WrapPusher.cs  userdata
void UpdateUnityEngineVector3(RealStatePtr L, int index, UnityEngine.Vector3 val){
    IntPtr buff = LuaAPI.lua_touserdata(L, index);
    CopyByValue.Pack(buff, 0,  val);
}

//PackUnPack.cs  userdata
bool Pack(IntPtr buff, int offset, UnityEngine.Vector3 field){
    if(!LuaAPI.xlua_pack_float3(buff, offset, field.x, field.y, field.z)){
        return false;
    }     
    return true;
}

3.将Vector3写入transform到position有二种方式:传LUA_TUSERDATA或者LUA_TTABLE

核心Lua CAPI调用过程如下:

lua_type:判断类型

//type为userdata
lua_touserdata:取userdata
xlua_unpack_float3: 读userdata中xyz

//type为table
{
xlua_pushasciistring:入栈FieldName:x
lua_rawget:通过FieldName找table中x的值并入虚拟栈
lua_tonumber:出栈拿到x值
}*3次

下列代码是完整调用过程:

--lua端
transform.position = CS.UnityEngine.Vector3(1, 2, 3) --userdata
transform.position = {x = 2008, y = 8, z = 8}  --table
// UnityEngineVector3Wrap.cs
static int _s_set_position(RealStatePtr L){
    UnityEngine.Transform gen_to_be_invoked = (UnityEngine.Transform)translator.FastGetCSObj(L, 1);
    UnityEngine.Vector3 gen_value;
    translator.Get(L, 2, out gen_value);
    gen_to_be_invoked.position = gen_value;
}

//WrapPusher.cs 判断是userdata还是table
public void Get(RealStatePtr L, int index, out UnityEngine.Vector3 val)
{
    LuaTypes type = LuaAPI.lua_type(L, index);
    if (type == LuaTypes.LUA_TUSERDATA )
    {
	    if (LuaAPI.xlua_gettypeid(L, index) != UnityEngineVector3_TypeID)
		{
		    throw new Exception("invalid userdata for UnityEngine.Vector3");
		}
		
        IntPtr buff = LuaAPI.lua_touserdata(L, index);
        if (!CopyByValue.UnPack(buff, 0, out val))
        {
            throw new Exception("unpack fail for UnityEngine.Vector3");
        }
    }
    else if (type ==LuaTypes.LUA_TTABLE)
    {
	    CopyByValue.UnPack(this, L, index, out val);
    }
    else
    {
        val = (UnityEngine.Vector3)objectCasters.GetCaster(typeof(UnityEngine.Vector3))(L, index, null);
    }
}

//userdata
 public static bool UnPack(IntPtr buff, int offset, out UnityEngine.Vector3 field)
 {
     field = default(UnityEngine.Vector3);
    
     float x = default(float);
     float y = default(float);
     float z = default(float);
    
     if(!LuaAPI.xlua_unpack_float3(buff, offset, out x, out y, out z))
     {
         return false;
     }
     field.x = x;
     field.y = y;
     field.z = z;
    
    
     return true;
 }

//table
public static void UnPack(ObjectTranslator translator, RealStatePtr L, int idx, out UnityEngine.Vector3 val){
    val = new UnityEngine.Vector3();
    int top = LuaAPI.lua_gettop(L);

    if (Utils.LoadField(L, idx, "x")){
        translator.Get(L, top + 1, out val.x);
    }
    LuaAPI.lua_pop(L, 1);

    if (Utils.LoadField(L, idx, "y")){
        translator.Get(L, top + 1, out val.y);
    }
    LuaAPI.lua_pop(L, 1);

    if (Utils.LoadField(L, idx, "z")){
        translator.Get(L, top + 1, out val.z);
    }
    LuaAPI.lua_pop(L, 1);	
}

//table
public static bool LoadField(RealStatePtr L, int idx, string field_name)
{
    idx = idx > 0 ? idx : LuaAPI.lua_gettop(L) + idx + 1;// abs of index
    LuaAPI.xlua_pushasciistring(L, field_name);
    LuaAPI.lua_rawget(L, idx);
    return !LuaAPI.lua_isnil(L, -1);
}

/*userdata*/
LUALIB_API int xlua_unpack_float3(void *p, int offset, float *f1, float *f2, float *f3) {
	CSharpStruct *css = (CSharpStruct *)p;
	if (css->fake_id != -1 || css->len < offset + sizeof(float) * 3) {
		return 0;
	} else {
		float *pos = (float *)(&(css->data[0]) + offset);
		*f1 = pos[0];
		*f2 = pos[1];
		*f3 = pos[2];
		return 1;
	}
}

4.也可以将自定义的C# struct映射到lua table或者userdata 。

自定义struct规则: a.含无参构造函数 b.只包含值类型 c.可以嵌套其它只包含值类型的struct

//ReImplementInLua.cs--table
[GCOptimize(OptimizeFlag.PackAsTable)]
public struct PushAsTableStruct
{
    public int x;
    public int y;
}

//NoGc.cs -- userdata
[GCOptimize]
[LuaCallCSharp]
 public struct MyStruct
{
    public MyStruct(int p1, int p2)
    {
        a = p1;
        b = p2;
        c = p2;
        e.c = (byte)p1;
    }
    public int a;
    public int b;
    public decimal c;
    public Pedding e;
}

5.xlua.genaccessor支持lua使用C#类型直接在lua侧完成,而且省掉了wrap代码以达成省text段的效果。 xlua.genaccessor不经过C#直接操作内存,效率应该是最高的。

--具体事例参见ReImplementInLua.cs
local get_x, set_x = xlua.genaccessor(0, 8)
/*xlua.c*/

static const luaL_Reg xlualib[] = {
	{"sethook", profiler_set_hook},
	{"genaccessor", gen_css_access},
	{"structclone", css_clone},
	{NULL, NULL}
};

LUA_API int gen_css_access(lua_State *L) {
	int offset = xlua_tointeger(L, 1);
	int type = xlua_tointeger(L, 2);
	if (offset < 0) {
		return luaL_error(L, "offset must larger than 0");
	}
	if (type < T_INT8 || type > T_DOUBLE) {
		return luaL_error(L, "unknow tag[%d]", type);
	}
	lua_pushvalue(L, 1);
	lua_pushcclosure(L, direct_getters[type], 1);
	lua_pushvalue(L, 1);
	lua_pushcclosure(L, direct_setters[type], 1);
	lua_pushcclosure(L, nop, 0);
	return 3;
}
对象获取

C#侧ObjectPool(数组)保存的是id<->c# object映射, Lua侧用userdata建立和C#关系,GC过程都是基于这个模型构建。

--lua
local gobject = CS.UnityEngine.GameObject.Find('helloworld')
gobject.transform
//UnityEngineGameObjectWrap.cs
 static int _g_get_transform(RealStatePtr L){
    try {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        //通过id(udata)在ObjectTranslator.objects(对象池Array)取c#对象,有boxing(装箱)行为
        UnityEngine.GameObject gen_to_be_invoked = (UnityEngine.GameObject)translator.FastGetCSObj(L, 1);
        //Push做了二步工作:
        //1.把transform push到objects中
        //2.transform对象id通过xlua_pushcsobj push到userdata
        translator.Push(L, gen_to_be_invoked.transform);
    } catch(System.Exception gen_e) {
        return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
    }
    return 1;
}
//ObjectTranslator.cs
internal object FastGetCSObj(RealStatePtr L,int index){
    return getCsObj(L, index, LuaAPI.xlua_tocsobj_fast(L,index));
}

/*xlua.c*/
/*通过userdata拿id*/
LUA_API int xlua_tocsobj_fast (lua_State *L,int index) {
	int *udata = (int *)lua_touserdata (L,index);

	if(udata!=NULL) 
		return *udata;
	return -1;
}

LUA_API void xlua_pushcsobj(lua_State *L, int key, int meta_ref, int need_cache, int cache_ref) {
	int* pointer = (int*)lua_newuserdata(L, sizeof(int));
	*pointer = key;
	
	if (need_cache) cacheud(L, key, cache_ref);

    lua_rawgeti(L, LUA_REGISTRYINDEX, meta_ref);//t[n] 的值压栈,t 是指索引LUA_REGISTRYINDEX的表

	lua_setmetatable(L, -2);
}

Lua调用CSharp函数

先包装一个LuaCSFunction函数,再通过Utils.RegisterFunc来注册C#函数, 包装函数必须打[MonoPInvokeCallbackAttribute]标签,为什么打标签参见Unity文档Mono文档

因为注册函数只需要消耗一次,其性能瓶颈主要就在c#函数的参数出栈(xlua_toXXX)和返回值的入栈(xlua_pushXXX)上


--lua
xxx:TestFunc(2008)

//c#函数
public void TestFunc(int i)
{
}

//按规则包装一个函数给C 
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate int lua_CSFunction(IntPtr L);
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int _m_TestFunc(RealStatePtr L)
 {
    ....
    //函数参数:出栈取值   
    int _i = LuaAPI.xlua_tointeger(L, 2);
    //c#函数调用
    gen_to_be_invoked.TestFunc( _i );
    //返回值处理
    ...
    //返回值个数
    return 0;
    ....
 }

//注册函数,这样非托管代码就可以反向调用托管代码了
LuaAPI.xlua_pushasciistring(L, name);
IntPtr fn = Marshal.GetFunctionPointerForDelegate(function);
/*lua_pushcfunction*/
xlua_push_csharp_function(L, fn, n);
LuaAPI.lua_rawset(L, idx);

CSharp调用Lua

1.读G表数据

--lua
money = 99999
//c#
luaenv.Global.Get<int>("money")

在LuaTable.Get主要做了如下LuaCAPI工作:

lua_pushstring(L,"money")
lua_gettable(L,-2)
xlua_tointeger(L,-1);

2.C#调Lua函数是把Lua函数映射到delegate的模式然后调用 这种模式因为函数参数和返回值类型双方都明确,从而避免了Boxing。


--lua
function f(a, b)
    print('a', a, 'b', b)
    return 1, {f1 = 1024}
end

//用例CSCallLua.cs
[CSharpCallLua]
public delegate int FDelegate(int a, string b, out DClass c);

//在DelegatesGensBridge.cs里生成映射参数关系,调用大致做下面三个步骤
public int __Gen_Delegate_Imp13(int p0, string p1, out CSCallLua.DClass p2)
{
....
LuaAPI.xlua_pushXXX //参数处理
LuaAPI.lua_pcall //lua函数执行
LuaAPI.xlua_toXXX //返回值处理
....
}

Slua v1.6.0
Unity独有类型使用

1.Vector3映射为luatable。

核心Lua CAPI调用过程如下:

luaL_checknumber //xlua直接用的luaL_tonumber
LuaDLL.luaS_pushVector3(l, v3.x, v3.y, v3.z)//构建table和写入值一次性全在c端做
--lua
import "UnityEngine"
Vector3(10,10,10)
//Lua_UnityEngine_Vector3.cs
static public int constructor(IntPtr l) {
    ...
    System.Single a1;
    checkType(l,2,out a1);
    System.Single a2;
    checkType(l,3,out a2);
    System.Single a3;
    checkType(l,4,out a3);
    o=new UnityEngine.Vector3(a1,a2,a3);
    pushValue(l,true);//lua_pushboolean 
    pushValue(l,o);//luaS_pushVector3
    ...
}

/*slua.c*/
LUA_API void luaS_pushVector3(lua_State *L, float x, float y, float z) {
	lua_newtable(L);
	lua_pushnumber(L, x);
	lua_rawseti(L, -2, 1);
	lua_pushnumber(L, y);
	lua_rawseti(L, -2, 2);
	lua_pushnumber(L, z);
	lua_rawseti(L, -2, 3);
	setmetatable(L, -2, MT_VEC3);
}

2.设置Vector3.x

整个赋值操作基本就是对lua table的处理

核心Lua CAPI调用过程如下:

luaL_checknumber //xlua直接用的luaL_tonumber
LuaDLL.luaS_checkVector3(l, p, out x, out y, out z) //取table值构造Vector3
Vector3.x = value:更新x值
LuaDLL.luaS_setDataVec(l, 1, v.x, v.y, v.z, float.NaN)//更新到数据到table

/*slua.c*/
LUA_API int luaS_checkVector3(lua_State *L, int p, float* x, float *y, float *z) {
	p=lua_absindex(L,p);
	if(lua_type(L,p)!=LUA_TTABLE)
		return -1;
	luaL_checktype(L, p, LUA_TTABLE);
	lua_rawgeti(L, p, 1);
	*x = (float)lua_tonumber(L, -1);
	lua_rawgeti(L, p, 2);
	*y = (float)lua_tonumber(L, -1);
	lua_rawgeti(L, p, 3);
	*z = (float)lua_tonumber(L, -1);
	lua_pop(L, 3);
	return 0;
}

LUA_API void luaS_setDataVec(lua_State *L, int p, float x, float y, float z, float w) {
	p=lua_absindex(L,p);
	setelementid(L, p, x, 1);
	setelementid(L, p, y, 2);
	setelementid(L, p, z, 3);
	setelementid(L, p, w, 4);
}

3.给transform.position赋值

相比xlua映射lua table的处理,slua整个取值过程(luaS_checkVector3)基本合并到C端处理了,性能更好一点。

核心Lua CAPI调用过程如下:

LuaDLL.luaS_checkVector3(l, p, out x, out y, out z) //取table值构造Vector3
self.position=v //c#端更新
--lua端
transform.position = Vector3(1, 2, 3) --table

4.复杂类型 slua只是对特定Unity类型,做了table映射处理,lua capi都在C侧做的批处理优化。xlua提供userdata和table二种选择, 而且xlua.genaccessor做的更通用,几乎完全在C侧完成操作。slua和xlua都提供Struct映射机制解决boxing问题。

对象获取

C#侧ObjectCache(Dictionary)保存的是IntPtr<->c# object映射, Lua侧用userdata建立和C#关系。 C#侧对象必须在lua侧gc后才解引用,最后Gc。

xlua在lua侧使用lua_rawgeti可以看出table索引是int。 slua在C#侧拿getAQName(xx)做key是String传到lua侧,元表处理用luaL_getmetatable(L,keyStr)。 这里就体现出差异了。

--lua
gobject.transform
static public int get_transform(IntPtr l) {
    UnityEngine.GameObject self=(UnityEngine.GameObject)checkSelf(l);//c#映射表取
    pushValue(l,true);
    /*
    1.push到c#映射表 
    2.luaS_pushobject push映射到lua侧userdata
    */
    pushValue(l,self.transform);
    return 2;
}

/*slua.c*/
LUA_API int luaS_pushobject(lua_State *l, int index, const char* t, int gco, int cref) {

	int is_reflect = 0;

	luaS_newuserdata(l, index);
	if (gco) cacheud(l, index, cref);


	luaL_getmetatable(l, t);//t对应的原表入栈
	if (lua_isnil(l, -1))
	{
		lua_pop(l, 1);
		luaL_getmetatable(l, "LuaVarObject");
		is_reflect = 1;
	}

	lua_setmetatable(l, -2);
	return is_reflect;
}
函数调用

lua函数调用c#并回调,参数生成代码做了映射关系。

--lua
Deleg.testAction( self.action )
function self.action(a,b)
	print("callback from action")
	print(a,b)
end
//Deleg.cs
public static void testAction(Action<int, string> f)
{
	f(1998, "bob");
}

Slua(2016年版本)c#调用lua,本质是调用包装的LuaFunction,它在call参数处理上就有boxing性能问题。

--lua
appMain:SetQuitCallback(function() self:OnApplicationQuit() end)
function luaApp:OnApplicationQuit()
    -- do something
end

function foo(a,b,c)
	return a,b,c,"slua"
end
//
private SLua.LuaFunction _AppQuitCallback;
public void SetQuitCallback(SLua.LuaFunction func)
{
	_AppQuitCallback = func;
}

_AppQuitCallback.call();


LuaSvr.mainState.getFunction("foo").call(1, 2, 3)

Slua新的版本做了改进,可以CustomLuaClass标记导出对应delegate,调用luafunction.cast转化对应的delegate,以避免gc开销.

[CustomLuaClass]
public delegate void GetMoneyDelegate(int x,int y);
GetMoneyDelegate ud;

LuaFunction getFunction;
getMoney = getFunction.cast<GetMoneyDelegate>();//实现不够简练
if (getMoney != null) getMoney(1,2);
Tolua v1.0.7.392
Unity类型使用

ToLua对于Lua CAPI调用优化点在于做了很多批处理,并对Unity很多类型做了Lua侧实现. 性能测试用例如果是测这些类型就太占优势了。但Tolua做的太过耦合,导致新增加一种自定义类型就必须 硬编码很多过程。有Xlua在这一点上做的更通用一些。在lua和C#相互持的处理上和Xlua方式一样,ObjectTranslator用index(int)持有c#对象 传给lua侧用userdata持有c#对象。

--具体代码参见ToLua\Lua\UnityEngine\Vector3.lua

function Vector3.New(x, y, z)				
	local t = {x = x or 0, y = y or 0, z = z or 0}
	setmetatable(t, Vector3)						
	return t
end

--调用
local v = Vector3.New(19,98,10)

[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int set_position(IntPtr L)
{
	object o = null;
	try
	{
		o = ToLua.ToObject(L, 1);
		UnityEngine.Transform obj = (UnityEngine.Transform)o;
		UnityEngine.Vector3 arg0 = ToLua.ToVector3(L, 2);
		obj.position = arg0;
		return 0;
	}
	catch(Exception e)
	{
		return LuaDLL.toluaL_exception(L, e, o, "attempt to index position on a nil value");
	}
}

public static Vector3 ToVector3(IntPtr L, int stackPos)
{
    float x = 0, y = 0, z = 0;
    LuaDLL.tolua_getvec3(L, stackPos, out x, out y, out z);
    return new Vector3(x, y, z);
}


void tolua_openluavec3(lua_State *L)
{    
	lua_getglobal(L, "Vector3");

    if (!lua_istable(L, 1))
    {        
        luaL_error(L, "Vector3 does not exist or not be loaded");
        return;
    }

	lua_pushstring(L, "New");
	lua_rawget(L, -2);
	lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_PACKVEC3);	
	lua_pushstring(L, "Get");
	lua_rawget(L, -2);
	lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_UNPACKVEC3);	
	lua_pop(L, 1);
}

LUALIB_API void tolua_getvec3(lua_State *L, int pos, float* x, float* y, float* z)
{
	lua_getref(L, LUA_RIDX_UNPACKVEC3);
	lua_pushvalue(L, pos);
	lua_call(L, 1, 3);
    *x = (float)lua_tonumber(L, -3);
    *y = (float)lua_tonumber(L, -2);
    *z = (float)lua_tonumber(L, -1);
    lua_pop(L, 3);
}

LUALIB_API void tolua_pushvec3(lua_State *L, float x, float y, float z)
{
	lua_getref(L, LUA_RIDX_PACKVEC3);
	lua_pushnumber(L, x);
	lua_pushnumber(L, y);
	lua_pushnumber(L, z);
	lua_call(L, 3, 1);
}

C#调用Lua函数
//CallLuaFunction.cs
function luaFunc(num)                        
    return num + 1
end

test = {}
test.luaFunc = luaFunc

//映射了函数参数和返回值类型
luaFunc = lua.GetFunction("test.luaFunc");
int num = luaFunc.Invoke<int, int>(123456);
public R1 Invoke<T1, R1>(T1 arg1)
{
    BeginPCall();
    PushGeneric(arg1);//通用push
    PCall();
    R1 ret1 = CheckValue<R1>();
    EndPCall();
    return ret1;
}

//c# 直接硬编码方式调用,简化PushGeneric内部查找操作,榨干最后一丝性能
num = CallFunc();
int CallFunc(int value)
{        
        luaFunc.BeginPCall();                
        luaFunc.Push(value);
        luaFunc.PCall();        
        int num = (int)luaFunc.CheckNumber();
        luaFunc.EndPCall();
        return num;                
}
num = CallFunc();

总结

总的来说,三个框架都相互吸收优点加以整合,为了效率,在lua、c、c#交互上都是尽量做到值拷贝,映射类型减少boxing和unboxing,避免GC。Tolua侧重简练实现,对Unity Vector3等特定类型在Lua侧做了很多实现,减少了和C#交互,其他接口的处理上基本都是坚持减少LuaCAPI调用频次的原则,所以性能稍占优势。但扩展必需借助硬编码方式。Tolua线上有大量游戏验证使用,集成了很多常见库。而Xlua更注重解耦、通用实现,从生成Wrap采用模板方式也可看出作者是很排斥硬编码模式, 必须做到可扩展性强。Xlua保持框架纯粹(也有Tencent license风险问题)并没有默认集成太多第三方功能,库独立提供使用。slua是较早放在github上开源,去反射改静态绑定。早期代码质量比cstolua好一些,当时一部分项目选择该框架,但后期这个优势就逐渐没有了。

最后讨论一下三个框架实现耦合度、可扩展性和集成C++库便利性。

类别 Slua ToLua XLua
C# hotfix
工程结构、代码质量 ★★★☆☆ ★★★★☆ 力求简洁实用 ★★★★☆ 力求解耦、减少硬编码、可扩展性强
lib luaprofile、socket等 cjson、pb、socket、lpeg、luaprofile、int64等 luasocket、luaprofile、int64 。其他库独立提供
烘焙lua 烘焙 烘焙+打AssetBundle
测试用例、教程 少量例子 简单文档说明、大量例子 例子、教程、测试用例、文档完备
生成wrap Unity和Custom分别生成、可视化设置生成输出目录,开发需生成胶水代码。 拆分成wrap+delegates+binder,开发需生成胶水代码。 开发时可以用反射,发布时再生成。Unity和Custom合并生成,可以自定义模板输出自定义格式
Build Lua库 硬编码脚本Build、默认只集成了luajit,C#集成了luajit烘焙lua工具。Luac需自行集成。 独立runtime工程 mingw+Msys2 build、默认集成luajit,Luac需自行集成。 依赖CMake Build,集成lib很方便、luajit和lua最新几个版本都很好集成了

优化

实际开发中大多是Lua调用C#,保持二个原则提高性能:1.减少跨语言对象访问,如上分析框架得知这个交互过程其实很繁琐,代价很高。2.减少参数传递,尽量传valueType,映射好参数类型避免boxing。

说白了就是减少marshaling消耗!!!

image

下列代码来源于实际项目中,可看出Lua对面板和面板上UI组件均采用句柄(int)方式持有,lua和c#传递valueType类型数据。在c#端包装静态方法,减少跨语言对象访问成本。

--lua
 UIControlWrap.SetActive(self.wndHandle, self.msgWndHandle, true)
//UIControlWrap.cs 对象交互完全是 int 传递
public static void SetActive(int wndHandle, int ctrlHandle, bool active)
{
     GameObject ctrl = GameWindow.GetWindowControl(wndHandle, ctrlHandle);
     if (ctrl == null || ctrl.activeSelf == active) return;
     if (ctrl.activeSelf != active)
     {
         ctrl.SetActive(active);
     }
}

//UIControlWrap.cs 这里设置对象属性Vector3,只是传递二个float。
//对象transform访问完全是在c#侧直接操作,很大程度提升性能。
public static void SetWorldPositionXY(int wndHandle, int ctrlHandle, float x, float y)
{
        GameObject ctrl = GameWindow.GetWindowControl(wndHandle, ctrlHandle);
        if (ctrl == null) return;
        //上面三个框架分析可知lua侧做这个操作代价有多高
        Vector3 pos = ctrl.transform.position;
        pos.x = x;
        pos.y = y;
        ctrl.transform.position = pos;
}