(轉)ToLua源碼分析:啓動流程

 

說明


臨時工先頂上來,回頭整理施工。發現流水賬敘述比較無趣和難懂,後面考慮更換形式。
ToLua版本1.0.6。
第一篇啓動不深入過多細節,後面對特性進行深入解析。
部分代碼進行了抽取,以c#、c、lua形式混寫。實際以源碼爲準。
系列前置關卡:

  Lua語言。
  Unity使用經驗。
  Lua與宿主語言交互經驗。
  ToLua接入和使用經驗。
  tolua Unity工程和tolua_runtime源碼(不給下載鏈接,搜索和查閱資料是一項基本功)。

  ToLua基於LuaInterface,LuaInterface是一個實現lua和微軟.Net平臺的CLR混合編程的開源庫,使得lua腳本可以實例化CLR對象,訪問屬性,調用方法甚至使用lua函數來處理事件。ToLua保留了LuaInterface基本形式,重寫或移除了部分內容,使代碼更加簡潔,提供了對Unity的支持、拓展了lua5.1.4源碼。而最大的改進在於,LuaInterface中lua訪問CLR需要運行時反射,對於遊戲應用來說效率不夠理想,ToLua則提供了一套中間層導出工具,對於需要訪問的CLR、Unity及自定義類預生成wrap文件,lua訪問時只訪問wrap文件,wrap文件接收lua傳遞來的參數,進行類型(值、對象、委託)轉換,再調用真正工作的CLR對象和函數,最後將返回值返回給lua,有效地提高了效率。

核心功能及文件


提供Lua-c#值類型、對象類型轉化操作交互層。(ObjectTranslator.cs、LuaFunction.cs、LuaTable.cs、ToLua.cs等)
提供Lua虛擬機創建、啓動、銷燬,Require、DoFile、DoString、Traceback等相關支持。(LuaState.cs、LuaStatic.cs)
提供導出工具,利用c#反射,對指定的c#類生成對應的wrap文件,啓動後將所有wrap文件註冊到lua虛擬機中。(ToLuaMenu.cs、ToLuaExport.cs、ToLuaTree.cs、LuaBinder.cs、CustomSetting.cs等)
提供c#對象和lua userdata對應關係,使該userdata能訪問對應c#對象屬性,調用對應c#對象函數。lua支持一定的面向對象(類、繼承)。管理這些對象的內存分配與生命週期、GC。(LuaState.cs)
提供支持功能Lua Coroutine、反射等,Lua層重寫部分性能有問題對象如Vector系列。(Vector3.lua等)

啓動代碼


  ToLua啓動只需要三句代碼即可。但裏面實際經歷了一段漫長的日夜。

void Start() 
{
    lua = new LuaState();  // 啓動1:創建tolua提供的LuaState對象。              
    lua.Start();           // 啓動2:虛擬機初始化。
    LuaBinder.Bind(lua);   // 啓動3:Lua-c#中間層wrap文件們分別向虛擬機註冊自己。
}

 

啓動1 :lua new LuaState()

實例化LuaState對象。類的成員屬性按子類基類的順序初始化,類的構造函數按基類子類順序執行。


LuaState繼承自LuaStatePtr,該類包含一個System.IntPtr L指針,即lua虛擬機棧,並提供了一系列LuaDLL API的封裝,可以認爲是LuaDLL的升級版。而成員屬性中比較重要的有ObjectTranslator和LuaReflection,暫時我們只用關注ObjectTranslator。

// LuaState.cs
public class LuaState : LuaStatePtr, IDisposable {
    public ObjectTranslator translator = new ObjectTranslator();
    public LuaReflection reflection = new LuaReflection();

    public LuaState() 
    {
        ...
    }
}

 

啓動1.1 : ObjectTranslator

ObjectTranslator主要用於緩存lua需要訪問的c# object對象。提供了對象池以及添加、查詢、刪除對象方法。管理對象的生命週期。LuaObjectPool對象池包含了一個PoolNode對象列表進行循環複用,PoolNode對象包含對真正object的引用,以及一個當前空閒索引的鏈表,其策略類似於lua_ref。

public class ObjectTranslator {

    //gc打印標誌
    public bool LogGC { get; set; } 

    // 靜態單例
    private static ObjectTranslator _translator = null;  

    // object對象在對象池中的索引。
    public readonly Dictionary<object, int> objectsBackMap = 
             new Dictionary<object, int>(new CompareObject());   

    // object對象池。
    public readonly LuaObjectPool objects = new LuaObjectPool();     

     // 延遲gc列表,目前只在GameObject.Destroy(go, float t)傳遞了參數t時使用。
    private List<DelayGC> gcList = new List<DelayGC>();

    public ObjectTranslator()
    {
        // 是否在RemoveObject時,打印信息
        LogGC = false;  
        // 單例
        _translator = this;  
    }

