tolua源码分析(一) tolua的初始化流程

tolua源码分析(一) tolua的初始化流程

tolua是一个Unity静态绑定lua代码的解决方案,它能够反射分析C#代码,生成C#包装类。它大大简化了C#代码和lua代码之间的集成,可以自动生成用于在Lua中访问Unity的绑定代码,并把C#中的常量、变量、函数、属性、类以及枚举暴露给Lua。简单来说就是tolua实现了一套方案,通过这个方案我们可以透明地在lua层调用C#的函数,也可以反过来从C#端调用lua的函数。这个项目是开源的,它的源代码部署在GitHub上:

tolua

tolua_runtime

这里有两个项目,是因为解决方案除了需要在C#端实现逻辑以外,还需要修改lua虚拟机,在C层扩展lua虚拟机的接口,完成C#对象的管理,以及接口的转发。那么,实际使用过程中,一般项目的业务代码架构如下图所示:

tolua源码分析(一) 业务代码架构

这里,tolua在大多数平台上使用的lua虚拟机并非是官方lua,而是LuaJIT,目前只支持lua5.1的语法。当然我们也可以对源码进行修改,替换lua虚拟机为官方lua的高版本。从图中也可以看出,tolua runtime是非常重要的中间层,它扩展了lua虚拟机,让lua和C#的交互畅通无阻。

我们来看一下官方给的第一个例子HelloWorld,这个例子很简单,就是演示了如何在C#端执行一段lua代码:

using UnityEngine;
using LuaInterface;
using System;

public class HelloWorld : MonoBehaviour
{
    void Awake()
    {
        LuaState lua = new LuaState();
        lua.Start();
        string hello =
            @"                
                print('hello tolua#')                                  
            ";
        
        lua.DoString(hello, "HelloWorld.cs");
        lua.CheckTop();
        lua.Dispose();
        lua = null;
    }
}

这段代码中,第9-10行是用来初始化tolua本身的,这里的LuaState数据结构表示lua虚拟机;第11-16行就是加载一段代码并执行,DoString函数与lua自带的loadstring函数功能类似,第一个参数是要执行的lua代码块字符串,第二个参数是代码块的名称chunkName,该参数主要用于调试。第17行的CheckTop函数用来检查当前lua虚拟机的栈顶状态,如果栈中还有元素残留,说明虚拟机出现了问题,很可能是某个自定义扩展lua的函数所导致。第18-19行是对LuaState数据结构进行销毁清理。

tolua的GitHub上也附带了Unity工程,我们尝试运行下HelloWorld这个例子,可以看到输出如下:

扫描二维码关注公众号,回复: 14789130 查看本文章

tolua源码分析(一) HelloWorld

控制台打印了初始化lua虚拟机的时间,以及LuaJIT的版本号和状态,操作系统的信息,还有我们执行的lua代码,最后是lua虚拟机销毁的log。

有了大概的了解之后,我们现在深入地去看初始化相关的两个函数。首先是LuaState的构造函数,长这样:

public LuaState()            
{
    if (mainState == null)
    {
        mainState = this;
        // MULTI_STATE Not Support
        injectionState = mainState;
    }

    float time = Time.realtimeSinceStartup;
    InitTypeTraits();
    InitStackTraits();
    L = LuaNewState();            
    LuaException.Init(L);
    stateMap.Add(L, this);                        
    OpenToLuaLibs();            
    ToLua.OpenLibs(L);
    OpenBaseLibs();
    LuaSetTop(0);
    InitLuaPath();
    Debugger.Log("Init lua state cost: {0}", Time.realtimeSinceStartup - time);
}        

从这些函数的名字来推断,LuaState的构造过程大致可以分为以下几个部分:

  • 初始化各种traits,这个traits究竟是啥我们等下再说;
  • 创建lua虚拟机;
  • 加载各种lua库,可能来自C#,C,甚至lua;
  • 初始化lua代码的加载路径

