lua与Cpp传递参数接口介绍

5.1k 词

最近在开源代码中遇到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 * 来传递的, 是一个一’