    // 獲得單例
    public static ObjectTranslator Get(IntPtr L) {
        return _translator;
    }

    // 緩存一個object,返回在LuaObjectPool中的索引。
    public int AddObject(object obj) {...}

    // 根據索引,移除一個object緩存。
    public void RemoveObject(int udata) {...}

    // 根據索引,獲得一個已緩存的object對象。
    public object GetObject(int udata)
    {
        return objects.TryGetValue(udata);         
    }
}

 

啓動1.2 : LuaState構造

public LuaState()            
{
    if (mainState == null)
    {
        mainState = this;
    }
    /* LuaException繼承自Exception,用於出錯堆棧信息,
       tolua發生錯誤時,會throw new LuaException("xxxx"),
       LuaException內部會將堆棧格式化。
     */
    LuaException.Init(); 
    L = LuaNewState(); // = LuaDLL.luaL_newstate(),啓動lua虛擬機。           
    OpenToLuaLibs();    // 見構造1
    ToLua.OpenLibs(L);  // 見構造2
    OpenBaseLibs();     // 見構造3                   
    LuaSetTop(0);       // 棧頂設置個0保險
    InitLuaPath();      // 見構造4
}

 

構造 1:ToLua.OpenLibs(L);

調用了LuaDLL.tolua_openlibs(L),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.5");
    lua_rawset(L, -3);

    lua_settop(L,top);
}

 

initmodulebuffer(): 初始化了一個 static stringbuffer sb,用於緩存命名空間。
luaL_openlibs(L): 初始化lua基本庫,math、debug、io、os等。
tolua_setluabaseridx: 在Lua註冊表中預留了1-64,註冊了以下幾項 ,用於C#層直接獲取
  #define LUA_RIDX_MAINTHREAD   1 ->  L // 表示定義了宏,並在相應註冊表中存儲了Lua棧L,以下含義類似。
  #define LUA_RIDX_GLOBALS     2  ->  G
  #define LUA_RIDX_REQUIRE     19 -> require
tolua_opentraceback :
   _G[“traceback”] = debug.traceback
  #define LUA_RIDX_TRACEBACK 3 -> debug.traceback
tolua_openpreload:
  #define LUA_RIDX_PRELOAD 25 -> package.preload
  #define LUA_RIDX_LOADED 26 -> package.loaded
tolua_openubox: 創建了一個值弱表
  #define LUA_RIDX_UBOX 4 -> setmetatable({}, {__mode = “v”})
tolua_openfixedmap: 創建了一個fixedmap table
  #define LUA_RIDX_FIXEDMAP 5 -> {}
tolua_openint64(L): 聲明並註冊了int64元表
  #define LUA_RIDX_INT64 20 -> global[“int64”] = package[“int64”] = {int64}
tolua_openuint64(L): 聲明並註冊了uint64元表
  #define LUA_RIDX_UINT64 27 -> global[“uint64”] = package[“uint64”] = {uint64}
tolua_openvptr(L): 聲明註冊了vptr元表
  #define LUA_RIDX_VPTR 21 -> { __index = vptr_index_event; __newindex = vptr_newindex_event}
luaL_register(L, “Mathf”, tolua_mathf): 添加了一些Mathf相關函數
luaL_register(L, “tolua”, tolua_funcs): 添加了一些tolua拓展函數
tolua.gettag = &gettag 向tolua中存了一些值。
tolua.settag = &settag
tolua.version = “1.0.5”

構造2 :ToLua.OpenLibs(L)

向lua虛擬機註冊了一些基礎的c函數。

// ToLua.cs line:55
public static void OpenLibs(IntPtr L) {

    // AddLuaLoader = table.insert(package.loaders, ToLua.Loader, 2)
    AddLuaLoader(L) 

    /* 設置虛擬機崩潰時處理函數 */
    LuaDLL.tolua_atpanic(L, Panic);

    /* 向lua註冊了一些全局函數 */
    _G["print"] = ToLua.Print
    _G["doFile"] = ToLua.DoFile
    _G["loadfile"] = ToLua.Loadfile

    /* 向tolua table裏註冊了一些函數 */
    tolua["isnull"] = ToLua.IsNull
    tolua["typeof"]= ToLua.GetClassType  /* Lua.AddComponet(typeof(Transform)) */
    tolua["tolstring"]= ToLua.BufferToString
    tolua["toarray"] = ToLua.TableToArray
    tolua["null"]= newudate()        
    _G["null"] = tolua.null  
}

 

構造3 :OpenBaseLibs