那么现在我们来说一下这个traits。熟悉C++的同学肯定会立马想到type_traits,是的,这里这两个函数的作用与之类似,通过C#的泛型,在编译期就完成了不同类型检查函数,转换函数的绑定。

void InitTypeTraits()
{
    LuaMatchType _ck = new LuaMatchType();
    TypeTraits<sbyte>.Init(_ck.CheckNumber);
    TypeTraits<byte>.Init(_ck.CheckNumber);
    TypeTraits<short>.Init(_ck.CheckNumber);
    TypeTraits<ushort>.Init(_ck.CheckNumber);
    TypeTraits<char>.Init(_ck.CheckNumber);
    TypeTraits<int>.Init(_ck.CheckNumber);
    TypeTraits<uint>.Init(_ck.CheckNumber);
    TypeTraits<decimal>.Init(_ck.CheckNumber);
    TypeTraits<float>.Init(_ck.CheckNumber);
    TypeTraits<double>.Init(_ck.CheckNumber);
    TypeTraits<bool>.Init(_ck.CheckBool);
    TypeTraits<long>.Init(_ck.CheckLong);
    TypeTraits<ulong>.Init(_ck.CheckULong);
    TypeTraits<string>.Init(_ck.CheckString);

    ...
}

void InitStackTraits()
{
    LuaStackOp op = new LuaStackOp();
    StackTraits<sbyte>.Init(op.Push, op.CheckSByte, op.ToSByte);
    StackTraits<byte>.Init(op.Push, op.CheckByte, op.ToByte);
    StackTraits<short>.Init(op.Push, op.CheckInt16, op.ToInt16);
    StackTraits<ushort>.Init(op.Push, op.CheckUInt16, op.ToUInt16);
    StackTraits<char>.Init(op.Push, op.CheckChar, op.ToChar);
    StackTraits<int>.Init(op.Push, op.CheckInt32, op.ToInt32);
    StackTraits<uint>.Init(op.Push, op.CheckUInt32, op.ToUInt32);
    StackTraits<decimal>.Init(op.Push, op.CheckDecimal, op.ToDecimal);
    StackTraits<float>.Init(op.Push, op.CheckFloat, op.ToFloat);
    StackTraits<double>.Init(LuaDLL.lua_pushnumber, LuaDLL.luaL_checknumber, LuaDLL.lua_tonumber);
    StackTraits<bool>.Init(LuaDLL.lua_pushboolean, LuaDLL.luaL_checkboolean, LuaDLL.lua_toboolean);
    StackTraits<long>.Init(LuaDLL.tolua_pushint64, LuaDLL.tolua_checkint64, LuaDLL.tolua_toint64);
    StackTraits<ulong>.Init(LuaDLL.tolua_pushuint64, LuaDLL.tolua_checkuint64, LuaDLL.tolua_touint64);
    StackTraits<string>.Init(LuaDLL.lua_pushstring, ToLua.CheckString, ToLua.ToString);

    ...
}

TypeTraitsStackTraits都是泛型类,来看一下它们的Init函数:

public static class TypeTraits<T>
{        
    static public Func<IntPtr, int, bool> Check = DefaultCheck;

    static public void Init(Func<IntPtr, int, bool> check)
    {            
        if (check != null)
        {
            Check = check;
        }
    }
}

public static class StackTraits<T>
{
    static public Action<IntPtr, T> Push = SelectPush();
    static public Func<IntPtr, int, T> Check = DefaultCheck;
    static public Func<IntPtr, int, T> To = DefaultTo;               

    static public void Init(Action<IntPtr, T> push, Func<IntPtr, int, T> check, Func<IntPtr, int, T> to)
    {
        if (push != null)
        {
            Push = push;
        }

        if (to != null)
        {
            To = to;
        }

        if (check != null)
        {
            Check = check;
        }            
    }
}

