lua与Cpp传递参数接口介绍

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

糖果

糖果
LUA教程

Lapis框架的常用处理方法

Lapis框架的常用处理方法 Continue reading

MoonScript实现选择排序

Published on February 26, 2017

MoonScript与Redis客户端

Published on January 19, 2017