c/c++导出lua绑定


【转载】https://note.youdao.com/share/?id=0f4132271151c4b62f9afb712e8304d9&type=note#/

1、在纯C环境下,把C函数注册进Lua环境,理解Lua和C之间可以互相调用的本质
2、在cocos2d-x项目里,把纯C函数注册进Lua环境,理解cocos2d-x是怎样创建Lua环境的、以及怎样得到这个环境并继续自定义它
3、了解为什么要使用toLua++来注册C++类
4、在纯C++环境下,使用toLua++来把一个C++类注册进Lua环境,理解toLua++的用法
5、在cocos2d-x项目里,使用cocos2d-x注册自身的方式把自定义的C++类注册进Lua环境,理解cocos2d-x是怎样通过bindings-generator脚本来封装toLua++的用法来节省工作量的  

第一层:纯C环境下,把C函数注册进Lua环境

直接看代码比啰哩啰嗦讲一大堆概念要清晰明了的多。建立一个a.lua和一个a.c文件,内容如下,一看就明白是怎么回事了:

a.lua

print(foo(99))

a.c

#include 
#include 
#include 
int foo(lua_State *L){
  int n = lua_tonumber(L, 1); //取栈底值(传入的第一个参数)
  lua_pushnumber(L, n + 1);   //将结果入栈
  return 1;
}

int main(){
  lua_State *L = lua_open();   //初始哈
  luaL_openlibs(L);            //打开lua标准库
  lua_register(L, "foo", foo); //注册c函数到lua环境
  luaL_dofile(L, "a.lua");     //执行lua脚本
  lua_close(L);                //关闭环境
  return0;
}

第二层:在cocos2d-x环境下,把C函数注册进Lua环境

AppDelegate.cpp文件中的关键代码如下:
auto engine = LuaEngine::getInstance();
ScriptEngineManager::getInstance()->setScriptEngine(engine);

LuaStack* stack = engine->getLuaStack();
stack->setXXTEAKeyAndSign("2dxLua", strlen("2dxLua"), "XXTEA", strlen("XXTEA"));

//register custom function
//LuaStack* stack = engine->getLuaStack();
//register_custom_function(stack->getLuaState());

可以看到cocos2d-x已经为我们留出了注册自定义C函数的位置,在注释代码后面这么写就可以了: lua_State *L = stack->getLuaState(); lua_register(L, "test_lua_bind", test_lua_bind);

也可以通过ScriptEngineManager类从头取得当前的LuaEngine对象,然后再getLuaStack()方法得到封装的LuaStack对象,再调用getLuaState()得到原始的lua_State结构指针。。

2、接下来,找个地方把test_lua_bind函数定义写进去就算大功告成了。

int test_lua_bind(lua_State *L){
    int number = lua_tonumber(L, 1);

    number = number + 1;

    lua_pushnumber(L, number);

    return1;
}

3、大功告成,现在就可以在main.lua文件里使用test_lua_bind()函数了:

local i = test_lua_bind(99)
print("lua bind: " .. tostring(i))

4、如果是新建一个.c文件呢?把AppDelegate.cpp文件里test_lua_bind函数定义的代码删掉,在头部#include后面加入:

#include "test_lua_bind.h"

frameworks/runtime-src/Classes目录下创建test_lua_bind.h文件,内容如下:

extern"C" {
    #include "lua.h "#include "lualib.h"
}

int test_lua_bind(lua_State *L);

再创建test_lua_bind.c文件,内容不变:

#include "test_lua_bind.h"
int test_lua_bind(lua_State *L){
    int number = lua_tonumber(L, 1);

    number = number + 1;

    lua_pushnumber(L, number);

    return1;
}

普通的C/C++项目都是用Makefile来指定编译哪些.c/cpp文件的,当前的cocos2d-x项目虽然没有Makefile文件,但也是遵循这个原则的,也即肯定是有一个地方来指定所有要编译的文件的,需要在这个地方把test_lua_bind.c加进去,使得整个项目编译时把它也作为项目的一部分。

答案是,cocos2d-x项目没有使用Makefile,而是非常聪明地使用了与具体环境相关的工程文件来作为命令行编译的环境,比如在编译iOS或Mac时就使用Xcode工程文件,在编译Android时就使用Android.mk文件。