从函数实现可以了解到,如果某个类型没有调用Init函数进行初始化,那么这两个泛型类会为该类型提供默认的函数。这个感觉是不是有点像C++的类模板?某些特定的类型,需要对类模板进行特化/偏特化处理,只不过C#要灵活得多。通过这种方式,我们就可以直接在外部根据不同类型,动态地增加/删除/修改对应绑定的函数,极大增加了代码的灵活性。

观察TypeTraitsInit实现,可以知道它的Check函数是一个接受两个参数(IntPtr,int),返回类型为bool的函数。我们来看一下int类型对应的CheckNumber函数:

public bool CheckNumber(IntPtr L, int pos)
{            
    return LuaDLL.lua_type(L, pos) == LuaTypes.LUA_TNUMBER;
}

这个函数很简单,就是判断lua栈上pos位置的元素类型是否为number。由于lua没有像C#那样提供那么多种数值类型,因此我们看到有很多C#数值类型的Init使用的是CheckNumber。那么不妨让我们大胆地猜测一下,TypeTraits<T>Check函数是用来判断lua栈上pos位置的元素类型是否为T

类似地,可以知道StackTraits拥有3个函数,其中Push函数接受两个参数(IntPtr,T),返回类型为void;Check函数接受两个参数(IntPtr,int),返回类型为T;To函数接受两个参数(IntPtr,int),返回类型为T。同样我们看一下int类型对应的处理:

public void Push(IntPtr L, int n)
{
    LuaDLL.lua_pushnumber(L, n);
}

public int CheckInt32(IntPtr L, int stackPos)
{
    double ret = LuaDLL.luaL_checknumber(L, stackPos);
    return Convert.ToInt32(ret);
}

public int ToInt32(IntPtr L, int stackPos)
{
    double ret = LuaDLL.lua_tonumber(L, stackPos);
    return Convert.ToInt32(ret);
}

那么,大胆地猜测,StackTraits<T>Push函数是用来把类型T的元素压入lua栈中;Check函数对栈上pos位置的元素进行检查,并尝试转换为T类型的值返回,可能会抛出异常;To函数对栈上pos位置的元素直接进行类型转换,返回T类型的值,不会抛出异常,如果转换失败返回T类型的默认值。

下一步就是创建lua虚拟机了,这块没有什么好说的,直接调用luaL_newstate即可。

接下来是这一句LuaException.Init(L);,它做的事情比较简单,就是为LuaException类指定了当前项目的路径,这个类顾名思义,是用来处理lua异常堆栈用的。

public static void Init(IntPtr L0)
{
    L = L0;
    Type type = typeof(StackTraceUtility);
    FieldInfo field = type.GetField("projectFolder", BindingFlags.Static | BindingFlags.GetField | BindingFlags.NonPublic);
    LuaException.projectFolder = (string)field.GetValue(null);
    projectFolder = projectFolder.Replace('\\', '/');
#if DEVELOPER
    Debugger.Log("projectFolder is {0}", projectFolder);
#endif
}

然后就到了重头戏,加载各种lua库的代码了。首先是OpenToLuaLibs,定义如下:

public void OpenToLuaLibs()
{
    LuaDLL.tolua_openlibs(L);
    LuaOpenJit();
}

tolua_openlibs是一个C函数,它的实现可以在tolua_runtime这里找到,位于tolua.c文件中。这里提一下,tolua.c基本上放了所有对lua虚拟机进行扩展的函数,主要是用在和unity,也就是C#层交互中。

LUALIB_API void tolua_openlibs(lua_State *L)
{
    
       
    initmodulebuffer();
    luaL_openlibs(L);   
    int top = lua_gettop(L);    

    tolua_setluabaseridx(L);    
    tolua_opentraceback(L);
    tolua_openpreload(L);
    tolua_openubox(L);
    tolua_openfixedmap(L);    
    tolua_openint64(L);
    tolua_openuint64(L);
    tolua_openvptr(L);    
    //tolua_openrequire(L);

    luaL_register(L, "Mathf", tolua_mathf);     
    luaL_register(L, "tolua", tolua_funcs);    

    lua_getglobal(L, "tolua");

    lua_pushstring(L, "gettag");
    lua_pushlightuserdata(L, &gettag);
    lua_rawset(L, -3);

    lua_pushstring(L, "settag");
    lua_pushlightuserdata(L, &settag);
    lua_rawset(L, -3);

    lua_pushstring(L, "version");
    lua_pushstring(L, "1.0.7");
    lua_rawset(L, -3);

    lua_settop(L,top);
}

