Lua C API 研究 —— 基础篇

Lua C API 研究 —— 基础篇


Lua 提供了和 C 交互的 API,可以在 C 中执行 Lua 代码,也可以在 Lua 中执行 C 代码。两者都通过 Lua C API 实现。本文基于 Lua 5.1,参考 http://www.lua.org/pil/24.html

头文件

在 C/C++ 中使用 Lua C API,需要引入 Lua 的头文件:

  • lua.h

    lua.h 包含了 Lua 的基础函数,这些函数以 lua_ 开头

  • lauxlib.h

    lauxlib.h 包含了 Lua 的辅助函数(称为 auxiliary library 或 auxlib),这些函数以 luaL_ 开头

  • lualib.h

    lualib.h 包含了 Lua 打开内置库的函数,如

    • luaopen_base(L); /* opens the basic library */
    • luaopen_table(L); /* opens the table library */
    • luaopen_io(L); /* opens the I/O library */
    • luaopen_string(L); /* opens the string lib. */
    • luaopen_math(L); /* opens the math lib. */

    在 Lua 5.1 中,可以直接使用 luaL_openlibs 来打开所有库函数,也可以根据需要,只打开需要使用的库

在 C++ 中使用 Lua C API,引用 Lua 头文件时,需要使用 extern “C”:

extern "C" {
#include <lua.h>
}

Lua 栈

Lua 与 C 通过 Lua 栈(lua_State *L)来进行参数传递,Lua 与 C 的互调,就是通过 Lua C API 对 Lua 栈进行操作

在 Lua 代码中,严格遵守 LIFO 的原则,只能操作 Lua 栈的栈顶。在 C 代码中,则可以操作栈中任意元素,甚至可以在栈的任意位置删除和插入元素

在 Lua 栈中可以存放各种类型的变量,如 number,string,可以存放函数,线程等

如果栈中有 4 个元素,如果以正数来表示,栈顶索引为 4,栈底索引为 1;如果以负数来表示,栈顶索引为 -1,栈底索引为 -4。索引 0 为保留槽,不要去对其进行操作

Lua 栈操作很多,后面会单开一篇进行探讨,这里不再进行详述

C 调用 Lua

应用场景

在 C 中调用 Lua,可以实现在用户的 C 程序中集成一个 Lua 解释器,用于执行 Lua 脚本。这样可以实现系统和业务分离,系统层提供底层能力的支持,业务层使用 Lua 进行编写,可以大大提高业务层的开发效率。另外 C 调用 Lua 时提供了保护执行机制,即使 Lua 代码写得有问题,只会影响当前正在执行的 Lua 实例,不会导致整个系统崩溃

目前比较流行的 Redis 和 Nginx Openresty 中都使用了类似的技术:

  • 如 Redis 中加载 Lua 脚本,通过 EVAL 命令可以在 Redis 服务端执行一段 Lua 脚本,以实现类似于存储过程的功能。
  • 在 Nginx Openresty 中,可以将 Lua 脚本加载于 Nginx 的配置中,当外部访问指定 URI 时,Nginx 可以执行相应的 Lua 脚本,实现一些复杂的逻辑,如数据库操作,Redis 操作,甚至向外发送 HTTP 请求等

基本流程

C 调用 Lua 基本流程为:

  1. 引入 Lua 头文件
  2. 创建 Lua 栈
  3. 打开需要使用的 Lua 库
  4. 加载 Lua 代码
  5. 执行 Lua 代码
  6. 获取 Lua 代码执行结果
  7. 关闭 Lua 栈

引入 Lua 头文件

Lua 的头文件为 lua.h,lauxlib.h 和 lualib.h。每个文件包含的内容前面已经介绍过,这里就不再赘述

创建 Lua 栈

lua_open 用于创建一个 Lua 栈,lua_open 实际是一个宏,它调用了 luaL_newstate

#define lua_open()  luaL_newstate()
lua_State *luaL_newstate(void);

lua_State 即为一个 lua 栈

打开需要使用的 Lua 库

打开 Lua 库对应的函数在 lualib.h 中,前面已经说过,这里不再赘述

