如何在Lua与C/C++之间实现table数据的交换?

  之前在《C/C++和Lua是如何进行通信的?》一文中简单的介绍了lua与宿主之间的通信。简单的说两种不同的语言之间数据类型不一样又如何进行数据交换呢?那就是lua_State虚拟栈,通过栈操作和lua库函数,我们很轻松就能完成两者之间的数据交换。

  开始之前,明确几个问题,lua中的虚拟栈的索引编号问题(我们假设栈大小为n),编号1是栈底,n视栈顶,编号-1是栈顶,-n是栈底。lua中的库函数需要访问和操作栈上的数据都是通过索引编号定位的。但是我们需要明确一点,有些API并没有使用索引编号作为参数,意味着默认对栈顶进行操作。如lua_pushnumber(L, 66)将数值66压入栈顶,lua_tonumber(L, -1)取编号-1(栈顶)元素等等,如果这些基本知识和API的都已经熟悉了,那么lua与宿主之间的数据交换就很容易理解了。

  姿势准备好了,那么问题来了。需求:我们现在要设计一个UI界面,我们希望这个UI是可以重用的。为了满足这个需求,显然我们必须将UI界面与显示数据分离。使用lua初始化数据后,将数据传递给UI界面然后显示。这样如果需求变更(游戏开发中经常产生这样的需求),我们也只需改变lua脚本就能重用UI界面,听上去真是程序猿的福音啊~~

  用于显示UI的数据必定很多,需要使用lua中的table来封装这些数据,现在给定如下lua table数据:

1 local tTest = 
2 {
3     gdp = 1234,
4     info = "this is test about exchange table data!",
5     task = {12, 23, 34, 45},
6 };

我们的脚本将调用一个程序封装好的c API(TestTable函数),然后将tTest作为参数,压入虚拟栈中,如下:

1 local tRet = TestTable(tTest);

虽然tTest table已经传给了程序,我们还需要对TestTable这个c API进行定制,使它能够正确的理解这个table中的数据,实现代码如下(LuaTestTable函数类型是lua_CFuntion类型,注册到lua虚拟机中的函数名为TestTable):

 1 int LuaTestTable(lua_State* L)
 2 {
 3     printf("stack size = %d\n", lua_gettop(L)); //打印栈中元素的个数
 4 
 5     lua_pushstring(L, "gdp");            //将gdp字符串压入栈顶
 6     //根据栈顶的key获取table中的value,将key(这里的“gdp”)移除,再将value压入栈顶
 7     lua_gettable(L, 1);                      
 8     printf("%s\n", lua_tostring(L, -1)); //取栈顶元素(注意这里的整型值都是string类型)
 9     lua_pop(L, 1); //取完之后清理栈顶
10     printf("stack size = %d\n", lua_gettop(L)); //打印栈中元素的个数
11 
12     lua_pushstring(L, "info");   //同上
13     lua_gettable(L, 1);
14     printf("%s\n", lua_tostring(L, -1));
15     lua_pop(L, 1);
16     printf("stack size = %d\n", lua_gettop(L));
17 
18     lua_pushstring(L, "task"); //这里的value值是一个table哦,没关系栈操作都是一样的
19     lua_gettable(L, 1);
20     for (int i = 0; i < 4; ++i)
21     {
22         lua_pushnumber(L, i+1);
23         lua_gettable(L, -2);
24         printf("%s\n", lua_tostring(L, -1));
25         lua_pop(L, 1);
26     }
27     lua_pop(L, 1);
28     printf("stack size = %d\n", lua_gettop(L)); //到这里tTest表依然在栈底,但不影响后面的操作。
29     
30     //------华丽的分割线------------//
31     //到这里table数据的解析就结束了,以下内容是c API给lua返回table数据
32 
33     lua_newtable(L);//要给lua脚本返回一个table类型,先要new一个,压入栈顶
34     lua_pushnumber(L, 1); //将key先压入栈
35     lua_pushstring(L, "table2lua"); //再将value压入栈
36     lua_settable(L, -3);//settable将操作-2,-1编号的键值对,设置到table中,并把key-value从栈中移除
37 
38     lua_pushstring(L, "key"); //同上
39     lua_newtable(L); //这里有个子table
40     lua_pushstring(L, "capi");
41     //这里的value类型使用lua_CFunction类型,可用做c API调用,函数实现请参看附录1
42     lua_pushcfunction(L, LuaSayHello); 
43     lua_settable(L, -3);
44     lua_pushnumber(L, 2);
45     lua_pushnumber(L, 10086);
46     lua_settable(L, -3);
47     lua_settable(L, -3); //这个从这里“lua_pushstring(L, "key"); //同上”开始匹配的
48     printf("stack size = %d\n", lua_gettop(L));
49     return 1; //返回栈顶1个元素
50 }