我们先来看一下函数的声明。LUALIB_API是一个宏,在不同的编译环境下定义不同:

#define LUALIB_API	LUA_API

/* Linkage of public API functions. */
#if defined(LUA_BUILD_AS_DLL)
#if defined(LUA_CORE) || defined(LUA_LIB)
#define LUA_API		__declspec(dllexport)
#else
#define LUA_API		__declspec(dllimport)
#endif
#else
#define LUA_API		extern
#endif

函数的返回类型为void,说明该函数返回时,不会在lua虚拟机栈上存放任何值。这也是函数实现中,一开始调用lua_gettop记录栈顶,然后最后调用lua_settop恢复栈顶的原因。函数相对来说比较长,大概可以分为以下几个部分:

  • 加载lua的标准C库
  • 初始化tolua需要的各种全局环境
  • 加载tolua需要的扩展C库
  • 设置tolua的标志信息,恢复lua虚拟机栈

luaL_openlibs就是加载lua自带的各种C库函数,对于tolua默认使用的LuaJIT来说,有如下这些:

static const luaL_Reg lj_lib_load[] = {
    
    
  {
    
     "",			luaopen_base },
  {
    
     LUA_LOADLIBNAME,	luaopen_package },
  {
    
     LUA_TABLIBNAME,	luaopen_table },
  {
    
     LUA_IOLIBNAME,	luaopen_io },
  {
    
     LUA_OSLIBNAME,	luaopen_os },
  {
    
     LUA_STRLIBNAME,	luaopen_string },
  {
    
     LUA_MATHLIBNAME,	luaopen_math },
  {
    
     LUA_DBLIBNAME,	luaopen_debug },
  {
    
     LUA_BITLIBNAME,	luaopen_bit },
  {
    
     LUA_JITLIBNAME,	luaopen_jit },
  {
    
     NULL,		NULL }
};

static const luaL_Reg lj_lib_preload[] = {
    
    
#if LJ_HASFFI
  {
    
     LUA_FFILIBNAME,	luaopen_ffi },
#endif
  {
    
     NULL,		NULL }
};

初始化tolua环境这一部分比较复杂,我们一个函数一个函数地过。首先是tolua_setluabaseridx函数:

void tolua_setluabaseridx(lua_State *L)
{
    
        
	for (int i = 1; i <= 64; i++)
	{
    
    
		lua_pushinteger(L, i);
		lua_rawseti(L, LUA_REGISTRYINDEX, i);
	}

    //同lua5.1.5之后版本放入mainstate和_G
	lua_pushthread(L);
	lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD);

	lua_pushvalue(L, LUA_GLOBALSINDEX);
	lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS);

    //cache require函数
    lua_getglobal(L, "require");
    lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_REQUIRE);      
}

3-7行是一个循环,这里的用处是为了提前占位,在registry表里预留64个key。lua提供了一个独立的名为registry的表,C代码可以自由使用,但是lua代码无法访问到。这里贴一下官方解释:

Lua provides a registry, a pre-defined table that can be used by any C code to store whatever Lua value it needs to store. This table is always located at pseudo-index LUA_REGISTRYINDEX. Any C library can store data into this table, but it should take care to choose keys different from those used by other libraries, to avoid collisions. Typically, you should use as key a string containing your library name or a light userdata with the address of a C object in your code.

The integer keys in the registry are used by the reference mechanism, implemented by the auxiliary library, and therefore should not be used for other purposes.

如代码注释所说,9-18行通过registry表,缓存了mainstate,_G,和require。在tolua.h中,可以看到tolua当前定义了哪些预留key。