加载 Lua 代码

Lua 代码可以通过内存的方式进行加载,也可以通过文件的方式进行加载

int luaL_loadbuffer (lua_State *L,
                     const char *buff,
                     size_t sz,
                     const char *name);

int luaL_loadfile (lua_State *L, const char *filename);

int luaL_loadstring (lua_State *L, const char *s);

这三个函数底层都调用了 lua_load,该函数会对传入的 Lua 代码进行一次预编译,检查代码中的语法错误。如果没有错误,返回 0,并将编译好的代码块作为函数放到 Lua 栈顶。如果错误,则向栈顶推入一条错误信息,错误信息可以通过 lua_tostring(L, -1) 直接获取

注意,这里的函数这个概念很重要,即使是加载一个 Lua 脚本文件(文件并不是以函数方式进行定义的),在 Lua 中都会把整个文件当成一个函数进行处理,即整个文件就是一个 Lua 函数,并且这个函数放到 Lua 栈的栈顶

执行 Lua 代码

上一步中将 Lua 代码作为函数放到了 Lua 栈顶,就可以通过调用 lua_call 或 lua_pcall 来执行 Lua 代码了

void lua_call (lua_State *L, int nargs, int nresults);
int lua_pcall (lua_State *L, int nargs, int nresults, int errfunc);

lua_call

如上,lua_call 中指定了函数的传入参数个数 nargs 和函数返回结果个数 nresults,以下是一个官方文档中,lua 函数调用与 lua_call 操作对应关系。里面涉及一些复杂的 Lua 栈操作,后面会单开一张进行探讨。

Lua 代码

a = f("how", t.x, 14)

Lua 代码在 C 中的执行代码

lua_getfield(L, LUA_GLOBALSINDEX, "f"); /* function to be called */
lua_pushstring(L, "how");                        /* 1st argument */
lua_getfield(L, LUA_GLOBALSINDEX, "t");   /* table to be indexed */
lua_getfield(L, -1, "x");        /* push result of t.x (2nd arg) */
lua_remove(L, -2);                  /* remove 't' from the stack */
lua_pushinteger(L, 14);                          /* 3rd argument */
lua_call(L, 3, 1);     /* call 'f' with 3 arguments and 1 result */
lua_setfield(L, LUA_GLOBALSINDEX, "a");        /* set global 'a' */

由上,lua_call 中指定了几个参数,就需要向 Lua 栈中推入几个参数

lua_pcall

如果 Lua 代码执行过程中没有任何错误,lua_pcall 的行为与 lua_call 是相同的。如果在执行的过程中有错误发生,lua_pcall 会捕捉该错误,并将错误信息推送到 Lua 栈上,并返回一个错误码。

lua_pcall 最后一个参数 errfunc,指定错误处理函数在 Lua 栈中的位置

一般系统嵌入 Lua 代码,都是使用 lua_pcall,调用方法一般都是:

lua_pcall (l, 0, 0, 0)

获取 Lua 代码执行结果

使用 lua_call 或 lua_pcall 执行完一个函数后,会将执行结果放到栈顶,如果有两个返回值,栈索引 -1 和 -2 就是返回值,如果有三个值,栈索引 -1,-2,-3 就是返回值,以此类推

获取这些返回值可以通过栈的操作来实现

关闭 Lua 栈

Lua 栈使用 lua_close 进行关闭

void lua_close (lua_State *L);

示例代码

C 代码:

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

#include <stdio.h>

void load (char *filename) {
    lua_State *L = lua_open();
    luaopen_base(L);
    luaopen_io(L);
    luaopen_string(L);
    luaopen_math(L);

    if (luaL_loadfile(L, filename) || lua_pcall(L, 0, 0, 0))
    if (luaL_loadfile(L, filename))
        error(L, "cannot run configuration file: %s",
            lua_tostring(L, -1));

    printf("end lua_call\n");

    lua_close(L);
}

int main(int argc, char** argv) {
    if (argc != 2) {
        printf("Usage: %s luafile\n", argv[0]);
        return -1;
    }

    load(argv[1]);

    return 0;
}

