🌚

Kam's Online Notebook


包装下 Lua 与 C/C++ 宿主交互

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);
}

Stack

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);

Interaction between Lua and C

在上面的例子已经可以看到了,lua_call 可以调用函数、执行脚本。如果我们需要在 Lua 里面发起与 C 交互需要:

  1. lua_pushcclosurelua_CFunction 类型回调函数压入栈上;
  2. lua_setglobal 将回调函数和函数名设置到 Lua 中;
  3. 执行 Lua 脚本,发起 C 函数调用;
  4. 在回调函数中取出栈上的参数;
  5. 在回调函数中压入返回值到栈上。

如果 C 要调用 Lua 函数:

  1. lua_getglobal 取出 Lua 函数到栈上;
  2. 压入参数到栈上;
  3. lua_call 发起函数调用。

综合地来看一个例子,先定义一个回调函数:

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++ 成员变量

我们的项目中需要传递 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));

EOF

— Jul 6, 2020