tolua會事先生成一些Base Wrap File,也就是.Net或者Unity的基本類型中間層,由於這些類型的重要性,所以在這裏先註冊。註冊之後,Lua虛擬機就可以訪問這些類。

    /* 向lua註冊了一些基本類 */
    void OpenBaseLibs() {
        Register(System.Object); 
        Register(LuaInterface.NullObject);  // regster domain-> System.null
        Register(System.String, System.Object);  
        Register(System.Delegate, System.Object);  
        Register(System.Enum);
        Register(System.Array, System.Object);
        Register(System.Type, System.Object);
        Register(System.Collections.IEnumerator);
        Register(System.Collections.ObjectModel.ReadOnlyCollection);
        Register(System.Collections.Generic.List);
        Register(System.Collections.Generic.Dictionary);
        Register(System.Collections.Generic.Dictionary.KeyCollection);
        Register(System.Collections.Generic.Dictionary.ValueCollection);

        package.preload["tolua.out"] = LuaInterface_LuaOutWrap.LuaOpen_ToLua_Out;

        Register(LuaInterface.EventObject, System.Object);
        Register(UnityEngine.Object, System.Object);
        Register(UnityEngine.Coroutine);

        LuaUnityLibs.OpenLibs(L);            
        LuaReflection.OpenLibs(L);
    }

 

接着調用了LuaUnityLibs.OpenLibs和LuaReflection.OpenLibs:

    public static void OpenLibs(IntPtr L)
    {

        InitMathf(L);  // 初始化了Mathf庫
        InitLayer(L);
    }

    /* 向tolua table註冊了一些反射相關的方法。*/
    public static void OpenLibs(IntPtr L) 
    {
        tolua.findtype = LuaReflection.FindType;
        tolua.loadassembly= LuaReflection.LoadAssembly;
        tolua.getmethod= LuaReflection.GetMethod;
        tolua.getconstructor= LuaReflection.GetConstructor;
        tolua.gettypemethod= LuaReflection.GetTypeMethod;
        tolua.getfield= LuaReflection.GetField;
        tolua.getproperty= LuaReflection.GetProperty;
        tolua.createinstance= LuaReflection.CreateInstance'
        package.preload.reflection = LuaReflection.OpenReflectionLibs;
    }

其中OpenLibs中InitLayer主要是將Unity中的Layer註冊到lua虛擬機中。lua中使用Layer.name來獲取索引。

 

構造4 :InitLuaPath

  這是LuaState構造函數中做的最後一件事情,主要是初始化lua虛擬機require、import、DOFile文件的搜索路徑。
核心類LuaFileUtils,該類主要記錄了所有的搜索路徑,提供了從這些路徑搜索出文件加載的方法。而這裏調用的方法是LuaState.AddSearchPath,看名字即知道是向LuaFileUtils添加一個搜索路徑。這些默認路徑可以在LuaConst.cs中找到。

InitPackagePath()  // lua自帶一些搜索路徑,在package.path,這句是將這些路徑取出添加給LuaFileUtils
AddSearchPath(LuaConst.toluaDir); // editor only: 默認 Application.dataPath + "/Lua";
AddSearchPath(LuaConst.luaDir);    // editor only: 默認 Application.dataPath + "/ToLua/Lua";

/* string.Format("{0}/{1}/Lua", Application.persistentDataPath, osDir) */
AddSearchPath(LuaConst.luaResDir); 

 

new LuaState()總結

  1. 初始化了LuaState成員屬性ObjectTranslator和LuaReflection。
  2. LuaState構造開始。
  3. 構造中先初始化了LuaException用於異常時拋出,提供了出錯信息堆棧的格式化顯示。
  4. 以標準的LuaDLL.luaL_newstate語句正式啓動lua虛擬機。
  5. 繼續教科書般的開局,luaL_openlibs開啓lua基本庫,額外地在c層初始了tolua的一些相關內容。
  6. OpenToLuaLibs向lua虛擬機註冊ToLua.Print、ToLua.DoFile、Panic等基礎c函數。
  7. OpenBaseLibs先向虛擬機註冊了一些System和Unity命名空間下的基礎類,接着初始化了Mathf、Layer和一些反射相關。
  8. InitLuaPath在LuaFileUtils中存儲了lua中package.path、LuaConst中一些路徑搜索路徑。用於查找文件。

  9. LuaState構造結束。


個人覺得6、7的函數命名有些容易混淆。7最後做的事情也與6非常類似,代碼的職責有些混亂。
接下來就是啓動的第二步——LuaState.Start()。

啓動2:LuaState.Start()

啓動後面兩步做的事情相對簡單,LuaState.Start()先是DoFile(“tolua.lua”),而tolua.lua中require了所有tolua重寫的一些Unity性能有問題的類型,例如Vector3、Vector2、Bound等。接着在C#緩存了這些類型的一些lua方法。