lua 代码:

print("Hello World")

编译(我这里装的 luajit,luajit 的链接库放在 /usr/local/lib 下):

$ gcc -g -o aa main.c -I/usr/local/include/luajit-2.1/ -lluajit-5.1

执行:

$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
$ ./aa test.lua 
Hello World
end lua_call

再来看一下前面提到过的 Lua 的保护机制,我们修改 Lua 代码为:

print("Hello World")
a = 10/0
print(a)

执行:

$ ./aa test.lua 
Hello World
inf
end lua_call

Lua 代码除 0 异常退出了,但是,整个 C 代码并没有因此崩溃,而是继续执行,并打印了 end lua_call。这里就点到为止,不再展开了。

Lua 调用 C

Lua 调用 C 的函数,有两种方式,一种是通过 Lua C API,另一种方式是通过 Luajit ffi(个人更喜欢这种方式 ^_^),另外我也单独开过一篇 Luajit ffi 的使用文章 luajit ffi 小结,本文就不再探讨,本文主要探讨的是 Lua C API。

C 函数原型

首先,并不是所有 C 函数都可以使用 Lua C API 进行调用的,能够调用的 C 函数必须遵从 Lua C API 定义的函数原型

typedef int (*lua_CFunction) (lua_State *L);

Lua 每次调用一个 C 函数时,每个 C 函数中传入的 L 都是一个本地栈,这样避免了栈之间的互相干扰。第一个参数在栈中的索引为 1,第二个参数索引为 2,依次类推。C 函数的返回值即为函数返回参数个数。如下面的 Lua 代码

a, b = add(10, 20)

add 对应的 C 函数,函数 Lua 栈索引为 1 的元素为 10,索引为 2 的元素为 20,函数返回值为 2,也就是在函数处理结束前,需要向栈中推入两个元素

C 函数注册

前面一节解决了 Lua 调用 C 函数的方法,但是 Lua 怎么找到 C 函数,这就需要将 C 函数注册到 Lua 的运行栈中,并给它一个 Lua 能够识别的名字

我们使用 lua_pushcfunction 注册函数

void lua_pushcfunction (lua_State *L, lua_CFunction f);

使用 lua_setglobal 指定函数名,该函数实际是弹出栈顶第一个元素,并把该元素设置在全局空间,并给其全局空间的名字

void lua_setglobal (lua_State *L, const char *name);

C 函数注册示例

C 代码:

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

#include <stdio.h>

static int l_add(lua_State *L) {
    double a = lua_tonumber(L, 1);
    double b = lua_tonumber(L, 2);
    lua_pushnumber(L, a+b);
    return 1;
}

void load (char *filename) {
    lua_State *L = lua_open();
    luaopen_base(L);
    luaopen_io(L);
    luaopen_string(L);
    luaopen_math(L);

    lua_pushcfunction(L, l_add);
    lua_setglobal(L, "add");

    if (luaL_loadfile(L, filename) || lua_pcall(L, 0, 0, 0))
    if (luaL_loadfile(L, filename))
        error(L, "cannot run configuration file: %s",
            lua_tostring(L, -1));

    printf("end lua_call\n");

    lua_close(L);
}

int main(int argc, char** argv) {
    if (argc != 2) {
        printf("Usage: %s luafile\n", argv[0]);
        return -1;
    }

    load(argv[1]);

    return 0;
}

Lua 代码:

print("Hello World")
a = add(10.1, 20)
print(a)

执行:

$ ./aa test.lua 
Hello World
30.1
end lua_call

C 链接库

在实际应用中,我们新增加一个函数,并不会像上面那样,把函数和 Lua 解释器放在一起,特别是常用的解释器可能直接是 lua 或 luajit,也不可能直接去修改它们的代码。最常用的方式是将自己要使用的函数封装成 C 链接库,然后通过加载 C 链接库的方式,调用链接库中的 C 函数。