所以,添加好了test_lua_bind.htest_lua_bind.c文件后,将这俩文件添加进工程中就行了。

Xcode中添加.h和.cpp文件进工程

Xcode中添加.h和.cpp文件进工程

再去命令行执行cocos compile -p win,编译就能成功了。

第三层:了解为什么要使用toLua++来注册C++类

因为Lua的本质是C,不是C++,Lua提供给C用的API也都是基于面向过程的C函数来用的,要把C++类注册进Lua形成一个一个的table环境是不太容易办到的事,因为这需要把C++类变成各种其他类型注册进Lua。单纯地手写lua_register()等代码来注册C++类是行不通的、代价高昂的,所以需要借助toLua++这个工具。

只有理解了手工用lua_register()去注册C++类的难度,才能理解使用toLua++这类工具的必要性。只有理解了使用toLua++工具的必要性,才会潜下心来冷静地接受toLua++本身的优点和缺点。只有看到了toLua++本身的缺点和使用上的麻烦,才会真心理解cocos2d-x使用bindings-generator脚本带来的好处。只有理解了bindings-generator脚本带来的好处,才能谅解这个脚本本身在使用上的一些不便之处。

第四层:在纯C++环境下,使用toLua++来把一个C++类注册进Lua环境

理解toLua++本身的用法还是必要的,知道了toLua++原本的用法,才能更好地理解cocos2d-x是怎么把自己的C++类都注册进Lua环境的。

使用toLua++的标准做法是:

1、定义实现c++类
2、仿造这个类的.h文件,改一个.pkg文件出来,具体格式要按照toLua++的规定,比如移除所有的private成员等
3、建一个专门用来桥接C++和Lua之间的C++类,使用特殊的函数签名来写它的.h文件,.cpp文件不写,等着toLua++来生成
4、给这个桥接的C++类写一个.pkg文件,按照toLua++的特殊格式来写,目的是把真正做事的C++类给定义进去
5、在命令行下用toLua++生成桥接类的.cpp文件
6、程序入口引用这个桥接类,执行生成的桥接函数,Lua环境中就可以使用真正做事的C++类了

下面我以尽量最少的代码来走一遍toLua++的流程,注意这是在纯C++环境下,跟任何框架都没关系,也不考虑内存释放等细节:

MyClass.h

class MyClass {
public:
  MyClass() {};
  int foo(int i);
};

MyClass.cpp

#include "MyClass.h"

int MyClass::foo(int i)
{
  return i + 100;
}

MyClass.pkg c++类的pkg文件

class MyClass
{
  MyClass();
  int foo(int i);
};

MyLuaModule.h 桥接类

extern "C" {
    #include "tolua++.h"
}

#include "MyClass.h"

TOLUA_API int tolua_MyLuaModule_open(lua_State* tolua_S);

MyLuaModule.pkg 桥接类的pkg文件

$#include "MyLuaModule.h"

$pfile "MyClass.pkg"

main.cpp

extern "C" { 
  #include 
  #include 
  #include 
}

#include "MyLuaModule.h"

int main()
{
  lua_State *L = lua_open();

  luaL_openlibs(L);

  tolua_MyLuaModule_open(L);

  luaL_dofile(L, "main.lua");

  lua_close(L);

  return 0;
}

main.lua

local test = MyClass:new()
print(test:foo(99))

先在命令行下执行:(生成桥接文件的实现体MyLuaModule.cpp)

tolua++ -o MyLuaModule.cpp MyLuaModule.pkg

意命令行中-o参数的顺序不能随意摆放,

生成好MyLuaModule.cpp文件后,就能看到它里面的那一大堆桥接代码了,比如tolua_beginmoduletolua_function等。

以后看到这些东西就不陌生了,就明白这些函数只是toLua++用来做桥接的必备代码了,

简单看一下代码,就理解toLua++是怎样把MyClass这个C++类注册进Lua中的了:

toLua++生成的桥接代码

toLua++生成的桥接代码

接下来,用g++来编译:

g++ MyClass.cpp MyLuaModule.cpp main.cpp -llua -ltolua++

默认就生成了a.out文件,执行,就能看到main.lua的执行结果了:

