Lua开发者运行: 2178天

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

作者:LuaStudio 发布于:2015-12-28 9:28 Monday 分类:心得体会

之前在《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.     lua_pushstring(L, "gdp");            //将gdp字符串压入栈顶
  5.     //根据栈顶的key获取table中的value,将key(这里的“gdp”)移除,再将value压入栈顶
  6.     lua_gettable(L, 1);                      
  7.     printf("%s\n", lua_tostring(L, -1)); //取栈顶元素(注意这里的整型值都是string类型)
  8.     lua_pop(L, 1); //取完之后清理栈顶
  9.     printf("stack size = %d\n", lua_gettop(L)); //打印栈中元素的个数

  10.     lua_pushstring(L, "info");   //同上
  11.     lua_gettable(L, 1);
  12.     printf("%s\n", lua_tostring(L, -1));
  13.     lua_pop(L, 1);
  14.     printf("stack size = %d\n", lua_gettop(L));

  15.     lua_pushstring(L, "task"); //这里的value值是一个table哦,没关系栈操作都是一样的
  16.     lua_gettable(L, 1);
  17.     for (int i = 0; i < 4; ++i)
  18.     {
  19.         lua_pushnumber(L, i+1);
  20.         lua_gettable(L, -2);
  21.         printf("%s\n", lua_tostring(L, -1));
  22.         lua_pop(L, 1);
  23.     }
  24.     lua_pop(L, 1);
  25.     printf("stack size = %d\n", lua_gettop(L)); //到这里tTest表依然在栈底,但不影响后面的操作。
  26.      
  27.     //------华丽的分割线------------//
  28.     //到这里table数据的解析就结束了,以下内容是c API给lua返回table数据

  29.     lua_newtable(L);//要给lua脚本返回一个table类型,先要new一个,压入栈顶
  30.     lua_pushnumber(L, 1); //将key先压入栈
  31.     lua_pushstring(L, "table2lua"); //再将value压入栈
  32.     lua_settable(L, -3);//settable将操作-2,-1编号的键值对,设置到table中,并把key-value从栈中移除

  33.     lua_pushstring(L, "key"); //同上
  34.     lua_newtable(L); //这里有个子table
  35.     lua_pushstring(L, "capi");
  36.     //这里的value类型使用lua_CFunction类型,可用做c API调用,函数实现请参看附录1
  37.     lua_pushcfunction(L, LuaSayHello); 
  38.     lua_settable(L, -3);
  39.     lua_pushnumber(L, 2);
  40.     lua_pushnumber(L, 10086);
  41.     lua_settable(L, -3);
  42.     lua_settable(L, -3); //这个从这里“lua_pushstring(L, "key"); //同上”开始匹配的
  43.     printf("stack size = %d\n", lua_gettop(L));
  44.     return 1; //返回栈顶1个元素
  45. }
复制代码

需要说明的是在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:

  1. //脚本函数实现打印一个table
  2. function printTable(t, n)
  3.         if "table" ~= type(t) then
  4.                 return 0;
  5.         end
  6.         n = n or 0;
  7.         local str_space = "";
  8.         for i = 1, n do
  9.                 str_space = str_space.."  ";
  10.         end
  11.         print(str_space.."{");
  12.         for k, v in pairs(t) do
  13.                 local str_k_v = str_space.."  ["..tostring(k).."] = ";
  14.                 if "table" == type(v) then
  15.                         print(str_k_v);
  16.                         printTable(v, n + 1);
  17.                 else
  18.                         str_k_v = str_k_v..tostring(v);
  19.                         print(str_k_v);
  20.                 end
  21.         end
  22.         print(str_space.."}");
  23. end

热烈欢迎各位留言,本人会虚心听取各位意见!