程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++導入自定義類到lua的詳細分析

C++導入自定義類到lua的詳細分析

編輯:關於C++

前言:

最近抽空看了下c++導出自定義類到lua中的一些底層實現,在這裡記錄,一方面方便自己以後查閱,另一方面也可以為他人提供一些思路(文章中如有錯誤,歡迎指正)

研究環境:

vs2012,cocos2dx-3.2,lua 5.3

一些有用的參考網站:

[Lua5.3 reference manual](http://www.lua.org/manual/5.3/manual.html)

正文:

先從入口講起,在這裡,關於lua的初始化、lua_state的初始化之類的就不再闡述了,這方面也是挺大的,有興趣的可以搜索關於它們的相關文章。這裡我們直接看關於本文的比較核心的代碼段:
    cocos2d::LuaEngine* pEngine = cocos2d::LuaEngine::getInstance();
    cocos2d::ScriptEngineManager::getInstance()->setScriptEngine(pEngine);
    cocos2d::LuaStack* stack = pEngine->getLuaStack();
    stack->setLuaErrorCB( onLuaError );
    register_all_moonton( stack->getLuaState() );

其中 最後一個函數,register_all_moonton這個函數就是注冊一些自定義類到lua中,其中就有我著重研究的類,其實現如下:

TOLUA_API int register_all_moonton(lua_State* tolua_S)
{
    tolua_open(tolua_S);

    tolua_module(tolua_S,nullptr,0);
    tolua_beginmodule(tolua_S,nullptr);

    lua_register_moonton_CGameFunc(tolua_S);

    tolua_endmodule(tolua_S);
    return 1;
}

這個函數接受一個lua_state指針作為參數,關於lua_state,可以理解為lua的一種多線程,深入的細節有興趣的同學可以查閱相關文章,在本文中你可以暫時把它當做一個“棧”來看待。
下面我們一步步看整個導入流程:(遇到不知道的lua api,可以去上面提供的manual手冊裡面進行查找,英語不太好的同學,開著詞典屏幕取詞也是可以大概看懂的,裡面的解釋都很易懂)

tolua_open(tolua_S);

打開相應實現:

TOLUA_API void tolua_open (lua_State* L)
{
    int top = lua_gettop(L);//獲取棧頂編號(因為index從1開始,所以變相等於獲取棧上面一共有多少元素)
    lua_pushstring(L,"tolua_opened");//將“tolua_opened”字符串壓入棧,此時棧結構:stack:“tolua_opened”
    lua_rawget(L,LUA_REGISTRYINDEX);//獲取名為LUA_REGISTRYINDEX的表t,然後取棧頂元素k,彈出棧頂元素k,然後將值t[k]壓入棧頂,此時棧結構:stack:value
    if (!lua_isboolean(L,-1))//這裡是重復初始化,以後會把tolua_opened置為true,這裡-1指的是棧頂,關於棧的正負索引,下面給圖,這裡判斷棧頂是否為bool值
    {
        lua_pushstring(L,"tolua_opened");//將“tolua_opened”壓入棧,此時棧結構:stack:value “tolua_opened”
        lua_pushboolean(L,1);//將1當做一個bool值壓入棧,此時棧結構:value “tolua_opened” 1
        lua_rawset(L,LUA_REGISTRYINDEX);//獲取lua注冊表t,棧頂元素作為value,棧頂後面一個作為key,t[key]=value,彈出value和key,此時棧結構:stack:value

        // create value root table
        lua_pushstring(L, TOLUA_VALUE_ROOT);//string壓入棧,棧結構:stack:value TOLUA_VALUE_ROOT
        lua_newtable(L);//創建新表,並壓入棧,棧結構:value TOLUA_VALUE_ROOT table
        lua_rawset(L, LUA_REGISTRYINDEX);//獲取lua注冊表t,棧中的table作為value,TOLUA_VALUE_ROOT作為key,實際上等於t[TOLUA_VALUE_ROOT] = table,分別彈出table,TOLUA_VALUE_ROOT,棧結構:stack:value

#ifndef LUA_VERSION_NUM /* only prior to lua 5.1 */
        /* create peer object table */
        lua_pushstring(L, "tolua_peers");//字符串壓入棧,棧結構:stack:value "tolua_peers"
        lua_newtable(L);//創建一個新表並壓入棧,棧結構:stack:value "tolua_peers" table
        /* make weak key metatable for peers indexed by userdata object */
        lua_newtable(L);//創建一個新表並壓入棧,棧結構:stack:value "tolua_peers" table table1
        lua_pushliteral(L, "__mode");//壓入一個字符串,棧結構:stack:value "tolua_peers" table table1 "__mode"
        lua_pushliteral(L, "k");//壓入一個字符串,棧結構:stack:value "tolua_peers" table table1 "__mode" "k"
        lua_rawset(L, -3);//分別取棧頂元素作為value,下一個作為key,第三個作為t,t[key]=value,並彈出棧頂和第二個元素,(這裡實際上是實現table的弱引用)當前棧結構:stack:value "tolua_peers" table table1
        lua_setmetatable(L, -2);//將棧頂table1作為第二個元素table的元表,彈出table1,棧結構:stack:value "tolua_peers" table
        lua_rawset(L,LUA_REGISTRYINDEX);//棧頂元素作為value,第二個元素作為key,lua注冊表作為t,t[key]=value,分別彈出棧頂元素、第二個元素,棧結構:stack:value
#endif
        //這裡與上面類似,就不在闡述
        /* create object ptr -> udata mapping table */
        lua_pushstring(L,"tolua_ubox");
        lua_newtable(L);
        /* make weak value metatable for ubox table to allow userdata to be
           garbage-collected */
        lua_newtable(L);
        lua_pushliteral(L, "__mode");
        lua_pushliteral(L, "v");
        lua_rawset(L, -3);               /* stack: string ubox mt */
        lua_setmetatable(L, -2);  /* stack: string ubox */
        lua_rawset(L,LUA_REGISTRYINDEX);

//        /* create object ptr -> class type mapping table */
//        lua_pushstring(L, "tolua_ptr2type");
//        lua_newtable(L);
//        lua_rawset(L, LUA_REGISTRYINDEX);

        //這裡實際上創建了名為"tolua_super"和"tolua_gc"兩張表,並且保存在lua注冊表裡
        lua_pushstring(L,"tolua_super");
        lua_newtable(L);
        lua_rawset(L,LUA_REGISTRYINDEX);
        lua_pushstring(L,"tolua_gc");
        lua_newtable(L);
        lua_rawset(L,LUA_REGISTRYINDEX);

        //垃圾回收
        //這裡用到了lua_pushcclosure,其實就是關聯了一些變量的c函數,與lua_pushcfunction類似,只是多了一些參數關聯,具體如下
        /* create gc_event closure */
        lua_pushstring(L, "tolua_gc_event");//stack:"tolua_gc_event"
        lua_pushstring(L, "tolua_gc");//stack:"tolua_gc_event" "tolua_gc"
        lua_rawget(L, LUA_REGISTRYINDEX);//stack:"tolua_gc_event" t["tolua_gc"](t是LUA_REGISTRYINDEX對應的lua注冊表)
        lua_pushstring(L, "tolua_super");//stack:"tolua_gc_event" t["tolua_gc"] "tolua_super"
        lua_rawget(L, LUA_REGISTRYINDEX);//stack:"tolua_gc_event" t["tolua_gc"] t["tolua_super"]
        lua_pushcclosure(L, class_gc_event, 2);//這裡指明了  函數class_gc_event關聯了兩個參數,也就是棧頂以及第二個參數,並且彈出他們,stack:"tolua_gc_event" class_gc_event
        lua_rawset(L, LUA_REGISTRYINDEX);//設置了t["tolua_gc_event"] = class_gc_event,並且將他們彈出棧,stack:空

        tolua_newmetatable(L,"tolua_commonclass");//創建了一個名字為XXX的元表,並且在這個函數裡面做了一些初始化,有興趣的同學可以跟進函數內部看看他是怎麼實現的,裡面也很簡單

        //這裡獲得module,module可以簡單粗暴地理解為c的命名空間,實際上它是一個table,tolua_module函數的展開在下面詳解,ps:不會改變棧結構,因為他最終會把壓入的都彈出
        tolua_module(L,NULL,0);
        tolua_beginmodule(L,NULL);//這裡就是使用剛才通過tolua_module創建出來的表,也就是把對應的module表壓入棧,具體實現放在下面,這裡棧已經存在一個module了,這裡是全局表在棧頂
        tolua_module(L,"tolua",0);//上面講過了,這裡就是創建了一個名字叫做tolua的table作為module
        tolua_beginmodule(L,"tolua");//使用tolua的module,注意:上面之前棧上已經存在全局表在棧頂了,這裡還會再把tolua的table壓入棧頂
        tolua_function(L,"type",tolua_bnd_type);//這個函數比較簡單,具體分析在下面,幾個相同的就不重復分析了
        tolua_function(L,"takeownership",tolua_bnd_takeownership);
        tolua_function(L,"releaseownership",tolua_bnd_releaseownership);
        tolua_function(L,"cast",tolua_bnd_cast);
        tolua_function(L,"isnull",tolua_bnd_isnulluserdata);
        tolua_function(L,"inherit", tolua_bnd_inherit);
#ifdef LUA_VERSION_NUM /* lua 5.1 */
        tolua_function(L, "setpeer", tolua_bnd_setpeer);
        tolua_function(L, "getpeer", tolua_bnd_getpeer);
#endif
        tolua_function(L,"getcfunction", tolua_bnd_getcfunction);
        tolua_function(L,"iskindof", tolua_bnd_iskindof);

        tolua_endmodule(L);//顧名思義,肯定就是回收module,就是從棧上彈出module
        tolua_endmodule(L);
    }
    lua_settop(L,top);//恢復棧的初始狀態
}

棧索引圖:

TOLUA_API void tolua_module (lua_State* L, const char* name, int hasvar)
{
    //在這裡可以看出其實module就是一張table
    //cocos源生的注釋已經很詳細了,這裡大概流程就是,
    //如果傳入name,即module名字,那麼就會先查一下它以前是不是創建過module表了,如果沒有那麼就會新建一個table,
    if (name)
    {
        /* tolua module */
        lua_pushstring(L,name);//stack: name
        lua_rawget(L,-2);//(這裡其實有點小坑,這裡要根據上下文,也就是要追溯很久之前的棧狀態,因為這裡棧只有一個值,但是嘗試去取-2,應該是在之前往棧中push一個全局表)嘗試獲取名字為name的全局表,並push入棧
        if (!lua_istable(L,-1))  /* check if module already exists */
        {
            //如果沒有就先把剛才那個nil彈出去,新建一個表,取名為name,並且push入棧
            lua_pop(L,1);//stack:global
            lua_newtable(L);//stack:global table
            lua_pushstring(L,name);//stack:global table name
            lua_pushvalue(L,-2);//stack: global table name table
            lua_rawset(L,-4);//stack:global table
        }
    }
    else
    {
        //如果沒傳名字,就直接把全局表拿出來push入棧
        /* global table */
        lua_pushvalue(L,LUA_GLOBALSINDEX);
    }
    if (hasvar)
    {
        //這裡是設置metatable的,語法也都講過,這裡可以嘗試自己翻譯一下
        if (!tolua_ismodulemetatable(L))  /* check if it already has a module metatable */
        {
            /* create metatable to get/set C/C++ variable */
            lua_newtable(L);
            tolua_moduleevents(L);
            if (lua_getmetatable(L,-2))
                lua_setmetatable(L,-2);  /* set old metatable as metatable of metatable */
            lua_setmetatable(L,-2);
        }
    }
    //最後把建好的module表彈出棧
    lua_pop(L,1);               /* pop module */
}
TOLUA_API void tolua_beginmodule (lua_State* L, const char* name)
{
    //這裡很多api都與之前講過的類似,各位同學可以自行翻譯棧結構
    //大體意思就是如果你傳了name,那麼他就會嘗試查找名字叫做name的一個table,如果isclass標志位為true,那就會獲取它的metatable,最終的棧結構為 stack: module metatable;如果他不是一個class,那麼直接就用這個table,而不用取它的metatable,棧結構就是:stack:module table
    //如果沒有傳name,那麼直接就用全局表作為返回值,push入棧以供後面使用
    if (name) { // ... module
//---- now module[name] is a table, get it's metatable to store keys
        // get module[name]
        lua_pushstring(L,name); // ... module name
        lua_rawget(L,-2);       // ... module module[name]
        // Is module[name] a class table?
        lua_pushliteral(L, ".isclass");
        lua_rawget(L, -2);                  // stack: ... module module[name] class_flag
        if (lua_isnil(L, -1)) {
            lua_pop(L, 1);                  // stack: ... module module[name]
            return;                         // not a class table, use origin table
        }
        lua_pop(L, 1);                      // stack: ... module class_table
        // get metatable
        if (lua_getmetatable(L, -1)) {  // ... module class_table mt
            lua_remove(L, -2);          // ... module mt
        }
//---- by SunLightJuly, 2014.6.5
    } else {
        lua_pushvalue(L,LUA_GLOBALSINDEX);
    }
}
TOLUA_API void tolua_function (lua_State* L, const char* name, lua_CFunction func)
{
    //之前已經把module壓入棧了,所以大概棧結構stack:... module
    lua_pushstring(L,name);//stack:... module name
    lua_pushcfunction(L,func);//stack: ... module name func
    lua_rawset(L,-3);//這裡把module[name] = func,並且彈出name func, 意思就是在module表中綁定了一個名字為name的函數,stack: ... module
}

恩,看起來一大堆,其實干的事情就那麼幾件事,初始化一些默認的值,導入了一些默認的類,並且綁定了相應的類方法,就可以理解為lua所進行的初始化動作。

繼續我們的主體函數 register_all_moonton
剛解釋了 tolua_open,
繼續看下面的,是不是發現之前都遇到了

    tolua_module(tolua_S,nullptr,0);
    tolua_beginmodule(tolua_S,nullptr);

對,這裡就是把全局表當做一個module壓入棧,繼續看最為重要的部分

lua_register_moonton_CGameFunc(tolua_S);

終於到了我們想要的綁定部分,這個CGameFunc就是本次我們想要綁定的類, 這個函數實現如下

int lua_register_moonton_CGameFunc(lua_State* tolua_S)
{
    tolua_usertype(tolua_S,"CGameFunc");//創建元表
    tolua_cclass(tolua_S,"CGameFunc","CGameFunc","",nullptr);//創建類表,並把元表復制給類表

    tolua_beginmodule(tolua_S,"CGameFunc");
        tolua_function(tolua_S,"GetObjID", lua_moonton_CGameFunc_GetObjID);
        tolua_function(tolua_S,"Int64Comp", lua_moonton_CGameFunc_Int64Comp);
        tolua_function(tolua_S,"TextFieldDetachIME", lua_moonton_CGameFunc_TextFieldDetachIME);
    tolua_endmodule(tolua_S);
    std::string typeName = typeid(CGameFunc).name();
    g_luaType[typeName] = "CGameFunc";
    g_typeCast["CGameFunc"] = "CGameFunc";
    return 1;
}

是不是感覺在有點似曾相識,沒錯,在tolua_open中我們已經遇到過類似的語句,只不過這次只有一點點不一樣,我們一點點說

tolua_usertype(tolua_S,"CGameFunc");

這個函數展開之後:

TOLUA_API void tolua_usertype (lua_State* L, const char* type)
{
    char ctype[128] = "const ";
    strncat(ctype,type,120);

    /* create both metatables */
    //創建了兩個元表,並且在mapsuper中繼承,具體的細節在下面
    if (tolua_newmetatable(L,ctype) && tolua_newmetatable(L,type))
        mapsuper(L,type,ctype);             /* 'type' is also a 'const type' */
}

這裡創建了兩張元表,並且做了一些初始化,tolua_newmetatable函數如下:

static int tolua_newmetatable (lua_State* L, const char* name)
{
    int r = luaL_newmetatable(L,name);//創建了一個名為name的表,並且壓入棧頂,返回值表示是否是新建的還是以前就曾經建過的(1,新建,0老的)

#ifdef LUA_VERSION_NUM /* only lua 5.1 */
    if (r) {
        //如果是新的那就把這張表作為key,name作為value保存在注冊表裡
        lua_pushvalue(L, -1);
        lua_pushstring(L, name);
        lua_settable(L, LUA_REGISTRYINDEX); /* reg[mt] = type_name */
    };
#endif

    if (r)
        //如果是新的就進行一些初始化動作
        tolua_classevents(L); /* set meta events */
    //將棧頂的metatable彈出,保持棧干淨
    lua_pop(L,1);
    return r;
}

函數luaL_newmetatable的功能其實就是創建一張名字為tname的表,作為元表並且保存在注冊表中,可以復用,其實現也很簡單,展開如下:

LUALIB_API int luaL_newmetatable (lua_State *L, const char *tname) {
  lua_getfield(L, LUA_REGISTRYINDEX, tname);  /* get registry.name */
  if (!lua_isnil(L, -1))  /* name already in use? */
    return 0;  /* leave previous value on top, but return 0 */
  lua_pop(L, 1);
  lua_newtable(L);  /* create metatable */
  lua_pushvalue(L, -1);
  lua_setfield(L, LUA_REGISTRYINDEX, tname);  /* registry.name = metatable */
  return 1;
}

函數mapsuper展開如下:

static void mapsuper (lua_State* L, const char* name, const char* base)
{
    /* push registry.super */
    //tolua_super這個東東在一開始的lua_open中創建的,是一張表,這裡把它當做基類取出
    lua_pushstring(L,"tolua_super");    /* stack: "tolua_super" */
    lua_rawget(L,LUA_REGISTRYINDEX);    /* stack: super */
    //獲取名為name的元表,之前的函數已經創建好
    luaL_getmetatable(L,name);          /* stack: super mt */
    //嘗試從tolua_super這個table中獲取metatable為key的value,之前初始化啥都沒做,正常這裡應該是空的,然後把結果壓入棧
    lua_rawget(L,-2);                   /* stack: super table */
    if (lua_isnil(L,-1))
    {
        //發現如果曾經沒初始化過,就進來初始化,就是創建一個metatable,並把它賦給tolua_super基類作為元表,棧結構cocos已經寫得很詳細了,我這裡就不修改它的了
        /* create table */
        lua_pop(L,1);
        lua_newtable(L);                    /* stack: super table */
        luaL_getmetatable(L,name);          /* stack: super table mt */
        lua_pushvalue(L,-2);                /* stack: super table mt table */
        lua_rawset(L,-4);                   /* stack: super table */
    }

    /* set base as super class */
    lua_pushstring(L,base);//傳入的基類名壓入棧
    lua_pushboolean(L,1);//壓入true
    lua_rawset(L,-3);//table[base]=true                    /* stack: super table */

    /* set all super class of base as super class of name */
    luaL_getmetatable(L,base);          /* stack: super table base_mt */
    lua_rawget(L,-3);                   /* stack: super table base_table */
    if (lua_istable(L,-1))
    {
        //遍歷積累的metatable,然後將基類metatable的key value值全部copy到name對應的元表中
        /* traverse base table */
        lua_pushnil(L);  /* first key */ /* stack: super table base_table nil*/
        while (lua_next(L,-2) != 0)
        {
            /* stack: ... base_table key value */
            lua_pushvalue(L,-2);    /* stack: ... base_table key value key */
            lua_insert(L,-2);       /* stack: ... base_table key key value */
            lua_rawset(L,-5);       /* stack: ... base_table key */
        }
    }
    lua_pop(L,3);                       /* stack:  */
}

再回到主函數,下面的一個函數:

tolua_cclass(tolua_S,"CGameFunc","CGameFunc","",nullptr);

展開如下:

TOLUA_API void tolua_cclass (lua_State* L, const char* lname, const char* name, const char* base, lua_CFunction col)
{
    char cname[128] = "const ";
    char cbase[128] = "const ";
    strncat(cname,name,120);
    strncat(cbase,base,120);

    mapinheritance(L,name,base);
    mapinheritance(L,cname,name);

    mapsuper(L,cname,cbase);
    mapsuper(L,name,base);

    lua_pushstring(L,lname);

    push_collector(L, name, col);
    /*
    luaL_getmetatable(L,name);
    lua_pushstring(L,".collector");
    lua_pushcfunction(L,col);

    lua_rawset(L,-3);
    */
    //關鍵部分的棧結構注釋已經很全,就不再重復注釋:
//---- create a new class table, set it's metatable, and assign it to module
    lua_newtable(L);                    // stack: module lname table
    luaL_getmetatable(L,name);          // stack: module lname table mt
    lua_setmetatable(L, -2);            // stack: module lname table
    //在這裡把新創建的表的".isclass"標識置為true,然後在module表中保存新建的類表,module[lname] = table
    //Use a key named ".isclass" to be a flag of class_table
    lua_pushliteral(L, ".isclass");
    lua_pushboolean(L, 1);
    lua_rawset(L, -3);                  // stack: module lname table
    lua_rawset(L, -3);                  // stack: module
//---- by SunLightJuly, 2014.6.5

    /* now we also need to store the collector table for the const
       instances of the class */
    push_collector(L, cname, col);
    /*
    luaL_getmetatable(L,cname);
    lua_pushstring(L,".collector");
    lua_pushcfunction(L,col);
    lua_rawset(L,-3);
    lua_pop(L,1);
    */


}

這裡其實就可以看出來,類轉化為一個table,這個table有個字段”.isclass”標識它是一個類表,並且保存在一開始申請的module名字下面

下面就是一些函數的賦值了

     tolua_beginmodule(tolua_S,"CGameFunc");
        tolua_function(tolua_S,"GetObjID", lua_moonton_CGameFunc_GetObjID);
        tolua_function(tolua_S,"Int64Comp", lua_moonton_CGameFunc_Int64Comp);
        tolua_function(tolua_S,"TextFieldDetachIME", lua_moonton_CGameFunc_TextFieldDetachIME);
    tolua_endmodule(tolua_S);

這裡在上面其實已經分析過了,在這裡就再簡單說一下,這裡有一點點區別,就是因為在這裡CGameFunc在之前已經創建了一個類表,所以它的 “.isclass”為true,在tolua_beginmodule裡面這個標志位有很大作用,如果不是類,那麼就會用表本身作為容器,然後壓入棧,之後所有的函數注冊都是保存在這張表裡,如果是一個類表,那麼就會把metatable作為容器,壓入棧,注冊的函數都會進metatable中保存。
至此,c++導出類到lua中大概的流程都已經講解完畢,如果哪裡有問題歡迎指正,共同進步,謝謝!

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved