luabinding如何配置 - GameKong/restaurant GitHub Wiki

require流程

使用require(modname)加载模块,这个函数首先会查找package.loaded表(package.loaded中储存或记录着已经加载过的模块),如果modname模块加载过,则直接返回package.loaded储存的值,若模块未加载过,则为模块寻找加载器

require使用modname作为参数,遵循package.searchers序列来寻找加载器(我们可以改变查找加载器的序列),以下未默认的加载序列:

1.【预加载器】查找package.preload[modname]是否有值(该值必须是一个函数,该函数就是加载器),如果存在预加载器,则调用该加载器加载模块,否则,执行下一步。
2.【lua加载器】根据package.path列表查找是否存在对应的lua文件。使用modname替换?,例如,如果路径是字符串"./?.lua;./?.lc;/usr/local/?/init.lua",搜索`foo.a`这个名字将依次尝试打开文件`./foo/a.lua` ,`./foo/a.lc`,以及`/usr/local/foo/a/init.lua` 。如果存在指定lua文件,则加载加载该lua文件,否则,执行下一步。		
3.【c加载器】根据package.cpath列表查找是否存在对应的.so文件等C库。使用modname替换?,如果C路径是这样一个字符串"./?.so;./?.dll;/usr/local/?/init.so"查找器查找模块`foo.a`会依次尝试打开文件`./foo.a.so`,`./foo.a.dll`,以及`/usr/local/foo/a/init.so` 。一旦它找到一个C库,查找器首先使用动态链接机制连接该库。 然后尝试在该库中找到可以用作加载器的C函数。这个C函数的名字是"`luaopen_`"紧接模块名modname的字符串,其中字符串中所有的点都会被替换成下划线。此外,如果模块名中有横线, 横线后面的部分(包括横线)都被去掉。例如,如果模块名为`foo.a-v2.1`,函数名就是`luaopen_foo_a`。如果找到符合的加载器则调用该加载器,否则,执行下一步。
4.【一体化加载器】根据package.cpath列表查找是否存在对应的so文件。它C路径中查找指定模块的根名字。例如,当请求`foo.a.b` 时,它将查找`foo`这个C库。如果找得到,它会在里面找子模块的加载函数。 在我们的例子中,就是找`luaopen_foo_a_b`。利用这个机制.可以把若干 C 子模块打包进单个库。 每个子模块都可以有原本的加载函数名。若找到则执行加载器,否则,加载失败。