#if !defined(LUA_RIDX_MAINTHREAD)
#define LUA_RIDX_MAINTHREAD	1
#endif

#if !defined(LUA_RIDX_GLOBALS)
#define LUA_RIDX_GLOBALS 2
#endif

#define LUA_RIDX_TRACEBACK 			3
#define LUA_RIDX_UBOX 				4
#define LUA_RIDX_FIXEDMAP			5
#define LUA_RIDX_CHECKVALUE			6
#define LUA_RIDX_PACKVEC3			7
#define LUA_RIDX_UNPACKVEC3			8
#define LUA_RIDX_PACKVEC2 			9
#define LUA_RIDX_UNPACKVEC2			10
#define LUA_RIDX_PACKVEC4			11
#define LUA_RIDX_UNPACKVEC4			12
#define LUA_RIDX_PACKQUAT			13
#define LUA_RIDX_UNPACKQUAT			14
#define LUA_RIDX_PACKCLR			15
#define LUA_RIDX_UNPACKCLR			16
#define LUA_RIDX_PACKLAYERMASK      17
#define LUA_RIDX_UNPACKLAYERMASK    18
#define LUA_RIDX_REQUIRE            19
#define LUA_RIDX_INT64              20
#define LUA_RIDX_VPTR               21
#define LUA_RIDX_UPDATE				22
#define LUA_RIDX_LATEUPDATE			23
#define LUA_RIDX_FIXEDUPDATE		24
#define LUA_RIDX_PRELOAD			25
#define LUA_RIDX_LOADED				26
#define LUA_RIDX_UINT64				27
#define LUA_RIDX_CUSTOMTRACEBACK 	28

下一个函数是tolua_opentraceback, 顾名思义,就是向registry表中注册打印函数堆栈的函数。可以看到除了lua本来就提供的debug.traceback外,tolua还新增了一个自定义函数:

void tolua_opentraceback(lua_State *L)
{
    
    
    lua_getglobal(L, "debug");
    lua_pushstring(L, "traceback");
    lua_rawget(L, -2);
    lua_pushvalue(L, -1);
    lua_setfield(L, LUA_GLOBALSINDEX, "traceback");            
    lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_TRACEBACK);
    lua_pop(L, 1);    

    lua_pushcfunction(L, traceback);
    lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_CUSTOMTRACEBACK);    
}

tolua_openpreload与之类似,在registry表中缓存了package.preloadpackage.loaded,这两个table主要控制lua的require逻辑:

void tolua_openpreload(lua_State *L)
{
    
    
    lua_getglobal(L, "package");
    lua_pushstring(L, "preload");
    lua_rawget(L, -2);
    lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_PRELOAD);    
    lua_pushstring(L, "loaded");
    lua_rawget(L, -2);
    lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_LOADED);    
    lua_pop(L, 1);
}

tolua_openubox新建了一个空的table,用于存放来自C#的userdata,可以认为它是lua层存储C#对象的对象池。该table的key为number,value为userdata。该table还设置了一个metatable,指定__mode=v,表示它是一个value弱引用表,就是如果表的value不被其他任何元素所引用,那么它就可以被垃圾回收。

void tolua_openubox(lua_State *L)
{
    
    
	lua_newtable(L);
	lua_newtable(L);            
	lua_pushstring(L, "__mode");
	lua_pushstring(L, "v");
	lua_rawset(L, -3);
	lua_setmetatable(L, -2);            
	lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_UBOX);	
}

tolua_openfixedmap新建了另一个空的table,该table与前面提到的LUA_RIDX_UBOX恰好相反,它的key为来自C#的userdata,value是number。

void tolua_openfixedmap(lua_State *L)
{
    
    
	lua_newtable(L); 	
	lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_FIXEDMAP);		
}

tolua_openint64tolua_openuint64顾名思义,就是去加载自定义的int64/uint64库,因为lua 5.1并不支持64位整数。这里就不展开说了。