toLua++的执行结果

toLua++的执行结果

至此,对toLua++的运作原理心里就透亮了,无非就是:

1、把自己该写的类写好
2、写个.pkg文件,告诉toLua++这个类暴露出哪些接口给Lua环境
3、再写个桥接的.h和.pkg文件,让toLua++去生成桥接代码
4、在程序里使用这个桥接代码,类就注册进Lua环境里了


第五层:使用cocos2d-x的方式来将C++类注册进Lua环境

所以从cocos2d-x 3.x开始,用bindings-generator脚本代替了toLua++。

bindings-generator脚本的工作机制是:

1、不用挨个类地写桥接.pkg和.h文件了,直接定义一个ini文件,告诉脚本哪些类的哪些方法要暴露出来,注册到Lua环境里的模块名是什么
2、摸清了toLua++工具的生成方法,改由Python脚本动态分析C++类,自动生成桥接的.h和.cpp代码,不调用tolua++命令了
3、虽然不再调用tolua++命令了,但是底层仍然使用toLua++的库函数,比如tolua_function,bindings-generator脚本生成的代码就跟使用toLua++工具生成的几乎一样

bindings-generator脚本掌握了生成toLua++桥接代码的主动权,不仅可以省下大量的.pkg和.h文件,而且可以更好地插入自定义代码,达到cocos2d-x环境下的一些特殊目的,比如内存回收之类的。

接下来说怎么用bindings-generator脚本:

1、写自己的C++类,按照cocos2d-x的规矩,继承cocos2d::Ref类,以便使用cocos2d-x的内存回收机制。
2、编写一个.ini文件,让bindings-generator可以根据这个配置文件知道C++类该怎么暴露出来
3、修改bindings-generator脚本,让它去读取这个.ini文件
4执行bindings-generator脚本,生成桥接C++类方法
5、将自定义的C++类和生成的桥接文件加入工程,不然编译不到
6、修改AppDelegate.cpp,执行桥接方法,自定义的C++类就注册进Lua环境里了

 

看着步骤挺多,其实都狠简单。下面一步一步来。

1.首先是自定义的C++类。我习惯将文件保存在frameworks/runtime-src/Classes/目录下:

frameworks/runtime-src/Classes/MyClass.h

#include "cocos2d.h"

using namespace cocos2d;

class MyClass : public Ref
{
public:
  MyClass()   {};
  ~MyClass()  {};
  bool init() { return true; };
  CREATE_FUNC(MyClass);

  int foo(int i);
};

frameworks/runtime-src/Classes/MyClass.cpp

#include "MyClass.h"

int MyClass::foo(int i)
{
  return i + 100;
}

2.然后编写.ini文件。在frameworks/cocos2d-x/tools/tolua/目录下能看到genbindings.py脚本和一大堆.ini文件,这些就是bindings-generator的实际执行环境了。随便找一个内容比较少的.ini文件,复制一份,重新命名为MyClass.ini。大部分内容都可以凑合不需要改,这里仅列出必须要改的重要部分:

frameworks/cocos2d-x/tools/tolua/MyClass.ini

[MyClass]
prefix           = MyClass
target_namespace = my
headers          = %(cocosdir)s/../runtime-src/Classes/MyClass.h
classes          = MyClass

也即在MyClass.ini中指定MyClass.h文件的位置,指定要暴露出来的类,指定注册进Lua环境的模块名。

然后修改genbindings.pyMyClass.ini文件加进去:

3.frameworks/cocos2d-x/tools/tolua/genbindings.py