luabinding配置方式与流程

  • 1.配置预加载器方式进行luabinding,该方式也是比较常用的方式,因为模式比较固定,所以比较简单。在cocos2d工程中,主要用于我们自己加入的第三方库。
    • 配置流程:

      • 1.定义库函数
      //  protobuf编码函数
      static int varint_encoder(lua_State *L)
      {
          lua_Number l_value = luaL_checknumber(L, 2);
          uint64_t value = (uint64_t)l_value;
          luaL_Buffer b;
          luaL_buffinit(L, &b);
          pack_varint(&b, value);
          lua_settop(L, 1);
          luaL_pushresult(&b);
          lua_call(L, 1, 0);
          return 0;
      }
      
      // 库函数和加载器函数必须遵循以下形式
      int function_name(lua_State *L)
      {
         ...
         ...
         return 0; // 返回值表示压入lua栈的返回值个数
      }
      
      • 2.定义库函数注册列表
      static const struct luaL_reg _pb [] = {
          {"varint_encoder", varint_encoder},  // {函数名(字符串),函数指针}
          {"varint_decoder", varint_decoder},
          .....
          {NULL, NULL}
      };
      
      • 3.定义预加载器函数,一般来说,该函数内部会创建一个table作为模块的容器,并且将定义的库函数注册进该表中
      int luaopen_pb (lua_State *L)
      {
          luaL_newmetatable(L, IOSTRING_META); // 创建一个名为IOSTRING_META的表到注册表
          lua_pushvalue(L, -1); // 压入栈顶的副本到栈顶
          lua_setfield(L, -2, "__index"); // 设置 -2对应的table的__index索引对应的值为栈顶值,弹出栈顶值
          luaL_register(L,  NULL , _c_iostring_m); // 注册_c_iostring_m中的函数到栈顶的表中
          // luaL_newlib(L, _c_iostring_m); lua 5.2以上才有
          luaL_register(L, "pb", _pb);  // 创建全局表pb。
          // luaL_newlib(L, _pb);
          return 1;
      }
      
      1.luaL_newlib 等价于 luaL_newlibtable(L,l) + luaL_setfuncs(L,l,0),但是只lua5.2以上(包含5.2)才有该函数,cocos2dx使用的是lua5.1,所以只能使用luaL_register(L, modname, luaL_reg_list);
      2.luaL_register的内部实现是:如果modname为NULL,则使用当前栈顶的table,注册luaL_reg_list中的函数到栈顶的table中。 否则,创建一个全局table,注册luaL_reg_list中的函数到table中,设置table名为modname,然后设置package.loaded[modname] = table,然后将该table压入栈顶。所以该实现会污染全局环境,lua5.2改为使用luaL_newlib。
      
      • 4.定义一个函数,将定义好的预加载器函数加入到package.preload表中。(当在lua代码中第一次require该模块就会调用该加载器加载模块)
      static int register_pb(lua_State* L)
      {
          lua_getglobal(L, "package");  // 获取package表
          lua_getfield(L, -1, "preload");  // 获取package.preload表
          lua_pushcfunction(L, luaopen_pb); 
          lua_setfield(L, -2, "pb"); // 将加载器函数加入到package.preload表中,package.preload["pb"] = luaopen_pb
          lua_pop(L, 2);
          return 1;
      }
      
      • 5.定义一个函数,集中调用不同模块的预加载函数,便于管理和阅读。
      int lua_custom_module_register(lua_State* L)
      {
          register_cjson(L);
          register_lfs(L);
          register_pack(L);
          register_pb(L);
          register_zlib(L);
          ....
          return 1;
      }
      
      // 注册自定义模块预加载器
      lua_custom_module_register(L);
      
  • 2.直接在C代码中进行luabinding,该方式与配置预加载器的方式的配置流程比较相似,主要区别在于,该方式运行C代码时会直接进行luabinding,而不是将加载器函数存入到package.preload表中。在游戏运行后,通过预加载器配置的luabinding模块不一定会加载模块到lua虚拟机中(不调用require(modname)),但是通过C代码直接进行luabinding在启动后就会将模块加载到lua虚拟机中。在cocos2d工程中,该方式主要用于cocos引擎基础的C模块和自带的第三方模块。
    • 配置流程:
      • 1.定义库函数
      • 2.定义库函数注册列表
      • 3.定义加载器函数,一般来说,该函数内部会创建一个table作为模块的容器,并且将定义的库函数注册进该表中
      • 4.调用加载器函数加载模块到lua虚拟机中。
  • 3.配置.so文件等C库的方式进行luabinding,因为该方式需要查找.so等C库文件,所以需要配置package.cpath,不同操作系统的包体路径各不相同,配置起来比较麻烦,所以建议使用预加载器的方式进行luabinding。该方式配置luabinding的方式与配置预加载器的方式基本相同,主要区别在于加载器函数无需加入到package.preload表中。且加载器函数命名方式比较固定,必须遵循【luaopen_模块名】的方式进行命名。因为require加载C库可能使用C加载器或一体化加载器,所以模块名和加载器名以及C库所在路径必须统一。
    • 例如执行 require "a.b.c" :
      • 如果使用C加载器。库名【c.so】、库路径【xx/a/b/c.so】、加载器名【luaopen_a_b_c 】。
      • 如果使用一体化加载器。库名【a.so】、库路径【xx/a.so】、加载器名【luaopen_a_b_c 】。
    • 配置流程:
      • 1.定义库函数
      • 2.定义库函数注册列表
      • 3.定义加载器函数luaopen_xx,一般来说,该函数内部会创建一个table作为模块的容器,并且将定义的库函数注册进该表中
      • 4.将C库函数编译成.so文件,并且放入指定的路径下。修改工程代码,根据不同平台设置cpath目录(当在lua代码中第一次require该模块就会调用该加载器加载模块)

cocos2dx提供的luabinding自动绑定工具

cocos2d-x提供了一个lua自动绑定工具,我们可以自定义库函数,然后修改自动绑定的工具脚本和配置文件,运行脚本后就可以生成lua绑定的C代码,大大节省了lua绑定的时间,lua-binding工具位于项目 tools/tolua 目录下, 可以看到里面有genbindings.py, xx.ini等文件, 这些就是自定义代码导出策略的配置文件。

  • 配置流程:
    • 1.定义库函数,例如MyClass.cpp,MyClass.h
    • 2.修改genbindings.py文件,这是生成lua-binding代码的脚本, 代码中的cmd_args = {'MyClass.ini' : ('mytestclass', 'lua_myclass_auto')}参数配置了需要导出的不同lua模块, 根据需要在这里添加条目即可. 括号里的第一个参数代表了模块名, 第二个参数代表了生成文件的名字。如果想要自定义生成目录, 可以修改output_dir变量。
    • 3.定义自己的.ini文件,这里是导出配置的关键, cocos2dx.ini为例,以下是一些重要字段的作用:
      • [cocos2d-x] 这里对应上文介绍的模块名, 要和第一个参数保持一致
      • prefix 给所有生成的c函数添加前缀, 防止命名重复。
      • target 导出的lua模块的命名空间。
      • headers 这个参数可以设置一组头文件, 程序根据这个头文件及其包含的头文件, 抽取出c++声明的类, 作为导出对象。
      • classes 这个参数配置基于正则表达式的模式列表, 过滤上一个参数提取出的类, 得到最终导出的类列表。
      • skip 不想导出的类级函数, 用于处理一些和lua不兼容的c++参数, 比如 std::function, std::pair 等, 这些参数没法通过栈空间传递给lua, 因此也就没办法导出给lua使用, 需要排除掉。
    • 4.运行genbindings.py文件,运行前需要安装相关的工具,具体可以看tools/tolua/README.mdown。
    • 5.运行成功后,会生成lua绑定文件,将lua绑定文件放入工程中。
    • 6.修改功能根目录的CmakeLists.txt文件,将新增的库文件和lua绑定文件及其头文件加入到编译列表中。