需要说明的是在lua中tTest["gdp"]和tTest.gdp的调用形式是一样的,这是lua的语法糖。当然操作栈中的table方法除了lua_gettable和lua_settable还有其它方法,请参看lua_rawget和lua_rawset。理解栈中的元素变化是非常重要的。

  LuaTestTable函数API的后面部分介绍了构造一个任意table作为返回值,返回给lua脚本。首先使用lua_newtable库函数新建一个table类型的数据,并压入栈。然后将键值对key-value依次压入栈,调用lua_settable(L, index)将key-value设置到table中,子table操作也是一样的(这里的index指的是要设置的table在栈中的索引编号)。

  好了,基本介绍完了,最后来编写脚本,看看效果(以下是程序调用的脚本):

1 --file: test.lua
2 local tTest = {
3     gdp = 1234,
4     info = "this is test about exchange table data!",
5     task = {12, 23, 34, 45},
6 };
7 local tRet = TestTable(tTest);
8 printTable(tRet);    //实现请参看附录2
9 tRet.key.capi(); //实现请参看附录1

TestTable成功解析tTest的数据,并且返回一个table类型的tRet。

1 printTable(tRet);

printTable简单的实现了一个table打印的脚本将tRet打印输出。

1 tRet.key.capi();

脚本调用tRet中返回的c API类型的函数。具体实现请参见附录。

运行结果:

 1 stack size = 1  //以下是LuaTestTable的输出
 2 1234
 3 stack size = 1
 4 this is test about exchange table data!
 5 stack size = 1
 6 12
 7 23
 8 34
 9 45
10 stack size = 1   
11 stack size = 2   //以下是printTable的输出
12 {
13   [1] = table2lua
14   [key] = 
15   {
16     [2] = 10086
17     [capi] = function: 0x409795
18   }
19 }
20 Lua call c/c++:SayHello() //这里是 [capi] = function: 0x409795被调用的输出
21 Hello Everyone!

综上述,我们只需要修改tTest中的数据(结构不能改,改了需要修改LuaTestTable函数)就能改变UI界面的显示,成功的解决了UI界面的复用问题。(文中没有具体讲到如何UI截面相关的细节,请参照例子自行脑补。)

附录1:

1 //注册到lua虚拟机中的c API函数
2 int LuaSayHello(lua_State* L)
3 {
4     printf("Lua call c/c++:SayHello()\n");
5     printf("Hello Everyone!\n");
6     return 0;
7 }

附录2:

//脚本函数实现打印一个table
function printTable(t, n)
    if "table" ~= type(t) then
        return 0;
    end
    n = n or 0;
    local str_space = "";
    for i = 1, n do
        str_space = str_space.."  ";
    end
    print(str_space.."{");
    for k, v in pairs(t) do
        local str_k_v = str_space.."  ["..tostring(k).."] = ";
        if "table" == type(v) then
            print(str_k_v);
            printTable(v, n + 1);
        else
            str_k_v = str_k_v..tostring(v);
            print(str_k_v);
        end
    end
    print(str_space.."}");
end

完整项目托管在github上:(支持vs2013和cmake编译)

转载请申明出处,如有任何疑问或建议指出,谢谢~