社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
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 打开内置库的函数,如
在 Lua 5.1 中,可以直接使用 luaL_openlibs 来打开所有库函数,也可以根据需要,只打开需要使用的库
在 C++ 中使用 Lua C API,引用 Lua 头文件时,需要使用 extern “C”:
extern "C" {
#include <lua.h>
}
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 解释器,用于执行 Lua 脚本。这样可以实现系统和业务分离,系统层提供底层能力的支持,业务层使用 Lua 进行编写,可以大大提高业务层的开发效率。另外 C 调用 Lua 时提供了保护执行机制,即使 Lua 代码写得有问题,只会影响当前正在执行的 Lua 实例,不会导致整个系统崩溃
目前比较流行的 Redis 和 Nginx Openresty 中都使用了类似的技术:
C 调用 Lua 基本流程为:
Lua 的头文件为 lua.h,lauxlib.h 和 lualib.h。每个文件包含的内容前面已经介绍过,这里就不再赘述
lua_open 用于创建一个 Lua 栈,lua_open 实际是一个宏,它调用了 luaL_newstate
#define lua_open() luaL_newstate()
lua_State *luaL_newstate(void);
lua_State 即为一个 lua 栈
打开 Lua 库对应的函数在 lualib.h 中,前面已经说过,这里不再赘述
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_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_call 或 lua_pcall 执行完一个函数后,会将执行结果放到栈顶,如果有两个返回值,栈索引 -1 和 -2 就是返回值,如果有三个值,栈索引 -1,-2,-3 就是返回值,以此类推
获取这些返回值可以通过栈的操作来实现
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_calln");
lua_close(L);
}
int main(int argc, char** argv) {
if (argc != 2) {
printf("Usage: %s luafilen", 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 API,另一种方式是通过 Luajit ffi(个人更喜欢这种方式 ^_^),另外我也单独开过一篇 Luajit ffi 的使用文章 luajit ffi 小结,本文就不再探讨,本文主要探讨的是 Lua C API。
首先,并不是所有 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,也就是在函数处理结束前,需要向栈中推入两个元素
前面一节解决了 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 代码:
#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_calln");
lua_close(L);
}
int main(int argc, char** argv) {
if (argc != 2) {
printf("Usage: %s luafilen", 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
在实际应用中,我们新增加一个函数,并不会像上面那样,把函数和 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 链接库代码:
#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
我们以一个带 2 个参数,返回一个值的函数为例,函数调用方式为:
add(20.1, 10)
前面说过,自己写 Lua 解释器,在载入 Lua 文件时,实际是将整个文件作为一个函数压入到 Lua 栈顶,因为一般情况下没有传入参数,也没有返回参数,所以调用函数的参数个数和返回值个数都为 0
执行 C 链接库中的 luaopen_*,这个 * 与 require 的传入参数相同,如果不同会报以下错误
./libtest.so: undefined symbol: luaopen_libtest
我们以一个带 2 个参数,返回一个值的函数为例,该函数注册名为 add,注册函数指针为 l_add,函数调用方式为:
add(20.1, 10)
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!