tolua_openvptr新建了一个table,该table包含一个__index__newindex的key,显然它是用作metatable的。这个metatable后面会提到,它主要是用来实现在lua层使用table继承C#的userdata机制。从函数名字也能得知一二,vptr,也就是C++的virtual table。

void tolua_openvptr(lua_State *L)
{
    
    
    lua_newtable(L);        

    lua_pushstring(L, "__index");
    lua_pushcfunction(L, vptr_index_event);
    lua_rawset(L, -3);  

    lua_pushstring(L, "__newindex");
    lua_pushcfunction(L, vptr_newindex_event);
    lua_rawset(L, -3);  

    lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_VPTR); 
}

下一步就是加载tolua的扩展库了,它们分别属于Mathftolua的模块下:

static const struct luaL_Reg tolua_mathf[] = 
{
    
    
    {
    
     "NextPowerOfTwo", mathf_nextpoweroftwo },
    {
    
     "ClosestPowerOfTwo", mathf_closestpoweroftwo },
    {
    
     "IsPowerOfTwo", mathf_ispoweroftwo},
    {
    
     "GammaToLinearSpace", mathf_gammatolinearspace},
    {
    
     "LinearToGammaSpace", mathf_lineartogammaspace},
    {
    
     "Normalize", mathf_normalize},
    {
    
     NULL, NULL }
};

static const struct luaL_Reg tolua_funcs[] = 
{
    
    
	{
    
     "gettime", tolua_gettime },
	{
    
     "typename", tolua_bnd_type },
	{
    
     "setpeer", tolua_bnd_setpeer},
	{
    
     "getpeer", tolua_bnd_getpeer},
    {
    
     "getfunction", tolua_bnd_getfunction},
    {
    
     "initset", tolua_initsettable},
    {
    
     "initget", tolua_initgettable},
    {
    
     "int64", tolua_newint64},        
    {
    
     "uint64", tolua_newuint64},
    {
    
     "traceback", traceback},
	{
    
     NULL, NULL }
};

最后就是设置tolua的相关标志信息了,这里设置了三个值,tolua.gettagtolua.settagtolua.version。前两者的值为light userdata,它们分别指向两个static变量的地址。这也是比较常见的做法,使用全局唯一的地址作为table的key,可以保证key绝对不会出现冲突,具体这两个light userdata的用处后面再说。tolua.version表示当前的版本号,这一点和lua的源码写法也类似。

自此,tolua_openlibs这个函数算是被我们拆解完了,下面回到C#层,看一下剩余的LuaOpenJit函数,该函数主要是检测Android平台是否支持jit on模式:

public void LuaOpenJit()
{
#if UNITY_ANDROID
    //某些机型如三星arm64在jit on模式下会崩溃,临时关闭这里
    if (IntPtr.Size == 8)
    {
        LuaDLL.luaL_dostring(L, "jit.off()");                                                
    }
    else if (!LuaDLL.luaL_dostring(L, jit))
    {
        string str = LuaDLL.lua_tostring(L, -1);
        LuaDLL.lua_settop(L, 0);
        throw new Exception(str);
    }
#endif
}

实际在项目应用时,为了稳定性起见,可以直接调用jit.off()关闭jit on模式,就不用走这段逻辑了。

回到前面,下一个加载lua库的函数为ToLua.OpenLibs