cmd_args = {'cocos2dx.ini' : ('cocos2d-x', 'lua_cocos2dx_auto'), \
            'MyClass.ini' : ('MyClass', 'lua_MyClass_auto'), \
            ...

4.至此,生成桥接文件的准备工作就做好了,执行genbindings.py脚本:

python ./genbindings.py

成功执行genbindings.py脚本后,会在frameworks/cocos2d-x/cocos/scripting/lua-bindings/auto/目录下看到新生成的文件:

成功执行genbindings.py后生成的桥接C++文件

成功执行genbindings.py后生成的桥接C++文件

每次执行genbindings.py脚本时间都挺长的,因为它要重新处理一遍所有的.ini文件,建议大胆修改脚本文件,灵活处理,让它每次只处理需要的.ini文件就可以了,比如像这个样子:

修改genbindings.py使其只生成自定义的桥接类

修改genbindings.py使其只生成自定义的桥接类

frameworks/cocos2d-x/cocos/scripting/lua-bindings/auto/目录下观察一下生成的C++桥接文件lua_MyClass_auto.cpp,里面的注册函数名字为register_all_MyClass(),这就是将MyClass类注册进Lua环境的关键函数:

生成的桥接文件内容

生成的桥接文件内容

5.编辑frameworks/runtime-src/Classes/AppDelegate.cpp文件,首先在文件头加入对lua_MyClass_auto.hpp文件的引用:

AppDelegate.cpp文件的头加入对lua_MyClass_auto.hpp文件的引用

AppDelegate.cpp文件的头加入对lua_MyClass_auto.hpp文件的引用

然后在正确的代码位置加入对register_all_MyClass函数的调用:

修改AppDelegate.cpp文件,将MyClass类注册进Lua环境

修改AppDelegate.cpp文件,将MyClass类注册进Lua环境

如何是lua工程则在:lua_module_register.h 中添加上述调用。

最后在执行编译前,将新加入的这几个C++文件都加入到Xcode工程中,使得编译环境知道它们的存在:

在Xcode中加入新冒出来的C++文件

在Xcode中加入新冒出来的C++文件

这其中还有一个小坑,由于lua_MyClass_auto.cpp文件要引用MyClass.h文件,而这俩文件分属于不同的子项目,互相不认识头文件的搜寻路径,因此需要手工修改一下cocos2d_lua_bindings.xcodeproj子项目的User Header Search Paths配置。特别注意一共有几个../

需要修改cocos2d_lua_bindings子项目的User Header Search Paths配置

需要修改cocos2d_lua_bindings子项目的User Header Search Paths配置

最后,就可以用cocos compile -p mac命令重新编译整个项目了,不出意外的话编译一定是成功的。

修改main.lua文件中,尝试调用一下MyClass类:

local test = my.MyClass:create()
print("lua bind: " .. test:foo(99))
6.android上运行的话需要做的事情是要将生成的桥接文件lua_MyClass_auto.cpp放到android.mk中。

配置ini时需要注意的选项:

  • [title]:要配置将被使用的工具/ tolua的/ gengindings.py脚本的称号。一般来说,标题可以是文件名。

  • prefix:要配置一个函数名的前缀,通常,我们还可以使用文件名作为前缀 生成函数一次为前缀。

  • target_namespace:要配置在脚本层模块的名字。在这里,我们使用cc作为模块名,当你想在脚本层REF的名称,您必须将一个名为前缀,CC在名称的前面。例如,CustomClass可以参考作为cc.CustomClass

  • headers:要配置所有需要解析的头文件和%(cocosdir)s是的Cocos2d-x的引擎的根路径。

  • classes:要配置所有绑定所需的类。在这里,它支持正则表达式。因此,我们可以设置MyCustomClass。*在这里,用于查找多个特定的用法,你可以对照到tools/tolua/cocos2dx.ini

  • skip:要配置需要被忽略的功能。现在绑定发电机无法解析的void *类型,并委托类型,所以这些类型的需要进行手动绑定。而在这种情况下,你应该忽略所有这些类型,然后再手动将它们绑定。你可以对照到配置文件路径下的cocos/scripting/lua-bindings/auto 。

  • rename_functions:要配置的功能需要在脚本层进行重命名。由于某些原因,开发者希望更多的脚本友好的API,所以配置选项就是为了这个目的。

  • rename_classes:不在使用。

  • remove_prefix:不在使用。

  • base_classes_to_skip = #当被它们的子类发现的时候会跳过的基类
  • classes_have_no_parents:要配置是过滤器所需要的父类。这个选项是很少修改。

  • abstract_classes:要配置的公共构造并不需要导出的类。

  • script_control_cpp:是的。要配置脚本层是否管理对象的生命周期。如果没有,那么C++层关心他们的生命周期。 
    现在,它是不完善的,以控制原生对象的续航时间在脚本层。所以,你可以简单地把它设置为no。