在 C 链接库中怎么注册要使用的函数?如同上面所说,我们不可能去修改 Lua 解释器的代码去注册用户 C 链接库中的函数。实际上 Lua 提供了一个入口函数用于注册 C 链接库中的函数,这个函数的命名方式为 luaopen_*,其执行原理为:当用户在 Lua 中执行 require(“mylib”)时,Lua 解释器就会去寻找 mylib.so,并执行 luaopen_mylib。

luaopen_* 与 Lua C 函数的原型相同,为:

typedef int (*lua_CFunction) (lua_State *L);

C 链接库示例

C 链接库代码:

#include <lua.h>

static int l_add(lua_State *L) {
    double a = lua_tonumber(L, 1);
    double b = lua_tonumber(L, 2);
    lua_pushnumber(L, a+b);
    return 1;
}

int luaopen_libtest(lua_State *L) {
    lua_pushcfunction(L, l_add);
    lua_setglobal(L, "add");

    return 1;
}

Lua 代码:

require("libtest")

print("Hello World")
a = add(10.1, 20)
print(a)

C 链接库编译:

gcc -g -o libtest.so -shared -fPIC test.c -I/usr/local/include/luajit-2.1/ -lluajit-5.1

执行 Lua 代码:

$ lua test.lua 
Hello World
30.1

这里需要注意,Lua 代码中 require 的库名和 C 链接库中的 luaopen_* 以及最后编译生成的库名必须保持一致

更高级的函数注册

前面使用的 lua_pushcfunction + lua_setglobal 进行函数注册的方法,对于只有一两个函数时问题不大,如果函数比较多,使用起来就比较繁琐,有没有更好的方法来注册函数?答案是有,这种方法就是 luaL_Reg + luaL_register

typedef struct luaL_Reg {
  const char *name;
  lua_CFunction func;
} luaL_Reg;

void luaL_register(lua_State *L, const char *libname, const luaL_Reg *l);

更高级的函数注册示例

我们在上面链接库代码的基础上进行修改,修改 luaopen_libtest 函数实现为

int luaopen_libtest(lua_State *L) {
    luaL_Reg lua_reg[] = {
        {"add", l_add},
        {NULL, NULL}
    };
    luaL_register(L, "aa", lua_reg);

    return 1;
}

这里使用了 luaL_* 的库,需要增加

#include <lauxlib.h>

luaL_register 第二个参数会创建一个 table,而对注册的函数,都需要使用该 table 进行引用,因此 Lua 代码修改为:

require("libtest")

print("Hello World")
a = aa.add(10.1, 20)
print(a)

执行 Lua 代码:

$ lua test.lua 
Hello World
30.1

Lua 和 C 互调模型小结

C 调用 Lua 函数

  1. C 调用 Lua,实际上是先向栈顶压入 Lua 函数
  2. 再向栈顶依次压入参数
  3. 调用 lua_call 或 lua_pcall 执行 lua 函数(这里决定了函数在栈中的位置)
  4. lua 函数执行完成,向栈顶压入返回值
  5. 取出返回值,得到函数执行结果

我们以一个带 2 个参数,返回一个值的函数为例,函数调用方式为:

add(20.1, 10)

前面说过,自己写 Lua 解释器,在载入 Lua 文件时,实际是将整个文件作为一个函数压入到 Lua 栈顶,因为一般情况下没有传入参数,也没有返回参数,所以调用函数的参数个数和返回值个数都为 0

Lua 加载 C 链接库

  1. require 找到对应的 C 链接库(lua 库搜索路径为 package.path,C 库搜索路径为 package.cpath)
  2. 执行 C 链接库中的 luaopen_*,这个 * 与 require 的传入参数相同,如果不同会报以下错误

    ./libtest.so: undefined symbol: luaopen_libtest
    

Lua 执行 C 函数

  1. 通过名字找到 C 函数的函数指针
  2. 创建一个 local Lua 栈
  3. 将传入参数压入 Lua 栈中
  4. 执行 C 函数
  5. C 函数将返回值压入 Lua 栈中
  6. C 函数返回返回值的个数

我们以一个带 2 个参数,返回一个值的函数为例,该函数注册名为 add,注册函数指针为 l_add,函数调用方式为:

add(20.1, 10)

展开阅读全文
©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值