public static void OpenLibs(IntPtr L)
{
    AddLuaLoader(L);
    LuaDLL.tolua_atpanic(L, Panic);
    LuaDLL.tolua_pushcfunction(L, Print);
    LuaDLL.lua_setglobal(L, "print");
    LuaDLL.tolua_pushcfunction(L, DoFile);
    LuaDLL.lua_setglobal(L, "dofile");
    LuaDLL.tolua_pushcfunction(L, LoadFile);
    LuaDLL.lua_setglobal(L, "loadfile");

    LuaDLL.lua_getglobal(L, "tolua");

    LuaDLL.lua_pushstring(L, "isnull");
    LuaDLL.lua_pushcfunction(L, IsNull);
    LuaDLL.lua_rawset(L, -3);

    LuaDLL.lua_pushstring(L, "typeof");
    LuaDLL.lua_pushcfunction(L, GetClassType);
    LuaDLL.lua_rawset(L, -3);

    LuaDLL.lua_pushstring(L, "tolstring");
    LuaDLL.tolua_pushcfunction(L, BufferToString);
    LuaDLL.lua_rawset(L, -3);

    LuaDLL.lua_pushstring(L, "toarray");
    LuaDLL.tolua_pushcfunction(L, TableToArray);
    LuaDLL.lua_rawset(L, -3);

    int meta = LuaStatic.GetMetaReference(L, typeof(NullObject));
    LuaDLL.lua_pushstring(L, "null");
    LuaDLL.tolua_pushnewudata(L, meta, 1);
    LuaDLL.lua_rawset(L, -3);
    LuaDLL.lua_pop(L, 1);

    LuaDLL.tolua_pushudata(L, 1);
    LuaDLL.lua_setfield(L, LuaIndexes.LUA_GLOBALSINDEX, "null");
}

从具体实现里可以看出,这里加载的lua库都来自于C#,也就是用C#实现了一些lua会用到的接口。那首先来看看AddLuaLoader做了啥:

static void AddLuaLoader(IntPtr L)
{
    LuaDLL.lua_getglobal(L, "package");
    LuaDLL.lua_getfield(L, -1, "loaders");
    LuaDLL.tolua_pushcfunction(L, Loader);

    for (int i = LuaDLL.lua_objlen(L, -2) + 1; i > 2; i--)
    {
        LuaDLL.lua_rawgeti(L, -2, i - 1);
        LuaDLL.lua_rawseti(L, -3, i);
    }

    LuaDLL.lua_rawseti(L, -2, 2);
    LuaDLL.lua_pop(L, 2);
}

package.loaders主要用在lua的require函数中,它会使用loaders里的load函数加载指定模块。函数第7-13行的主要作用就是将C#层自定义的Loader函数插入到原有的package.loaders的第2个位置中。这个函数接管了lua文件加载的逻辑,可以方便我们使用项目自身加载文件的方式来管理lua文件。可能有人会问,为什么不是插入到表头呢?答案是第1个位置的lua loader叫做preload,它使用package.preload这个内部table,这个table的key是加载模块的名称,value是加载函数。也就是说,它是一种更特殊的loader,会根据模块名而启动特殊的加载逻辑。那当然C#的Loader函数要为它让步啦。

这里也贴一下官方的解释:

require (modname)

Loads the given module. The function starts by looking into the package.loaded table to determine whether modname is already loaded. If it is, then require returns the value stored at package.loaded[modname]. Otherwise, it tries to find a loader for the module.

To find a loader, require is guided by the package.loaders array. By changing this array, we can change how require looks for a module. The following explanation is based on the default configuration for package.loaders.

First require queries package.preload[modname]. If it has a value, this value (which should be a function) is the loader. Otherwise require searches for a Lua loader using the path stored in package.path. If that also fails, it searches for a C loader using the path stored in package.cpath. If that also fails, it tries an all-in-one loader (see package.loaders).

Once a loader is found, require calls the loader with a single argument, modname. If the loader returns any value, require assigns the returned value to package.loaded[modname]. If the loader returns no value and has not assigned any value to package.loaded[modname], then require assigns true to this entry. In any case, require returns the final value of package.loaded[modname].

If there is any error loading or running the module, or if it cannot find any loader for the module, then require signals an error.

