Lua 是一门非常简洁的动态语言,简洁到连 class 的概念都没有,没有明确的 OOP 系统,全靠 table 走天下。最近需要拿它嵌入到程序里面做动态计算的逻辑,遂记下 Lua 与 C 交互的一些要点。
Lua 的 C API 基本都在 lua.h 和 luaxlib.h 中,前者提供基础设施(Lua 环境创建,注册、执行 Lua 函数等)系列函数以 lua_
作为前缀,后者为前者提供了一层抽象作为辅助层,函数以为 luaL_
开头。一个简单的 C 宿主内执行 Lua 脚本的例子:
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
int main() {
lua_State *l = luaL_newstate();
luaL_openlibs(l); // 打开 Lua 标准库,因为使用到 print 函数
const char *script = R"(
print("print from Lua env");
)";
luaL_loadstring(l, script);
lua_call(l, 0, 0); // 执行刚 load 进去的脚本,两个参数 0 分别代表参数以及返回值个数
lua_close(l);
}
Lua 与 C 之间使用一个 stack 作为数据交换的容器,一方压入数据,另一方取出。Stack 上的元素使用整数索引,从栈底到栈顶为 1, 2 .. n
,但通常操作元素时候经常要从栈顶开始,但又懒得通过lua_gettop
函数获取栈顶元素的索引,所以-1, -2 .. -n
作为索引,获取栈顶到栈底的元素也是可以的。
lua.h 中声明了用于操作这个 stack 的函数,其中下面四组(当然是没有列全)用于数据交换:
// push functions (C -> stack)
LUA_API void (lua_pushnumber) (lua_State *L, lua_Number n);
...
// access functions (stack -> C)
LUA_API lua_Number (lua_tonumberx) (lua_State *L, int idx, int *isnum);
...
// get functions (Lua -> stack)
LUA_API int (lua_getglobal) (lua_State *L, const char *name);
...
// set functions (stack -> Lua)
LUA_API void (lua_setglobal) (lua_State *L, const char *name);
...
下面的一个简单的实例,展示了 Lua 如何与宿主程序通信,从 stack 上获取配置的变量:
-- 服务器下发的配置文件 config.lua
width = 200
height = 300
// 宿主程序 main.c
luaL_loadfile(L, "config.lua");
lua_call(L, 0, 0))
lua_getglobal(L, "width");
lua_getglobal(L, "height");
if (lua_isnumber(L, -2)) // -1 栈顶为 height, -2 为 width
error(L, "width is not a number\n");
if (lua_isnumber(L, -1))
error(L, "height is not a number\n");
int w = lua_tointeger(L, -2);
int h = lua_tointeger(L, -1);
在上面的例子已经可以看到了,lua_call 可以调用函数、执行脚本。如果我们需要在 Lua 里面发起与 C 交互需要:
lua_pushcclosure
将 lua_CFunction
类型回调函数压入栈上;lua_setglobal
将回调函数和函数名设置到 Lua 中;如果 C 要调用 Lua 函数:
lua_getglobal
取出 Lua 函数到栈上;综合地来看一个例子,先定义一个回调函数:
static int KTNativeFunction(lua_State *state) {
int parameterCount = lua_gettop(state);
for (int i = 1; i <= parameterCount; i++) {
int type = lua_type(state, i); // see `basic types` in lua.h
}
// 这里可以压返回值(们)入栈
return 0; // 返回值数目
}
然后主程序代码:
int main() {
lua_State *L = luaL_newstate();
luaL_openlibs(L);
// 注册 C 函数到 Lua, 实际上有宏 lua_register 代替下面两行
const char *functionInLuaName = "callingInLua";
lua_pushcclosure(L, KTNativeFunction, 0); // C -> Stack
lua_setglobal(L, functionInLuaName); // Stack -> Lua
const char *script = R"(
-- 调用已注册的 C 函数
callingInLua(32, "string");
-- 定义一个 Lua 函数给 C 侧调用
function callByNativeNoParameter(num)
print("Just print a " .. num);
end
)";
luaL_loadstring(L, script);
// 执行脚本,callingInLua 会跳到 KTNativeFunction
// 实现 Lua 调用 C 函数
lua_call(L, 0, 0);
// 从 C 调用 Lua 函数
lua_getglobal(L, "callByNativeNoParameter"); // Lua -> Stack
lua_pushnumber(L, 77); // C -> Stack
lua_call(L, 1, 0); // 1 个参数, 0 个返回值
lua_close(L);
return 0;
}
我们的项目中需要传递 C++ 对象到 Lua 中进行计算并回写,结合 Lua 的一个语法糖:
obj:function()
-- 等同于
obj.function(obj)
以及 metatable 能够改变 Lua table 的默认行为的特性:
> t = {}
> mt = {__index = {foo="bar"}}
> setmetatable(t, mt)
table: 0x7f9212d046e0
> t.foo
bar
可是设计出以传递 C++ 对象到 Lua,并在 Lua 中调用成员函数的机制。Lua 侧读写成员变量:
funtion increase(obj)
local w = obj:width() + 300;
obj:width(w)
end
C++ 侧构造成员变量的读写函数,然后构造 metatable(索引字符串与 C 函数的对应关系):
class NativeObject {
public:
float width;
};
// 读写成员变量的函数
static int NativeObjectWidthAccessory(lua_State *l) {
int pCount = lua_gettop(l);
NativeObject **ptr = (NativeObject **)lua_touserdata(l, 1);
NativeObject *obj = *ptr;
int retCnt = 0;
if (pCount == 1) {
lua_pushnumber(l, obj->width);
retCnt = 1;
} else if (pCount == 2) {
lua_Number n = lua_tonumber(l, 2);
obj->width = n;
}
return retCnt;
}
// metatable 的数据
static const struct luaL_Reg NativeObjectMeta[] = {
{"width", NativeObjectWidthAccessory},
{nullptr, nullptr}
};
然后将 C++ 对象指针包裹为 userdata 放到栈上,作为参数传递给 Lua :
lua_getglobal(L, "increase");
NativeObject *obj = new NativeObject();
obj->width = 100;
NativeObject **ptr = (NativeObject **)lua_newuserdata(state, sizeof(NativeObject *));
*ptr = obj;
luaL_newlib(L, NativeObjectMeta);
lua_pushvalue(L, -1);
lua_setfield(L, -2, "__index");
lua_setmetatable(L, -2);
lua_call(L, 1, 0);
这些如果存在很多成员变量需要读写,那么就需要写很多代码。写了一些宏并包装了一些函数调用 KTLuaCChannel,方便实现简单的 Lua 与 C++ 对象通讯,使用起来大概长下面这样:
KTLuaFloatMemberAccessor(NativeObject, width);
KTLuaCreateMeta(NativeObject,
KTLuaMetaEntry(NativeObject, width),);
NativeObject *obj = new NativeObject();
obj-> width = 70;
KT::CallLuaFunction(state, "callByNative", KTLuaWrapped(NativeObject, obj));
— Jul 6, 2020