`
August 22, 2023 本文阅读量

C和lua互操作实践

lua底层采用C语言实现,在设计时就考虑到了C和lua的互操作性,在某些场景下可以使用lua来提高c程序的灵活性,在使用lua的时候也可以用过c来提高性能。

相信了解 redis 和 openresty 的小伙伴们都知道 lua 代码可以嵌入这两种程序中运行,极大的提高了软件的扩展性;尤其是 openresty 中,通过使用 lua 我们可以很快速(相比c)的定制web服务器,或者增强 nginx 的功能。那么 lua 是如何嵌入到这些程序中的呢?lua 和 c 是如何互操作的呢?

下文的相关环境和工具版本为:Lua 5.4.6; Mac OS 13.4.1 (Darwin Kernel Version 22.5.0) arm64 M2 pro; Apple clang version 14.0.3 (clang-1403.0.22.14.1)

redis 中的 lua

下面展示了一段 redis 中操作 lua API 的代码:

这里出现了很多 lua_ 开头的函数,这些函数都是 lua 库中的函数,redis 通过这些函数来操作 lua 环境, 这里先不展开讲,后面会详细介绍。

更多的代码,如 luaRegisterRedisAPI 就不展示了,有兴趣的可以去看源码。

// redis-v7.2/src/eval.c#183

/* 初始化 lua 环境
 *
 * redis 首次启动时调用,此时 setup 为 1,
 * 这个函数也会在 redis 的其他生命周期中被调用,此时 setup 为 0,但是被简化为 scriptingReset 调用。
 */ 
void scriptingInit(int setup) {
    lua_State *lua = lua_open();

    if (setup) {
        // 首次启动时,初始化 lua 环境 和 ldb (Lua debugger) 的一些数据结构
        lctx.lua_client = NULL;
        server.script_disable_deny_script = 0;
        ldbInit();
    }

    /* 初始化 lua 脚本字典,用于存储 sha1 -> lua 脚本的映射
     * 用户使用 EVALSHA 命令时,从这个字典中查找对应的 lua 脚本。
     */
    lctx.lua_scripts = dictCreate(&shaScriptObjectDictType);
    lctx.lua_scripts_mem = 0;

    /* 注册 redis 的一些 api 到 lua 环境中 */
    luaRegisterRedisAPI(lua);

    /* 注册调试命令 */
    lua_getglobal(lua,"redis");

    /* redis.breakpoint */
    lua_pushstring(lua,"breakpoint");
    lua_pushcfunction(lua,luaRedisBreakpointCommand);
    lua_settable(lua, -3);
    /* redis.debug */
    lua_pushstring(lua,"debug");
    lua_pushcfunction(lua,luaRedisDebugCommand);
    lua_settable(lua,-3);
    /* redis.replicate_commands */
    lua_pushstring(lua, "replicate_commands");
    lua_pushcfunction(lua, luaRedisReplicateCommandsCommand);
    lua_settable(lua, -3);

    lua_setglobal(lua,"redis");

    /* 注册一个错误处理函数,用于在 lua 脚本执行出错时,打印出错信息。
     * 需要注意的是,当错误发生在 C 函数中时,我们需要打印出错的 lua 脚本的信息,
     * 这样才能帮助用户调试 lua 脚本。
     */
    {
        char *errh_func =       "local dbg = debug\n"
                                "debug = nil\n"
                                "function __redis__err__handler(err)\n"
                                "  local i = dbg.getinfo(2,'nSl')\n"
                                "  if i and i.what == 'C' then\n"
                                "    i = dbg.getinfo(3,'nSl')\n"
                                "  end\n"
                                "  if type(err) ~= 'table' then\n"
                                "    err = {err='ERR ' .. tostring(err)}"
                                "  end"
                                "  if i then\n"
                                "    err['source'] = i.source\n"
                                "    err['line'] = i.currentline\n"
                                "  end"
                                "  return err\n"
                                "end\n";
        luaL_loadbuffer(lua,errh_func,strlen(errh_func),"@err_handler_def");
        lua_pcall(lua,0,0,0);
    }

    /* 创建一个 lua client (没有网络连接),用于在 lua 环境中执行 redis 命令。
     * 这个客户端没必要在 scriptingReset() 调用时重新创建。
     */
    if (lctx.lua_client == NULL) {
        lctx.lua_client = createClient(NULL);
        lctx.lua_client->flags |= CLIENT_SCRIPT;

        /* We do not want to allow blocking commands inside Lua */
        lctx.lua_client->flags |= CLIENT_DENY_BLOCKING;
    }

    /* Lock the global table from any changes */
    lua_pushvalue(lua, LUA_GLOBALSINDEX);
    luaSetErrorMetatable(lua);
    /* Recursively lock all tables that can be reached from the global table */
    luaSetTableProtectionRecursively(lua);
    lua_pop(lua, 1);

    lctx.lua = lua;
}

通过这部分代码,应该对于 lua 的嵌入式使用有了一个大概的印象。这里可以回答以下的问题:

  1. lua 是如何嵌入到 redis 中的?

    通过包含 lua 的相关库,然后通过 API 来操作 lua 环境。主要分为以下几个步骤:

    • 初始化 lua 环境,也就是 lua_State
    • 通过 lua 的 API 来操作 lua_State,比如注册 redis 的 api 到 lua 环境中
  2. redis 中的 lua 可以使用那些 redis 的 api?

    这部分从 luaRegisterRedisAPI 函数中可以看到,redis 注册了以下的 api 到 lua 环境中:

    API 说明
    pcall 执行函数,如果出错返回false和错误 (lua 内置,但是 redis 用了自己的实现)
    redis.call 执行 redis 命令
    redis.pcall 执行 redis 命令,如果产生异常(如内存不足),返回错误信息
    redis.error_reply 返回错误回复的辅助函数 { err = text } = redis.error_reply(text)
    redis.status_reply 返回简单字符串回复的辅助函数 { ok = text } = redis.status_reply(text)
    redis.sha1hex 返回其单个字符串参数的 SHA1 十六进制摘要
    redis.log 打印日志 redis.log(redis.LOG_WARNING, ‘Something is terribly wrong’)
    redis.setresp(n) 允许执行脚本在 redis.call() 和 redis.pcall() 返回的回复的 Redis 序列化协议 (RESP) 版本之间切换,默认2
    redis.set_repl 脚本执行过程中控制指令是否 同步/复制
    redis.replicate_commands 将脚本的复制模式从 逐字复制(复制脚本) 切换为 效果复制(复制脚本生成的写入命令)。从 7.0 以后默认效果复制。
    redis.breakpoint 使用 Redis Lua 调试器时,此函数会触发断点
    redis.debug 该函数在 Redis Lua 调试器控制台中打印其参数
    redis.register_function 此函数只能在 FUNCTION LOAD 命令的上下文中使用。调用时,它会向加载的库注册一个函数。
    math.random 生成随机数 (lua 内置,但是 redis 用了自己的实现)
    math.randomseed 设置随机数种子 (lua 内置,但是 redis 用了自己的实现)

    除此之外,还有一些调试的 api,这里就不展开了。可以参考 REDIS-LUA-API 了解更多。

  3. redis 中的 lua 可以使用那些变量?

    这部分从 luaRegisterRedisAPI 函数中可以看到,redis 注册了以下的变量到 lua 环境中:

    变量名 说明
    redis redis 的命名空间
    REPL_NONE REPL 模式,不复制命令
    REPL_AOF REPL 模式,执行 lua 脚本,将命令复制到 AOF 文件中
    REPL_SLAVE REPL 模式,执行 lua 脚本,将命令复制到从节点中
    REPL_REPLICA REPL 模式,执行 lua 脚本,将命令复制到从节点中
    REPL_ALL REPL 模式,执行 lua 脚本,将命令复制到 AOF 文件中和从节点中
    REDIS_VERSION_NUM redis 版本号,比如 0x00070200 表示 7.2.0
    REDIS_VERSION redis 版本号,比如 7.2.0
    LOG_DEBUG 日志级别,debug
    LOG_VERBOSE 日志级别,verbose
    LOG_NOTICE 日志级别,notice
    LOG_WARNING 日志级别,warning

lua-json 中的 C

lua-json 是一个 lua 的 json 库,这里只是用来展示 lua 中调用 C 的例子。更多信息请查阅 lua-cjson 的使用手册 https://www.kyne.com.au/~mark/software/lua-cjson-manual.html

从它的帮助手册我们可以看到如下的代码:

-- 模块实例化
local cjson = require "cjson"
local cjson2 = cjson.new()
local cjson_safe = require "cjson.safe"

-- 在 lua 和 json 之间转换
text = cjson.encode(value)
value = cjson.decode(text)

-- 读取 lua json 中的值
setting = cjson.decode_invalid_numbers([setting])
setting = cjson.encode_invalid_numbers([setting])
keep = cjson.encode_keep_buffer([keep])
depth = cjson.encode_max_depth([depth])
depth = cjson.decode_max_depth([depth])
convert, ratio, safe = cjson.encode_sparse_array([convert[, ratio[, safe]]])

同时在 make 安装 一节中, 指明了如果需要手动安装的话,需要执行以下的命令:

make
cp cjson.so $LUA_MODULE_DIRECTORY

很明显,这里的 cjson.so 是一个共享库,也就是说在 lua 中如果想要调用 c 的扩展,那么共享库是一种方式。

这里需要注意的是,lua 中通过 require 来引入的时候,需要保证 cjson.so 在 lua 的搜索路径中(package.cpath)。

为什么说只是一种方式呢?因为 lua 还提供了一种方式,那就是通过 lua 的 API 让 c 代码编写的函数注册到 lua 环境中,然后在 c 中执行 lua 脚本,如下:

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

static int sumForLua(lua_State *lua)
{
    // 如果给定虚拟栈中索引处的元素可以转换为数字,则返回转换后的数字,否则报错。
    double a = luaL_checknumber(lua, 1);
    double b = luaL_checknumber(lua, 2);

    // push 计算结果到 lua 虚拟栈中
    lua_pushnumber(lua, a+b); 

    /* 返回值用于提示该C函数的返回值数量。
     * 
     * 这里可以看出,C可以返回给Lua多个结果,
     * 通过多次调用lua_push*(),之后return返回结果的数量。
     */
    return 1;  
}

int main(void)
{
    lua_State *lua = lua_open(); // 创建 Lua 环境
    luaL_openlibs(lua);          // 打开Lua状态机"L"中的所有Lua标准库。

    // 注册 C函数到 lua 中 csum 函数
    lua_register(lua, "csum", sumForLua);  

    // 执行 Lua 脚本
    const char* script = "print(csum(3.14, 2.0))";
    if(luaL_dostring(lua, script))    
        printf("Failed to invoke.\n");
    lua_close(lua); 

    return 0;
}

我们还没有搞清楚 c 是如何给 lua 扩展的,再回到 lua-cjson 的源码中去:

// https://github.com/mpx/lua-cjson/blob/master/lua_cjson.c#1416
CJSON_EXPORT int luaopen_cjson(lua_State *l)
{   
    // local cjson = require "cjson"
    lua_cjson_new(l);

// 如果编译时定义了 ENABLE_CJSON_GLOBAL,注册 cjson (table) 到全局变量中
#ifdef ENABLE_CJSON_GLOBAL
    lua_pushvalue(l, -1);
    // #define CJSON_MODNAME "cjson"
    lua_setglobal(l, CJSON_MODNAME);
#endif

    return 1;
}

// cjson 模块(table)的初始化
static int lua_cjson_new(lua_State *l)
{
    // 函数注册表, 这些函数的函数原型都满足:
    // int (*lua_CFunction) (lua_State *L); return value: number of results
    // 
    // 注册表结构体:
    // typedef struct luaL_Reg {
    //   const char *name;
    //   lua_CFunction func;
    // } luaL_Reg;
    // 
    luaL_Reg reg[] = {
        { "encode", json_encode },
        { "decode", json_decode },
        { "encode_sparse_array", json_cfg_encode_sparse_array },
        { "encode_max_depth", json_cfg_encode_max_depth },
        { "decode_max_depth", json_cfg_decode_max_depth },
        { "encode_number_precision", json_cfg_encode_number_precision },
        { "encode_keep_buffer", json_cfg_encode_keep_buffer },
        { "encode_invalid_numbers", json_cfg_encode_invalid_numbers },
        { "decode_invalid_numbers", json_cfg_decode_invalid_numbers },
        { "new", lua_cjson_new },
        { NULL, NULL } /* 以 NULL, NULL 结束 */
    };
    // ...

    // 创建一个新的 table, 并且将其压入栈中
    lua_newtable(l);
    // ...

    json_create_config(l);
    // 将 reg 中的函数注册到 table 中
    // void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup);
    // nup = 1 (不等于0) 表示将 nup 位置的值副本来初始化所有的函数,这些值会弹出栈。
    // 因此这里是给 reg 中的所有函数都隐式的传入了一个 config 参数, 函数内部可以通过
    // int lua_upvalueindex(int i); lua_to* 函数来获取这个参数。
    luaL_setfuncs(l, reg, 1);

    // 设置 table.null = nil, userdata 代表 Lua中的 C 值
    lua_pushlightuserdata(l, NULL);
    // void lua_setfield (lua_State *L, int index, const char *k);
    // table = stack[index]; table[k] = v (stack[-1]), 设置后 v会从栈中弹出
    lua_setfield(l, -2, "null");

    // 下面的类似,都是在设置 table 中的字段, 如: _NAME, _VERSION
    lua_pushliteral(l, CJSON_MODNAME);
    lua_setfield(l, -2, "_NAME");
    lua_pushliteral(l, CJSON_VERSION);
    lua_setfield(l, -2, "_VERSION");

    // 此时栈中还剩下 1 个 table
    return 1;
}

我们来引入 lua-cjson 模块来看看:

这里因为我使用的是 lua 5.4, 而 lua-cjson 貌似不兼容?

这样我们也大概弄清楚了,如何为 lua 扩展 c 代码了:

  • 编写一个 luaopen_xxx 函数,该函数会在 lua 中通过 require “xxx” 时被调用;
  • 在 luaopen_xxx 去注册 c 模块或者函数到 lua 环境中;
  • 将 c 扩展编译为 so 文件,然后将 so 文件放到 lua 的搜索路径中;

lua 和 C 互操作的基础

通过上述的例子,对于 lua 和 C 互操作有了一个大概的印象,那么我们看到的代码中那些 lua_luaL_ 开头的函数是什么呢?各种 lua_push* 和 lua_pop* 之类的函数又是做什么的呢?

PIL 的 24.2 小节中解释在 Lua 和 C API 交互的过程中,会使用一抽象栈来传递数据,这个虚拟栈是一个 LIFO 栈,它由 Lua 来管理(方便 lua 的垃圾收集器工作)。该栈中的每个槽都可以保存任何 Lua 值。每当你想从 Lua 请求一个值(例如全局变量的值)时,你就调用 Lua,它将所需的值压入堆栈。每当你想向 Lua 传递一个值时,你首先将值压入堆栈,然后调用 Lua(这将弹出该值)。

需要注意的是,虚拟栈并不是无限的,它的大小可以通过 lua.h 中的 LUA_MINSTACK 定义的。通常来说虚拟栈的大小是 20,一般情况足够使用了。如果不确实栈空间是否足够,可以通过使用 int lua_checkstack (lua_State *L, int sz); 来检查。

有了这个基础我们就很容易理解,上面 redis 中的是如何去初始化 lua 环境的。

重新解读 redis 中的 lua 初始化

为了更容易的解读,这里把一些 lua_ 函数做了简单的说明:

函数名 说明
lua_State *lua_newstate (lua_Alloc f, void *ud); 创建一个新的 lua 环境, lua_open 的替代
int lua_getglobal (lua_State *L, const char *name); 把全局变量的值压入堆栈, 等价声明了一个 name 的全局变量
void lua_setglobal (lua_State *L, const char *name); 把堆栈顶部的值弹出并设置为 name 全局变量
const char *lua_pushstring (lua_State *L, const char *s); 把一个字符串压入堆栈,lua 会创建 s 的副本或者复用内部的副本
void lua_pushcfunction (lua_State *L, lua_CFunction f); 把一个 C 函数压入堆栈
void lua_settable (lua_State *L, int index); 给 table 的设定 kv, 等价于 table[k] = v, table = stack[index], v=stack[-1], k=stack[-2], 会从栈中弹出 k/v
// redis-v7.2/src/eval.c#183
void scriptingInit(int setup) {
    // ... some codes ignored

    // 声明全局变量 redis, 同时压入栈中
    lua_getglobal(lua, "redis"); 
    // 往虚拟栈中压入 字符串 "breakpoint"
    lua_pushstring(lua, "breakpoint");
    // 往虚拟栈中压入 C 函数 luaRedisBreakpointCommand
    lua_pushcfunction(lua, luaRedisBreakpointCommand);
    // 设置 redis["breakpoint"] = luaRedisBreakpointCommand
    lua_settable(lua, -3);

    // 将 redis 弹出栈
    lua_setglobal(lua, "redis");

    // ... some codes ignored
}

上面的片段等价于以下的 lua 代码:

redis = {}
redis["breakpoint"] = luaRedisBreakpointCommand

这期间的栈空间变化可以用下面的图来表示:

+----bottom----+
|      {}      | [1, -3]  lua_getglobal(lua, "redis");
+--------------+
| "breakpoint" | [2, -2]  lua_pushstring(lua,"breakpoint");
+--------------+
|   function   | [3, -1]  lua_pushcfunction(lua,luaRedisBreakpointCommand);
+------top-----+


+---------------------bottom--------------------------+
|      {"breakpoint": luaRedisBreakpointCommand}      | [1, -1]  lua_settable(lua, -3);
+-----------------------top---------------------------+

异常处理

虽然 C 语言没有提供异常处理机制,但是在 lua 中提供了 errorpcall 函数来抛出异常,处理异常。因此在 lua 和 C 交互的过程中,特意提供了一些函数来处理异常。如 lua_pcall,在调用期间如果没有异常,那么 lua_pcall 和 lua_call 行为一致,反之如果发生了异常,那么 lua_pcall 会捕获异常,并把错误对象(单个值)压入堆栈,并返回错误码。同时从堆栈中删除函数和参数。

// 普通调用
void lua_call (lua_State *L, int nargs, int nresults);
// 异常捕获调用
// msgh 用于是否使用自定义的错误处理函数,如果为 0,那么使用默认的错误处理函数
// 否则,msgh 用于说明:异常消息处理函数在堆栈中的位置
int lua_pcall (lua_State *L, int nargs, int nresults, int msgh);

应该还记得 redis 中的 lua 初始化代码中有这么一段:

// redis-v7.2/src/eval.c#183
void scriptingInit(int setup) {
    // ...

    /* 注册一个错误处理函数,用于在 lua 脚本执行出错时,打印出错信息。
     * 需要注意的是,当错误发生在 C 函数中时,我们需要打印出错的 lua 脚本的信息,
     * 这样才能帮助用户调试 lua 脚本。
     */
    {
        char *errh_func =       "local dbg = debug\n"
                                "debug = nil\n"
                                "function __redis__err__handler(err)\n"
                                "  local i = dbg.getinfo(2,'nSl')\n"
                                "  if i and i.what == 'C' then\n"
                                "    i = dbg.getinfo(3,'nSl')\n"
                                "  end\n"
                                "  if type(err) ~= 'table' then\n"
                                "    err = {err='ERR ' .. tostring(err)}"
                                "  end"
                                "  if i then\n"
                                "    err['source'] = i.source\n"
                                "    err['line'] = i.currentline\n"
                                "  end"
                                "  return err\n"
                                "end\n";
        // int luaL_loadbuffer (lua_State *L, const char *buff, size_t sz, const char *name);
        // 加载 lua 块,但是不执行,会将编译后的 lua 块压入栈中,如果出错会将错误信息压入栈中
        luaL_loadbuffer(lua, errh_func, strlen(errh_func), "@err_handler_def");
        // int lua_pcall (lua_State *L, int nargs, int nresults, int msgh);
        // 执行 lua 块,如果出错,pcall 会将错误信息压入栈中
        lua_pcall(lua, 0, 0, 0);
    }
    // ...
}

这里这么只是完成了定义错误处理函数,但是并没有使用,因为需要在 lua_pcall(lua_State *L, int nargs, int nresults, int msgh) 通过 msgh 来指定才能使用。那么再看看 redis 中的关于 __redis__err__handler 的使用:

// redis-v7.2/src/eval.c#472
void evalGenericCommand(client *c, int evalsha) {
    // ...

    // 将 __redis__err__handler 压入栈中,已经在初始化过程中通过 lua 块定义成了全局变量
    lua_getglobal(lua, "__redis__err__handler");
    // ... 

    // void luaCallFunction(scriptRunCtx* run_ctx, lua_State *lua, robj** keys, size_t nkeys, robj** args, size_t nargs, int debug_enabled)
    luaCallFunction(&rctx, lua, c->argv+3, numkeys, c->argv+3+numkeys, c->argc-3-numkeys, ldb.active);

    // 从栈中移除 __redis__err__handler 
    lua_pop(lua,1);
    // ...
}

// redis-v7.2/src/script_lua.c#L1615
void luaCallFunction(scriptRunCtx* run_ctx, lua_State *lua, robj** keys, size_t nkeys, robj** args, size_t nargs, int debug_enabled) {
    // ...

    // 创建一个数组,用于存储 keys
    luaCreateArray(lua,keys,nkeys);
    if (run_ctx->flags & SCRIPT_EVAL_MODE){
        // EVAL模式:KEYS 从栈中弹出成为全局变量 KEYS
        lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 0);
        lua_setglobal(lua,"KEYS");
        lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 1);
    }
    // 下同,创建一个数组,用于存储 args
    luaCreateArray(lua,args,nargs);
    if (run_ctx->flags & SCRIPT_EVAL_MODE){
        lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 0);
        lua_setglobal(lua,"ARGV");
        lua_enablereadonlytable(lua, LUA_GLOBALSINDEX, 1);
    }

    /* 执行 lua 脚本
     * 如果是 EVAL 模式, 那么 lua 代码应该没有参数,然后接受一个返回值,此时 error_handler 在 -2 的位置。
     * 如果是 FUNCTION 模式,那么 lua 代码应该有两个参数(keys 和 args 数组),然后接受一个返回值,
     * 此时 error_handler 在 -4 (stack: error_handler, callback, keys, args) 的位置。
     */
    int err;
    if (run_ctx->flags & SCRIPT_EVAL_MODE) {
        err = lua_pcall(lua,0,1,-2);
    } else {
        err = lua_pcall(lua,2,1,-4);
    }

    // ...
}

动手实践

使用 c 编写一个生成 snowflake ID 的库, 同时提供 lua 扩展,主要代码如下:

完整代码可以在 yeqown/snowflake/contrib/lua 查看

require "snowflake"

print("   NAME: " .. snowflake._NAME)
print("VERSION: " .. snowflake._VERSION)
print(" AUTHOR: " .. snowflake._AUTHOR)

local worker = snowflake.new(1)

local id = worker.next_id()
print("generating one id: " .. id .. "\n")

print("parsing snowflake id: " .. id)
local state = snowflake.parse(id);
print("timestamp: " .. state.timestamp)
print("worker_id: " .. state.worker_id)
print("    count: " .. state.count .. "\n")

print("generating 10 ids in a batch: ")
local ids = worker.next_ids(10)
for i, id in ipairs(ids) do
    print(id)
end

运行效果如下:

/*
 * Usage in lua:
 * worker = snowflake.new(1?)
 */
int l_snowflake_new(lua_State *L) {
  uint64_t worker_id = 0; 
  // ...
  lua_newtable(L);
  snowflake_Worker *worker = snowflake_NewWorker(worker_id);
  // ...
  lua_pushlightuserdata(L, worker);

  luaL_Reg snowflake_worker_funcs[] = {
      {"next_id", l_snowflake_worker_next_id},
      {"next_ids", l_snowflake_worker_next_ids},
      {NULL, NULL},
  };

  luaL_setfuncs(L, snowflake_worker_funcs, 1);

  return 1;
}

/*
 * Usage in Lua:
 * snowflake.parse()
 */
int l_snowflake_parse(lua_State *L) {
  uint64_t id = (uint64_t) lua_tointegerx(L, -1, NULL);
  // ...
  lua_pop(L, -1);

  const snowflake_IDState *state = snowflake_ParseId((snowflake_ID) id);

  lua_newtable(L);
  lua_pushinteger(L, (long long) state->timestamp);
  lua_setfield(L, -1, "timestamp");

  lua_pushinteger(L, (long long) state->worker_id);
  lua_setfield(L, -1, "worker_id");

  lua_pushinteger(L, (long long) state->count);
  lua_setfield(L, -1, "count");

  return 1;
}

/*
 * Usage in lua:
 * local worker = snowflake.new(1)
 * local id = worker.next_id()
 */
int l_snowflake_worker_next_id(lua_State *L) {
  snowflake_Worker *worker = (snowflake_Worker *) lua_touserdata(L, lua_upvalueindex(1));
  // ...
  snowflake_ID id = snowflake_NextId(worker, true);
  // ...

  lua_pushinteger(L, (long long) id);

  return 1;
}

/*
 * Usage in Lua:
 * local worker = snowflake.new(1)
 * local ids = worker.next_ids(10)
 */
int l_snowflake_worker_next_ids(lua_State *L) {
  // ...
  uint64_t count = (uint64_t) lua_tointegerx(L, -1, NULL);
  // ...
  lua_pop(L, -1);

  snowflake_Worker *worker = (snowflake_Worker *) lua_touserdata(L, lua_upvalueindex(1));
  // ...

  snowflake_ID *ids = malloc(sizeof(snowflake_ID) * count);
  if (snowflake_NextIds(worker, count, ids, false) <= 0) {
    lua_pushstring(L, "failed to generate snowflake ids");
    lua_error(L);
  }

  lua_newtable(L);
  for (uint64_t i = 0; i < count; i++) {
    lua_pushinteger(L, (long long) ids[i]);
    lua_rawseti(L, -2, i + 1);
  }

  free(ids);

  return 1;
}

int luaopen_snowflake(lua_State *L) {
  luaL_Reg snowflake_lib[] = {
      {"new", l_snowflake_new},
      {"parse", l_snowflake_parse},
      {NULL, NULL}
  };

  luaL_newlib(L, snowflake_lib);

  lua_pushliteral(L, "_AUTHOR");
  lua_setfield(L, -2, "yeqown");

  lua_pushliteral(L, "lua-snowflake");
  lua_setfield(L, -2, "_NAME");

  lua_pushliteral(L, "0.1.0");
  lua_setfield(L, -2, "_VERSION");

  // set snowflake as global variable
  lua_pushvalue(L, -1);
  lua_setglobal(L, "snowflake");

  return 1;
}

这里就不再展开讲解了,里面的每一部分都是前面讲解的内容,这里只是做了一个简单的整合。当然实际扩展的时候,不会这么简单,可能需要处理复杂的数据结构,繁琐的流程,但这不在本文的讨论范围内,请善用工具和官方文档。

总结

lua 本身就是由 C 语言编写的,因此 lua 和 C 之间的交互是非常方便的。而他们交互的核心方式就是通过 lua 的虚拟栈来传递数据,然后通过 lua 的 API 来操作虚拟栈。

参考