OpenLibs里的剩下几个函数,这里就不一一展开了,第4-10行主要就是用C#重写了lua自带的panicprintdofileloadfile函数,重写它们的原因也是为了能够在C#层获得详细的日志和堆栈,以及可以在C#层控制lua的加载逻辑;第12-28行就是新增了tolua.isnulltolua.typeoftolua.tolstringtolua.toarray一些辅助函数。再看第30-37行,我们先不用管这些函数调用的具体含义,最后的结果就是在lua层定义了一个tolua.nullnull两个全局变量,这两个变量指向同一个userdata,这个userdata的含义就是C#的NullObject类。这个类是tolua自己定义的,就是一个空空如也的类,用来标记空对象:

    public class NullObject { }

好了,现在可以回到LuaState的构造函数继续分析了。接下来看到的是OpenBaseLibs函数,这个函数实际上就是将常用的C#类注册到lua中,使得lua可以访问C#类的方法或属性,这个后面会展开来说;LuaSetTop(0)所做的事情就是清理当前lua虚拟机的堆栈,使其保持调用前的模样;InitLuaPath函数就是设置了加载lua文件的路径,也就是扩充了lua的package.path

自此,包含tolua环境的lua虚拟机算是构造完成了。剩下的就是一个Start函数。这个函数中我们目前只需关注OpenBaseLuaLibs

void OpenBaseLuaLibs()
{
    DoFile("tolua.lua");            //tolua table名字已经存在了,不能用require
    LuaUnityLibs.OpenLuaLibs(L);
}

可以看到这里又去加载了一个lua文件,不难发现,这个lua文件对一些可能会频繁调用,但又实现简单的C#方法,以及一些常用的值类型,改用lua实现了:

-- tolua.lua
...
require "misc.functions"
Mathf		= require "UnityEngine.Mathf"
Vector3 	= require "UnityEngine.Vector3"
Quaternion	= require "UnityEngine.Quaternion"
Vector2		= require "UnityEngine.Vector2"
Vector4		= require "UnityEngine.Vector4"
Color		= require "UnityEngine.Color"
Ray			= require "UnityEngine.Ray"
Bounds		= require "UnityEngine.Bounds"
RaycastHit	= require "UnityEngine.RaycastHit"
Touch		= require "UnityEngine.Touch"
LayerMask	= require "UnityEngine.LayerMask"
Plane		= require "UnityEngine.Plane"
Time		= reimport "UnityEngine.Time"

list		= require "list"
utf8		= require "misc.utf8"

require "event"
require "typeof"
require "slot"
require "System.Timer"
require "System.coroutine"
require "System.ValueType"
require "System.Reflection.BindingFlags"
...

LuaUnityLibs.OpenLuaLibs分为两步,第一步的tolua_openlualibs对lua实现的C#方法和类型,进行了注册,这样做的目的是使得lua和C#互相调用时,这些方法和类型会自动转换。例如C#传入一个C#版本的Vector3给lua时,lua层接收到的实际上是lua版本的Vector3,而不是一个userdata;第二步的SetOutMethods为这些类型设置了out方法,可以让lua层调用C#层的带有out参数的方法。

public static void OpenLuaLibs(IntPtr L)
{                        
    if (LuaDLL.tolua_openlualibs(L) != 0)
    {
        string error = LuaDLL.lua_tostring(L, -1);
        LuaDLL.lua_pop(L, 1);
        throw new LuaException(error);
    }

    SetOutMethods(L, "Vector3", GetOutVector3);
    ...           
}

自此,tolua的初始化流程算是被我们拆解完毕了。总结来说,初始化主要分为两大块内容,一是进行各种配置,例如修改了若干原生lua方法,缓存了各种变量,创建了tolua所需的各种数据;二是加载了各种tolua用的库,这些库既可能来自C,也可能来自C#,甚至从lua频繁调用的角度考虑,来自lua。

下一节我们将关注C#如何调用lua函数的实现机制。

如果你觉得我的文章有帮助,欢迎关注我的微信公众号 我是真的想做游戏啊

Reference

[1] Lua 5.1 Reference Manual

[2] The LuaJIT Project

[3] tolua

[4] tolua_runtime

[5] Lua的require小结

[6] 再探Lua的require

猜你喜欢

转载自blog.csdn.net/weixin_45776473/article/details/129101882