最近在开源代码中遇到MySQL-Proxy, 其允许lua脚本实现用户的个性化配置, lua脚本可以引用C/C++的动态链接库完成一些复杂的功能. 本文对最近接触到的lua和C/C++混合的相关接口使用做个总结. 本文的完整代码在文末的附录中, 代码测试在Ubuntu16.04+lua5.1下完成, 不同版本可能API有所变化, 可以参考文末给出的官方文档链接.
相关环境配置
首先, 要在C++中使用相关的lua的工具, 需要lua.hpp这个头文件. 在Ubuntu下, 首先需要安装lua, 然后可以在/usr/include 目录下找到相关的头文件. 其他系统可能有所不同, 可以根据具体的头文件来进行设置, 完成这步以后, 就可以开始写相关程序.
1 2
| //Ubuntu16.04下的环境配置 sudo apt-get install lua5.1
|
HelloWorld
HelloWorld程序
为了能够快速了解怎么混合使用C++和lua, 这一小节先实现一个最简单的helloworld来了解整个程序的结构, 以及相应的需要注意的点, 然后介绍具体细节.
首先, 我们的目标是提供一个firstFile.so动态库文件, 给我们的lua脚本使用. 于是, 我们新建一个文件test.cpp, 写入以下内容.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include<lua5.1/lua.hpp> #include<lua5.1/lualib.h> #include<lua5.1/lauxlib.h> #include<iostream> extern "C" int luaopen_firstLib(lua_State *L); int InternalHello(lua_State* L) { std::cout<<"Hello World!"<<std::endl; return 0; } int luaopen_firstLib(lua_State *L){ static const luaL_reg Map[]={ {"look",InternalHello}, {NULL,NULL} }; luaL_register(L,"first",Map); return 1; }
|
然后使用如下的命令编译动态链接库firstLib.so
1
| g++ -fPIC -shared -o firstLib.so test.cpp -llua5.1
|
如果上面的配置环境没有错误的话, 这段代码应该正常编译, 并形成firstLib.so库文件.
我们使用lua look.lua命令执行如下的脚本, 就可以获得HelloWorld输出:
1 2 3
| package.cpath = "./?.so" require "firstLib" first.look()
|
HelloWorld的解释
我们现在对上面的helloworld做一定的解释, 如下:
首先, 要编写一个firstLib.so, 我们需要在C++文件中编写对应的函数luaopen_firstLib.这个函数的名字是和库文件的名字对应的, 且返回值是int, 参数列表是lua_State*.
需要为luaopen_firstLib函数添加extern “C”做声明.
在luaopen_firstLib函数中, 可以注册自己库中希望对lua脚本提供的函数. 注册的方法如下:
1 2 3 4 5 6 7 8 9 10 11 12
| int luaopen_firstLib(lua_State *L){ //1. 使用luaL_reg array类型进行注册 static const luaL_reg Map[]={ //2. 左边是字符串,表示对外提供的函数名. 右边是自己内部实现的函数名 {"look",InternalHello}, //3. 以NULL,NULL结尾 {NULL,NULL} }; //4. 调用注册函数, 其中first表示对外提供的库的名字 luaL_register(L,"first",Map); return 1; }
|
这里需要注意几个命名的规则:
我们需要的库文件的名称是firstLib.so, 所以需要编写luaopen_firstLib函数做初始化
每个函数在c++文件中有一个名字(如InternalHello), 在注册给lua脚本用的时候, 可以指定另外一个名字(如look)
注册的时候, 可以给自己的库起名字, 比如first
lua中使用动态库的代码注释如下:
1 2 3 4 5 6
| --指定lua寻找动态库的路径 package.cpath = "./?.so" --设置动态库, 并且调用luaopen_xxx函数进行初始化, 这里firstLib和库文件的名字对应 require "firstLib" --执行动态库中提供的函数, 这里的库引用和自己注册的时候提供的库名字对应 first.look()
|
对于自定义的函数, 其函数的返回值和参数列表是固定的, 不能改变, 如下:
1
| int (*lua_CFunction) (lua_State *L);
|
至此, 命名规则介绍完成, 我们可以编写任意的函数, 命名任意的库, 并且在lua脚本中进行调用. 剩下的部分, 就是传递参数了.
lua与c++传递参数
我在在C++中定义的函数只有一个参数, 即lua_State*, 我们需要通过这个参数来完成所有的参数传递, 以及传递返回值的功能, 这个功能基于lua的虚拟栈,并且需要使用一系列配套的函数来完成. 关于虚拟栈, 先可以简单理解成一个数组空间, lua要传参数给C++函数时, 就把数据放在这个数组中, C++函数从这个数组中读取数据. C++函数要返回数据时, 也把数据放在这个数组中, 这样lua脚本可以读取返回的数据, 所以虚拟栈就是两边通信的管道.这个虚拟栈可以通过下标访问,下表从1开始,1表示栈底.也可以接受负数的下标,-1表示栈顶. 后面小结将对其做具体介绍, 我们首先考虑从C++函数中返回内容给lua脚本的情况.
返回值
返回值可以使用如下的配套参数:
1 2 3 4 5 6
| void lua_pushnumber (lua_State *L, lua_Number n); void lua_pushnil (lua_State *L); void lua_pushinteger (lua_State *L, lua_Integer n); void lua_pushboolean (lua_State *L, int b); void lua_createtable (lua_State *L, int narr, int nrec); ...
|
其中对于普通内置类型, 只要使用固定的函数就可以了, 官方文档的描述也比较详细, 代码可以参考文末的附录. 下面只考虑如何传table(表)类型.需要注意的是, 每个函数结束的时候, 有一个int类型的返回值, 这个返回值表示该函数返回给lua脚本的参数个数.如果返回值和实际入栈的参数不同, 就会出现错误.
对于table类型, 有两种情况, 一种是一维的表, 其结构如下
key |
value |
k1 |
v1 |
k2 |
v2 |
k3 |
v3 |
可以看到, 这就是普通的lua中的一维key-value结构表, 要在C++函数中产生这样的表并返回, 需要经过以下的步骤:
首先, lua_createtable函数可以创建一个新表, 然后把其作为单个参数放到栈中. 这样, 栈中就增加了一个元素, 剩下的工作就是向表里面添加kv对. 比如要往一个表中添加4个kv对, 可以写如下的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| static void mylua_pushtable2(lua_State *const l){ --建立新的一维表, 入栈, 表中有4个kv对, 第二个参数设置为0, 第一个参数是lua_state* lua_createtable(l,0,4); --依次插入四个kv对, 先设置value, 然后设置key lua_pushstring(l,"v1"); lua_setfield(l,-2,"k1"); lua_pushstring(l,"v2"); lua_setfield(l,-2,"k2"); lua_pushstring(l,"v3"); lua_setfield(l,-2,"k3"); lua_pushstring(l,"v4"); lua_setfield(l,-2,"k4"); }
|
上面调用的函数setfield中的-2, 表示下标-2的参数, 也就是我们建立的table,现假设其名字是tableA,则 setfield达到的效果是,tableA[“key”]=top, 其中top是当前栈顶元素,并且同时栈顶元素出栈. top在这里正好就是v1.于是, 我们可以通过这样的方法设计一维的表返回. 如果需要key为int类型, 可以用lua_rawseti函数, 其函数签名如下:
1 2
| https://www.lua.org/manual/5.1/manual.html#2.8 void lua_rawseti (lua_State *L, int index, int n);
|
更多的函数介绍, 可以看官方文档.
还有一种情况是多维的表, 也即嵌套的表, 给出如下的例子:
1 2 3 4
| myTable = { [0] = { ["field1"] = 1, ["field2"] = 2,["field3"] = 3 }, [1] = { ["field1"] = 10, ["field2"] = 20,["field3"] = 30 } }
|
返回嵌套表的实例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| static void mylua_pushMultiTable(lua_State *const L){ lua_createtable(L, 2, 0); lua_pushnumber(L, 1); lua_createtable(L, 0, 3); lua_pushnumber(L, 1); lua_setfield(L, -2, "field1"); lua_pushnumber(L, 2); lua_setfield(L, -2, "field2"); lua_pushnumber(L, 3); lua_setfield(L, -2, "field3"); lua_settable(L, -3); lua_pushnumber(L, 2); lua_createtable(L, 0, 3); lua_pushnumber(L, 10); lua_setfield(L, -2, "field1"); lua_pushnumber(L, 20); lua_setfield(L, -2, "field2"); lua_pushnumber(L, 30); lua_setfield(L, -2, "field3"); lua_settable(L, -3); }
|
可以看到, 对于一个key对应内部value结构是一个表的情况, 需要用到lua_createtable的第二个参数, 表示最外层需要的项目个数.对于内部的每个表, 则再次使用建立一维表的方式来完成kv对的插入, 这里的lua_settable的作用和上面介绍的lua_setfield相似. 对于我们例子中的表, 外层有两个项目,key分别是0和1, 所以lua_createtable的第二个参数设置为2. 对于内层的表, 由于有三个项目, 所以lua_createtable的第三个参数设置为3.
接受参数
接受从lua脚本中传递的参数可以使用如下的函数:
1 2 3 4 5 6
| int lua_toboolean (lua_State *L, int index); double lua_tonumber (lua_State *L, int index); lua_Integer lua_tointeger (lua_State *L, int index); const char *lua_tolstring (lua_State *L, int index, size_t *len); int lua_next (lua_State *L, int index); ....
|
可以看到, 接受参数需要用户显式指定下标和数据类型, 这样lua传递过来的数据才能正确解析.
- 传string类型
lua传string类型是以const char * 来传递的, 是一个一’