public void Start()
{
        OpenBaseLuaLibs();
        PackBounds = GetFuncRef("Bounds.New");
        UnpackBounds = GetFuncRef("Bounds.Get");
        PackRay = GetFuncRef("Ray.New");
        UnpackRay = GetFuncRef("Ray.Get");
        PackRaycastHit = GetFuncRef("RaycastHit.New");
        PackTouch = GetFuncRef("Touch.New");
    }

 

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

 

-- 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"

 

這部分先了解下即可。

 

啓動3:LuaBinder.Bind(LuaState L)

LuaBinder.cs這個文件是ToLua導出自動生成的,而Bind函數是向lua虛擬機註冊所有的wrap類。 
舉例來說,LuaBinder.Bind可能如下:

// LuaBinder.cs
public static class LuaBinder
{
    public static void Bind(LuaState L)
    {
        float t = Time.realtimeSinceStartup;
        L.BeginModule(null);
        LuaInterface_DebuggerWrap.Register(L);
        L.BeginModule("UnityEngine");
        UnityEngine_ComponentWrap.Register(L);
        UnityEngine_TransformWrap.Register(L);
        UnityEngine_MaterialWrap.Register(L);
        UnityEngine_LightWrap.Register(L);
        UnityEngine_CameraWrap.Register(L);
        UnityEngine_AudioSourceWrap.Register(L);
        UnityEngine_BehaviourWrap.Register(L);  // behaviour
        L.EndModule();
        L.EndModule();
        ……
    }
}

 

  這裏BeginModule可以理解成命名空間,在lua中以table形式實現。BeginModule(null)代表當前命名空間是global全局域,而BeginModule(“UnityEngine”)則代表後面都註冊到_G[“UnityEngine”]表中。
  我們打開一個比較基礎的UnityEngine類Behaviour的wrap文件UnityEngine_BehaviourWrap.cs查看。剛纔LuaBinder中就調用了他的Register方法。

public class UnityEngine_BehaviourWrap
{
    public static void Register(LuaState L)
    {
        L.BeginClass(typeof(UnityEngine.Behaviour), typeof(UnityEngine.Component));
        L.RegFunction("New", _CreateUnityEngine_Behaviour);
        L.RegFunction("__eq", op_Equality);
        L.RegFunction("__tostring", ToLua.op_ToString);
        L.RegVar("enabled", get_enabled, set_enabled);
        L.RegVar("isActiveAndEnabled", get_isActiveAndEnabled, null);
        L.EndClass();
    }
}

 

  這裏BeginClass類似BeginModule,但實際功能比BeginModule更加複雜,並且傳遞了第二個可選的參數,代表着class的基類,在c層以metatable的形式實現對c#繼承的支持,暫時不必細究。RegFunction是向該class註冊一個方法,RegVar是向該class註冊一個屬性,即get和set方法。
  一個RegFunction和RegVar例子代碼如下:

//L.RegFunction("New", _CreateUnityEngine_Behaviour);
static int _CreateUnityEngine_Behaviour(IntPtr L)
{
    try
    {
        int count = LuaDLL.lua_gettop(L); // 取出參數個數
        if (count == 0)
        {
            UnityEngine.Behaviour obj = new UnityEngine.Behaviour();
            ToLua.Push(L, obj);  // 返回值放到棧上
            return 1;  // 有1個返回值
        }
        else
        {
            return LuaDLL.luaL_throw(L, "xxxerror"); // 異常
        }
    }
    catch(Exception e)
    {
        return LuaDLL.toluaL_exception(L, e); // 異常
    }
}

// L.RegVar("enabled", get_enabled, set_enabled);
static int get_enabled(IntPtr L)
{
    object o = null;

    try
    {
        o = ToLua.ToObject(L, 1);  // 從棧上取出一個對象,此時應爲UnityEngine.Behaviour
        UnityEngine.Behaviour obj = (UnityEngine.Behaviour)o;
        bool ret = obj.enabled; // 取出該對象上的enabled值
        LuaDLL.lua_pushboolean(L, ret); // 將值放到棧上
        return 1; // 返回了1個參數
    }
    catch(Exception e)
    {
        return LuaDLL.toluaL_exception(L, e, o == null ? "defaultException" : e.Message);
    }
}

 

  至此,ToLua變形結束,已經可以愉快地與Unity進行混合編程。但在實際項目中,還需要把項目中我們寫的lua文件進行加載,可以使用LuaState.DoFile、LuaState.DoString,也可以在lua文件中進行require。但無論何種方式,都要格外注意搜索路徑,如果使用LuaState中的DoFile和DoString方法,要調用相應的LuaState.AddSearchPath(“path”)加入進去。而在真機中,由於Android和ios路徑問題,默認的tolua.lua是require不到它所需的文件的。需要根據自己的項目有一定的調整。


轉載鏈接:https://blog.csdn.net/lodypig/article/details/60160020

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章