Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

总览与准备

本指南由 Kimi K2.6 编写生成。

仓库地址:https://github.com/qaqland/lua-c-api-guide

什么是 Lua C API

Lua 被设计为一个可嵌入的脚本语言。它提供了一组 C 语言接口,允许宿主程序(Host Program):

  • 创建和销毁 Lua 状态机(lua_State
  • 在 C 和 Lua 之间传递数据
  • 从 C 调用 Lua 函数
  • 将 C 函数暴露给 Lua 脚本
  • 操作 Lua 中的 table、userdata、metatable 等高级类型

关键头文件

只需关注两个头文件:

头文件作用
lua.h核心 API。所有以 lua_ 开头的函数
lauxlib.h辅助库(auxiliary library)。以 luaL_ 开头的辅助函数,让代码更简洁安全

通常 C 文件顶部只需要:

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>   // 如需打开标准库 

编译环境

需要预先安装 Lua 5.4 开发包(如 lua5.4-dev)。下面介绍两种编译方式:手动用 gcc,以及本指南使用的 Meson

手动编译

通过 pkg-config 获取头文件和库路径,手动调用 gcc

gcc 00-overview.c $(pkg-config --cflags --libs lua5.4) -o 00-overview
./00-overview

Meson(本指南使用)

Meson 自动通过 pkg-config 获取 Lua 5.4 的编译参数,无需手动指定 -I-L-l

meson setup build
meson compile -C build
./build/src/01_stack

最小可运行程序

#include <lua.h>
#include <stdio.h>

int main(void) {
    printf("Lua version: %.0f\n", lua_version(NULL));
    return 0;
}

这个程序只打印 Lua 版本号,验证开发环境是否正确链接了 Lua 库。后续章节会逐步介绍状态机的创建、使用和销毁。

参考资源

源码头文件(Lua 5.4 官方仓库):

致谢

感谢 st0nie 提供支持。

虚拟栈(The Stack)

Lua C API 的核心是虚拟栈。Lua 与 C 之间不直接共享变量,所有数据交换都通过栈完成。

状态机的创建与销毁

操作栈之前,需要先创建一个 Lua 状态机(lua_State):

lua_State *L = luaL_newstate();   // 创建状态机
// ... 使用栈 ...
lua_close(L);                      // 销毁状态机,释放资源

luaL_newstate 使用默认内存分配器创建一个全新的 Lua 状态机。后续第 12 章会介绍如何自定义分配器。

栈的基本特性

  • Lua 管理栈的内存,栈会自动增长
  • C 代码负责保持栈平衡:压入多少就要相应弹出多少
  • 栈索引从 1 开始表示从底向上,从 -1 开始表示从顶向下
        index         value
  top    [5] [-1]    "hello"
  |      [4] [-2]    3.14
  |      [3] [-3]    42
  |      [2] [-4]    true
  bottom [1] [-5]    nil

常用栈操作

操作函数示例说明
压栈lua_pushnil, lua_pushboolean, …将值压入栈顶
查询lua_type, lua_isnumber, …获取栈中值的类型或内容
控制lua_pop, lua_remove, …调整栈的内容
大小lua_gettop, lua_settop获取/设置栈高度

索引规则

  • 绝对索引1lua_gettop(L),从栈底开始
  • 相对索引-1-lua_gettop(L),从栈顶开始
  • 伪索引:如 LUA_REGISTRYINDEX,用于访问注册表,不在栈上

惯例:Lua C API 中绝大多数接受栈索引的函数同时支持正索引和负索引,-1 始终代表栈顶。

栈平衡

C 函数操作的是共享栈,返回前必须确保栈回到预期状态。推荐在进入 C 函数时记录栈底,返回前恢复:

static int myfunc(lua_State *L) {
    int base = lua_gettop(L);
    // ... 各种中间栈操作 ...
    lua_settop(L, base);       // 清理中间值
    lua_pushinteger(L, result);
    return 1;
}

栈检查

在 C 函数被 Lua 调用时,栈空间可能有限。如果你需要压入大量值,应先检查:

lua_checkstack(L, 20);  // 成功返回 1,失败返回 0 

完整代码


#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <stdio.h>
#include <string.h>

int main(void) {
    lua_State *L = luaL_newstate();

    lua_pushnil(L);
    lua_pushboolean(L, 1);
    lua_pushinteger(L, 42);
    lua_pushnumber(L, 3.14);
    lua_pushstring(L, "hello");
    assert(lua_gettop(L) == 5);
    assert(lua_type(L, -1) == LUA_TSTRING);

    lua_pop(L, 2);
    assert(lua_gettop(L) == 3);

    assert(lua_isinteger(L, -1) == 1);
    assert(strcmp(lua_tostring(L, -1), "42") == 0);

    lua_pushvalue(L, 1);
    lua_remove(L, 1);
    lua_insert(L, 1);

    lua_settop(L, 5);
    assert(lua_gettop(L) == 5);
    lua_settop(L, 2);
    assert(lua_gettop(L) == 2);

    lua_close(L);
    puts("01-stack: ok");
    return 0;
}

数据类型转换

Lua 是动态类型语言,C 是静态类型语言。交互时需要在两者之间转换。

Lua ↔ C 类型对照

Lua 类型C API 检查C API 读取C API 写入
nillua_isnillua_pushnil
booleanlua_isbooleanlua_tobooleanlua_pushboolean
integerlua_isintegerlua_tointegerlua_pushinteger
number (float)lua_isnumberlua_tonumberlua_pushnumber
stringlua_isstring*lua_tostringlua_pushstring
tablelua_istablelua_newtable
functionlua_isfunctionlua_pushcfunction
userdatalua_isuserdatalua_touserdatalua_newuserdatauv
threadlua_isthreadlua_tothread

*注意:lua_isstring 对 number 也返回 true,因为 number 可以自动转换为 string。

类型检查与转换

安全读取(带默认值)

lua_Integer n = luaL_optinteger(L, 1, 10);   // 第1参数不是整数则返回10 
lua_Number  x = luaL_optnumber(L, 2, 0.0);
const char *s = luaL_optstring(L, 3, "default");

严格检查(C 函数参数校验)

lua_Integer n = luaL_checkinteger(L, 1);     // 类型不对则抛出 Lua 错误 
lua_Number  x = luaL_checknumber(L, 2);
const char *s = luaL_checkstring(L, 3);

字符串处理

lua_tostring 返回的指针指向 Lua 内部字符串,只要该字符串还在栈上或未被 GC,指针就有效。不要 free 它。

对于包含 \0 的二进制字符串,使用 lua_pushlstring / lua_tolstring

size_t len;
const char *data = lua_tolstring(L, -1, &len);  // 获取长度和内容 
lua_pushlstring(L, data, len);                  // 压入二进制串 

boolean 陷阱

lua_toboolean 对以下值返回 0(false):

  • nil
  • false
  • 其余所有值(包括 0 和空字符串)都返回 1(true)

这与 C 的“0 为假”逻辑不同,需特别注意。

完整代码


#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <stdio.h>
#include <string.h>

int main(void) {
    lua_State *L = luaL_newstate();

    lua_pushnil(L);
    lua_pushboolean(L, 1);
    lua_pushinteger(L, 42);
    lua_pushnumber(L, 3.14);
    lua_pushstring(L, "hello");
    lua_pushlstring(L, "hi\x00world", 8);

    assert(lua_type(L, -1) == LUA_TSTRING);
    assert(lua_isinteger(L, -2) == 0);

    size_t len;
    const char *s = lua_tolstring(L, -1, &len);
    assert(strcmp(s, "hi") == 0);
    assert(len == 8);

    assert(lua_tointeger(L, 3) == 42);
    assert(lua_tonumber(L, 4) == 3.14);

    lua_settop(L, 0);
    lua_pushinteger(L, 100);
    assert(luaL_checkinteger(L, 1) == 100);
    assert(luaL_optinteger(L, 2, 99) == 99);

    lua_close(L);
    puts("02-types: ok");
    return 0;
}

从 C 调用 Lua

C 代码可以加载并执行 Lua 代码,也可以直接调用 Lua 函数。

执行 Lua 代码

最简单的方式是使用辅助库的 luaL_dostringluaL_dofile(第 14 章会讲更底层的加载机制):

// 执行字符串
if (luaL_dostring(L, "return 1 + 1") != LUA_OK) {
    // 出错,错误信息在栈顶
    fprintf(stderr, "%s\n", lua_tostring(L, -1));
    lua_pop(L, 1);
}

它们实际上是两步的封装:先加载,再执行。

lua_pcall 用于在保护模式下调用栈上的函数,这是 C 与 Lua 交互的核心机制。第 6 章会深入讲解错误处理。

调用 Lua 函数

假设 Lua 全局环境中有一个函数 add

-- config.lua
function add(a, b)
    return a + b
end

在 C 中调用它:

// 1. 将函数压栈
// lua_getglobal 从全局表读取变量并压入栈(第 5 章会讲 table 操作)
lua_getglobal(L, "add");

// 2. 压入参数
lua_pushinteger(L, 10);
lua_pushinteger(L, 20);

// 3. 调用: 2 个参数, 1 个返回值
if (lua_pcall(L, 2, 1, 0) == LUA_OK) {
    lua_Integer result = lua_tointeger(L, -1);
    lua_pop(L, 1);       // 弹出返回值 
} else {
    fprintf(stderr, "error: %s\n", lua_tostring(L, -1));
    lua_pop(L, 1);
}

pcall 参数详解

int lua_pcall(lua_State *L, int nargs, int nresults, int msgh);
参数含义
nargs参数个数(必须在栈顶紧挨着函数)
nresults期望的返回值个数,或 LUA_MULTRET(全部)
msgh错误处理函数的栈索引,0 表示无

调用前栈布局:[ ... | function | arg1 | arg2 | ... | argN ](栈顶是最后一个参数)

调用成功后,函数和参数被弹出,返回值留在栈上。

读取 Lua 返回值

如果 Lua 函数返回多个值:

lua_getglobal(L, "multi_return");
lua_pcall(L, 0, LUA_MULTRET, 0);
int nrets = lua_gettop(L);   // 计算返回了多少个值

完整代码


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

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    luaL_dostring(L, "return 1 + 1");
    assert(lua_tointeger(L, -1) == 2);
    lua_pop(L, 1);

    luaL_dostring(L, "function add(a, b) return a + b end");

    lua_getglobal(L, "add");
    lua_pushinteger(L, 10);
    lua_pushinteger(L, 20);
    lua_pcall(L, 2, 1, 0);
    assert(lua_tointeger(L, -1) == 30);
    lua_pop(L, 1);

    luaL_dostring(L, "function pair() return 1, 2 end");

    lua_getglobal(L, "pair");
    lua_pcall(L, 0, LUA_MULTRET, 0);
    int nrets = lua_gettop(L);
    assert(nrets == 2);
    assert(lua_tointeger(L, 1) == 1);
    assert(lua_tointeger(L, 2) == 2);
    lua_pop(L, nrets);

    lua_close(L);
    puts("03-call-lua: ok");
    return 0;
}

从 Lua 调用 C

将 C 函数暴露给 Lua 是扩展 Lua 的主要方式。这允许你用 C/C++ 编写高性能模块,供 Lua 脚本调用。

C 函数原型

所有可被 Lua 调用的 C 函数必须符合以下签名:

int my_c_function(lua_State *L);
  • 参数通过栈传递,lua_gettop(L) 可获取参数个数
  • 返回值通过压栈实现,return n 表示返回 n 个值

最小示例

static int c_add(lua_State *L) {
    lua_Integer a = luaL_checkinteger(L, 1);
    lua_Integer b = luaL_checkinteger(L, 2);
    lua_pushinteger(L, a + b);
    return 1;       // 1 个返回值 
}

注册到 Lua

方式一:注册为全局函数

lua_pushcfunction(L, c_add);
lua_setglobal(L, "c_add");   // Lua 中可直接调用 c_add(1,2) 

方式二:组织为模块(推荐)

使用 luaL_Reg 数组批量注册:

static const struct luaL_Reg mylib[] = {
    {"add", c_add},
    {"sub", c_sub},
    {NULL, NULL}        // 哨兵,必须以此结尾
};

// luaL_newlib 内部会创建 table 并把函数填入(第 5 章会讲 table)
int luaopen_mylib(lua_State *L) {
    luaL_newlib(L, mylib);
    return 1;                    // 返回 table
}

在 Lua 中:

local mylib = require("mylib")
print(mylib.add(10, 20))

对于 require 能正确找到 luaopen_mylib,通常需要将 C 代码编译为共享库(.so / .dll),并放入 Lua 的 package.cpath 搜索路径。第 19 章会讲标准库加载和模块搜索机制。

完整代码


#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int c_add(lua_State *L) {
    lua_Integer a = luaL_checkinteger(L, 1);
    lua_Integer b = luaL_checkinteger(L, 2);
    lua_pushinteger(L, a + b);
    return 1;
}

static int c_reverse(lua_State *L) {
    size_t len;
    const char *s = luaL_checklstring(L, 1, &len);

    char *buf = malloc(len);
    for (size_t i = 0; i < len; i++)
        buf[i] = s[len - 1 - i];

    lua_pushlstring(L, buf, len);
    free(buf);
    return 1;
}

static int c_sum(lua_State *L) {
    int n = lua_gettop(L);
    lua_Number sum = 0;
    for (int i = 1; i <= n; i++) {
        sum += luaL_checknumber(L, i);
    }
    lua_pushnumber(L, sum);
    return 1;
}

static const struct luaL_Reg mylib[] = {
    {"add", c_add}, {"reverse", c_reverse}, {"sum", c_sum}, {NULL, NULL}};

int luaopen_mylib(lua_State *L) {
    luaL_newlib(L, mylib);
    return 1;
}

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    lua_pushcfunction(L, c_add);
    lua_setglobal(L, "g_add");

    luaopen_mylib(L);
    lua_setglobal(L, "mylib");

    const char *script =
        "print('mylib.add(3,4) =', mylib.add(3, 4))\n"
        "print('mylib.reverse(\"abc\") =', mylib.reverse('abc'))\n"
        "print('mylib.sum(1,2,3,4) =', mylib.sum(1, 2, 3, 4))\n"
        "print('g_add(10,20) =', g_add(10, 20))\n";

    luaL_dostring(L, script);

    lua_close(L);
    puts("04-call-c: ok");
    return 0;
}

操作 Table

Table 是 Lua 唯一的数据结构,掌握 C API 中的 table 操作至关重要。

创建 Table

lua_newtable(L);           // 等价于 lua_createtable(L, 0, 0) 
lua_createtable(L, narr, nrec);  // 预分配数组部分 narr、哈希部分 nrec 

读写字段

假设栈顶是一个 table:

// 读:t[key] 
lua_pushstring(L, "name");     // 压入 key 
lua_gettable(L, -2);           // 查询,弹出 key,结果压栈 
// 此时栈顶是 t["name"] 的值 

// 写:t[key] = value 
lua_pushstring(L, "name");
lua_pushstring(L, "Alice");
lua_settable(L, -3);           // 弹出 key 和 value,设置到 table 

lua_gettablelua_settable 会触发 __index / __newindex 元方法。

便捷 API(不触发元方法)

操作函数说明
获取整数键值lua_geti(L, idx, n)t[n],结果压栈
设置整数键值lua_seti(L, idx, n)t[n] = value
获取字符串键值lua_getfield(L, idx, k)t.k,结果压栈
设置字符串键值lua_setfield(L, idx, k, v)t.k = v
原始获取lua_rawget / lua_rawgeti / lua_rawgetp绕过元方法
原始设置lua_rawset / lua_rawseti / lua_rawsetp绕过元方法

遍历 Table

lua_pushnil(L);   // 第一个 key 
while (lua_next(L, table_index) != 0) {
        // 此时栈顶:value (-1), key (-2) 
        // 处理 key-value... 

    lua_pop(L, 1);       // 弹出 value,保留 key 供下一次迭代 
}

lua_next 会触发 __pairs metamethod(如果存在)。如果需要原始遍历,用 lua_rawlen +

整数下标遍历数组部分,或使用 luaH_next 的内部逻辑(不推荐)。

数组长度

lua_Integer len = luaL_len(L, -1);   // 调用 #t,触发 __len 
size_t rawlen = lua_rawlen(L, -1);   // 不触发元方法 

完整代码


#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    lua_newtable(L);

    lua_pushstring(L, "Alice");
    lua_setfield(L, -2, "name");
    lua_pushinteger(L, 30);
    lua_setfield(L, -2, "age");

    lua_pushstring(L, "lua");
    lua_seti(L, -2, 1);
    lua_pushstring(L, "c");
    lua_seti(L, -2, 2);

    lua_getfield(L, -1, "name");
    assert(strcmp(lua_tostring(L, -1), "Alice") == 0);
    lua_pop(L, 1);

    lua_geti(L, -1, 1);
    assert(strcmp(lua_tostring(L, -1), "lua") == 0);
    lua_pop(L, 1);

    assert(luaL_len(L, -1) == 2);

    lua_rawgeti(L, -1, 1);
    assert(strcmp(lua_tostring(L, -1), "lua") == 0);
    lua_pop(L, 1);

    lua_pop(L, 1);

    lua_close(L);
    puts("05-tables: ok");
    return 0;
}

错误处理

Lua 使用异常机制longjmp)处理错误。在 C 代码中调用 Lua 时,必须注意错误传播,否则会导致程序崩溃。

保护模式调用

lua_pcall

始终用 lua_pcall 代替 lua_call(除非你在一个被 Lua 调用的 C 函数内部,且确定不会出错)。

if (lua_pcall(L, nargs, nresults, msgh) != LUA_OK) {
        // 出错 
    const char *errmsg = lua_tostring(L, -1);
    fprintf(stderr, "Lua error: %s\n", errmsg);
    lua_pop(L, 1);       // 弹出错误信息 
}

错误处理函数(msgh)

pcall 的第 4 个参数可以指定一个错误处理函数。当发生错误时,Lua 会先调用这个函数,将原始错误信息传给它,然后将处理函数的返回值作为最终错误信息。

常见用途是打印调用栈:

lua_getglobal(L, "debug");
lua_getfield(L, -1, "traceback");
lua_remove(L, -2);   // 移除 debug table,保留 traceback 函数 

// 此时 traceback 函数在栈顶,其索引就是 -1 
if (lua_pcall(L, nargs, nresults, -2) != LUA_OK) {  // 注意索引计算 
    ...
}

从 C 报告错误

抛出 Lua 错误

在注册给 Lua 的 C 函数中,如果发现参数错误,应该使用 luaL_errorluaL_argerror

static int myfunc(lua_State *L) {
    if (!lua_isinteger(L, 1)) {
        return luaL_argerror(L, 1, "expected integer");           // 不会返回 
    }
        // ... 
}

格式化错误信息

return luaL_error(L, "bad value at index %d: %s", idx, reason);

luaL_error 使用 lua_error 抛出异常,不会返回到调用者

C 函数中的保护模式

如果你想在 C 函数内部保护一段代码,使用 lua_pcall 即可。但如果需要在 C 层面做更细粒度的保护,可以使用:

int lua_cpcall(lua_State *L, lua_CFunction func, void *ud);

不过 Lua 5.2+ 更推荐的方式是在 C 函数内直接组织好栈,然后使用 lua_pcall

常见错误场景

场景结果正确做法
lua_call 在保护模式外调用出错代码程序 longjmp 到未知位置,崩溃使用 lua_pcall
lua_tostring 对非 string/number 使用返回 NULL先用 lua_isstring 检查
栈索引越界未定义行为lua_gettop 确认栈大小
lua_tolstring 后修改字符串未定义行为复制到 C 缓冲区后再修改

完整代码


#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>

static int c_divide(lua_State *L) {
    lua_Integer a = luaL_checkinteger(L, 1);
    lua_Integer b = luaL_checkinteger(L, 2);
    if (b == 0)
        return luaL_error(L, "division by zero");
    lua_pushinteger(L, a / b);
    return 1;
}

static int safe_divide(lua_State *L) {
    lua_getglobal(L, "debug");
    if (!lua_istable(L, -1)) {
        lua_pop(L, 1);
        return luaL_error(L, "debug library not available");
    }
    lua_getfield(L, -1, "traceback");
    lua_remove(L, -2);

    lua_pushcfunction(L, c_divide);
    lua_pushvalue(L, 1);
    lua_pushvalue(L, 2);

    if (lua_pcall(L, 2, 1, -4) != LUA_OK) {

        return 1;
    }
    return 1;
}

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    lua_pushcfunction(L, c_divide);
    lua_setglobal(L, "c_divide");

    lua_pushcfunction(L, safe_divide);
    lua_setglobal(L, "safe_divide");

    luaL_dostring(L, "print(c_divide(10, 3))");

    assert(luaL_dostring(L, "print(c_divide('a', 3))") != LUA_OK);
    lua_pop(L, 1);

    assert(luaL_dostring(L, "print(c_divide(10, 0))") != LUA_OK);
    lua_pop(L, 1);

    luaL_dostring(L, "print(safe_divide(10, 0))");

    lua_close(L);
    puts("06-errors: ok");
    return 0;
}

Userdata

Userdata 是 Lua 用来在脚本中保存任意 C 数据的类型。它是 C 扩展 Lua 的基石。

两种 Userdata

类型API特点
Full userdatalua_newuserdatauv(L, size, nuvalue)Lua 分配内存,受 GC 管理
Light userdatalua_pushlightuserdata(L, p)仅保存 C 指针,不受 GC 管理

Full Userdata 详解

创建

// 分配一块大小为 sizeof(MyStruct) 的内存
MyStruct *obj = (MyStruct *)lua_newuserdatauv(L, sizeof(MyStruct), 0);
obj->x = 0;
obj->y = 0;

lua_newuserdatauv 的第三个参数是关联的 uservalue 数量(Lua 5.4), 用于将 Lua 值与 userdata 关联。此处设为 0 表示不关联。

创建后,userdata 在 Lua 中是一个完整对象,可以赋给变量、放入 table、 作为参数传递。当不再被引用时,Lua GC 会自动回收它。

读写

C 代码通过返回的指针直接读写内存:

// 从 Lua 获取 userdata
MyStruct *obj = (MyStruct *)lua_touserdata(L, 1);
obj->x += 10;

Lua 代码只能持有 userdata 引用,不能直接访问其字段。 通常通过 C 函数来读写(第 8 章会讲 metatable 如何让 userdata 支持 . 访问)。

Light Userdata

用于传递已存在的 C 指针:

void *some_c_ptr = ...;
lua_pushlightuserdata(L, some_c_ptr);
lua_setglobal(L, "c_handle");

注意:Light userdata 不受 GC 管理,生命周期由 C 代码保证。 它也没有独立的元表(所有 light userdata 共享同一个元表)。

完整代码


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

typedef struct {
    int count;
} Counter;

static int get_counter(lua_State *L) {
    Counter *c = (Counter *)lua_newuserdatauv(L, sizeof(Counter), 0);
    c->count = 0;
    return 1;
}

static int inc_counter(lua_State *L) {
    Counter *c = (Counter *)lua_touserdata(L, 1);
    int n = (int)luaL_optinteger(L, 2, 1);
    c->count += n;
    lua_pushinteger(L, c->count);
    return 1;
}

static int get_count(lua_State *L) {
    Counter *c = (Counter *)lua_touserdata(L, 1);
    lua_pushinteger(L, c->count);
    return 1;
}

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    lua_pushcfunction(L, get_counter);
    lua_setglobal(L, "get_counter");
    lua_pushcfunction(L, inc_counter);
    lua_setglobal(L, "inc_counter");
    lua_pushcfunction(L, get_count);
    lua_setglobal(L, "get_count");

    const char *script = "local c = get_counter()\n"
                         "print('initial:', get_count(c))\n"
                         "print('inc 5:', inc_counter(c, 5))\n"
                         "print('inc 1:', inc_counter(c))\n"
                         "c = nil\n"
                         "collectgarbage('collect')\n"
                         "print('done')\n";

    luaL_dostring(L, script);

    lua_close(L);
    puts("07-userdata: ok");
    return 0;
}

Metatable

Metatable 让 C 可以为 userdata 和 table 定义自定义行为(运算符重载、面向对象等)。

常用元方法

元方法触发时机
__index访问不存在的字段
__newindex给不存在的字段赋值
__gcGC 回收对象时
__len使用 # 运算符
__tostringtostring()print()
__eq, __lt, __le比较运算符
__add, __sub, __mul, __div算术运算符
__call将对象当作函数调用

在 C 中设置 Metatable

为 Userdata 设置(最常见)

// 注册 metatable 到注册表 
void register_my_meta(lua_State *L) {
    luaL_newmetatable(L, "MyType");       // 若不存在则创建 

    lua_pushcfunction(L, my_tostring);
    lua_setfield(L, -2, "__tostring");

    lua_pushcfunction(L, my_add);
    lua_setfield(L, -2, "__add");

    lua_pop(L, 1);       // 弹出 metatable 
}

// 创建对象时 
MyType *obj = lua_newuserdatauv(L, sizeof(MyType), 0);
luaL_getmetatable(L, "MyType");
lua_setmetatable(L, -2);

__index 的两种模式

模式 A:__index 是一个 table

lua_newtable(L);                     // 方法表 
lua_pushcfunction(L, method1);
lua_setfield(L, -2, "method1");
lua_pushcfunction(L, method2);
lua_setfield(L, -2, "method2");

luaL_newmetatable(L, "MyType");
lua_pushvalue(L, -2);                // 复制方法表 
lua_setfield(L, -2, "__index");      // mt.__index = methods 
lua_pop(L, 2);

这是实现面向对象风格 obj:method() 最简洁的方式。

模式 B:__index 是一个函数

适合需要动态计算字段,或字段名不确定的情况。

__gc 元方法(析构)

当 userdata 被 GC 回收时,如果其 metatable 有 __gc 字段,Lua 会调用它。 这是释放 C 资源(如文件句柄、网络连接)的正确位置:

static int mystruct_gc(lua_State *L) {
    MyStruct *obj = (MyStruct *)luaL_checkudata(L, 1, "MyStructMeta");
    printf("MyStruct %p collected\n", (void *)obj);
    return 0;
}

类型安全检查

luaL_checkudata 确保参数是指定类型的 userdata:

MyStruct *obj = (MyStruct *)luaL_checkudata(L, 1, "MyStructMeta");

如果类型不匹配,会自动抛出 Lua 错误。

注册表(Registry)

luaL_newmetatable 实际上是在 Lua Registry 中创建一个以字符串为 key 的 table。 Registry 是一个全局的、对 C 代码可见的 table,Lua 脚本无法直接访问 (除非用 debug 库)。它是 C 模块间共享数据的常用机制。

// 手动读写 registry
lua_pushstring(L, "my_key");
lua_gettable(L, LUA_REGISTRYINDEX);   // 读取 registry["my_key"]

lua_pushstring(L, "my_key");
lua_pushinteger(L, 42);
lua_settable(L, LUA_REGISTRYINDEX);   // registry["my_key"] = 42

完整代码


#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>

#define VEC2_MT "Vec2Meta"

typedef struct {
    double x, y;
} Vec2;

static Vec2 *check_vec2(lua_State *L, int idx) {
    return (Vec2 *)luaL_checkudata(L, idx, VEC2_MT);
}

static int vec2_tostring(lua_State *L) {
    Vec2 *v = check_vec2(L, 1);
    lua_pushfstring(L, "Vec2(%f, %f)", v->x, v->y);
    return 1;
}

static int vec2_add(lua_State *L) {
    Vec2 *a = check_vec2(L, 1);
    Vec2 *b = check_vec2(L, 2);
    Vec2 *res = (Vec2 *)lua_newuserdatauv(L, sizeof(Vec2), 0);
    res->x = a->x + b->x;
    res->y = a->y + b->y;
    luaL_getmetatable(L, VEC2_MT);
    lua_setmetatable(L, -2);
    return 1;
}

static int vec2_len(lua_State *L) {
    Vec2 *v = check_vec2(L, 1);
    lua_pushnumber(L, v->x * v->x + v->y * v->y);
    return 1;
}

static int vec2_eq(lua_State *L) {
    Vec2 *a = check_vec2(L, 1);
    Vec2 *b = check_vec2(L, 2);
    lua_pushboolean(L, a->x == b->x && a->y == b->y);
    return 1;
}

static int vec2_new(lua_State *L) {
    double x = luaL_optnumber(L, 1, 0);
    double y = luaL_optnumber(L, 2, 0);
    Vec2 *v = (Vec2 *)lua_newuserdatauv(L, sizeof(Vec2), 0);
    v->x = x;
    v->y = y;
    luaL_getmetatable(L, VEC2_MT);
    lua_setmetatable(L, -2);
    return 1;
}

static int vec2_index(lua_State *L) {
    Vec2 *v = check_vec2(L, 1);
    const char *key = luaL_checkstring(L, 2);
    if (strcmp(key, "x") == 0) {
        lua_pushnumber(L, v->x);
        return 1;
    }
    if (strcmp(key, "y") == 0) {
        lua_pushnumber(L, v->y);
        return 1;
    }

    luaL_getmetatable(L, VEC2_MT);
    lua_pushvalue(L, 2);
    lua_rawget(L, -2);
    return 1;
}

int luaopen_vec2(lua_State *L) {

    luaL_newmetatable(L, VEC2_MT);
    lua_pushcfunction(L, vec2_tostring);
    lua_setfield(L, -2, "__tostring");
    lua_pushcfunction(L, vec2_add);
    lua_setfield(L, -2, "__add");
    lua_pushcfunction(L, vec2_len);
    lua_setfield(L, -2, "__len");
    lua_pushcfunction(L, vec2_eq);
    lua_setfield(L, -2, "__eq");
    lua_pushcfunction(L, vec2_index);
    lua_setfield(L, -2, "__index");
    lua_pop(L, 1);

    lua_newtable(L);
    lua_pushcfunction(L, vec2_new);
    lua_setfield(L, -2, "new");
    return 1;
}

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    luaopen_vec2(L);
    lua_setglobal(L, "vec2");

    const char *script = "local vec2 = vec2\n"
                         "local a = vec2.new(1, 2)\n"
                         "local b = vec2.new(3, 4)\n"
                         "local c = a + b\n"
                         "print('a =', a)\n"
                         "print('b =', b)\n"
                         "print('a + b =', c)\n"
                         "print('a.x =', a.x)\n"
                         "print('#a (len_sq) =', #a)\n"
                         "print('a == b ?', a == b)\n"
                         "local a2 = vec2.new(1, 2)\n"
                         "print('a == a2 ?', a == a2)\n";

    luaL_dostring(L, script);

    lua_close(L);
    puts("08-metatable: ok");
    return 0;
}

协程(Coroutine)

Lua 的协程是非对称协程(asymmetric coroutine),支持 yield/resume。C API 允许 C 代码创建、恢复和中断 Lua 协程。

核心概念

  • Thread:在 Lua C API 中,协程的类型是 LUA_TTHREAD,用 lua_State* 表示
  • 主线程也是一个 lua_State*,由 luaL_newstate 创建
  • 协程有自己的调用栈和全局环境,但共享同一个全局表

创建协程

lua_State *co = lua_newthread(L);   // 在当前栈压入 thread,返回其状态机指针 

创建后,协程的栈是空的。需要把要运行的函数压入它的栈:

lua_State *co = lua_newthread(L);
lua_getglobal(co, "my_coroutine_func");   // 在 co 的栈上压入函数 

恢复(Resume)与让出(Yield)

从 C 恢复协程

int status = lua_resume(co, L, nargs, &nresults);
返回值含义
LUA_OK协程正常结束
LUA_YIELD协程执行了 coroutine.yield(),可再次 resume
其他出错,错误信息在 co 的栈顶

在 C 函数中让出

被 Lua 协程调用的 C 函数也可以让出执行权,但需要使用 C 延续点(C continuation)

static int my_c_func(lua_State *L) {
        // ... 做一些工作 ... 
    return lua_yieldk(L, nresults, ctx, my_continuation);
}

static int my_continuation(lua_State *L, int status, lua_KContext ctx) {
        // 协程被 resume 后,从这里继续执行 
    return 0;
}

在标准 Lua 5.4 中,C 函数中直接调用 lua_yield 是允许的(在协程内)。但如果是在主线程中调用,会报错。

典型使用模式

lua_State *co = lua_newthread(L);
lua_getglobal(co, "producer");   // 协程入口函数 

int nresults;
while ((status = lua_resume(co, L, 0, &nresults)) == LUA_YIELD) {
        // 协程 yield 了一个值,在 co 栈上 
    printf("yielded: %s\n", lua_tostring(co, -1));
    lua_pop(co, nresults);       // 清理返回值 
        // 可以再次 resume 
}

if (status != LUA_OK) {
    fprintf(stderr, "coroutine error: %s\n", lua_tostring(co, -1));
}

完整代码


#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    const char *script = "function counter(max)\n"
                         "    for i = 1, max do\n"
                         "        coroutine.yield(i)\n"
                         "    end\n"
                         "    return 'done'\n"
                         "end\n";

    luaL_dostring(L, script);

    lua_State *co = lua_newthread(L);
    lua_getglobal(co, "counter");
    lua_pushinteger(co, 5);

    int status;
    int nresults;
    int narg = 1;
    int expected = 1;

    while ((status = lua_resume(co, L, narg, &nresults)) == LUA_YIELD) {
        assert(lua_tointeger(co, -nresults) == expected);
        lua_pop(co, nresults);
        narg = 0;
        expected++;
    }

    assert(status == LUA_OK);
    assert(nresults == 1);
    assert(strcmp(lua_tostring(co, 1), "done") == 0);
    lua_pop(co, nresults);

    lua_pop(L, 1);

    lua_close(L);
    puts("09-coroutine: ok");
    return 0;
}

垃圾回收(GC)

Lua 使用增量式标记-清除垃圾回收器。C 代码可以通过 API 与 GC 交互。

控制 GC

lua_gc(L, LUA_GCCOLLECT, 0);       // 执行完整 GC 周期 
lua_gc(L, LUA_GCSTOP, 0);          // 停止 GC 
lua_gc(L, LUA_GCRESTART, 0);       // 重启 GC 
lua_gc(L, LUA_GCSETSTEPMUL, 200);  // 设置步进倍率 
lua_gc(L, LUA_GCSETPAUSE, 100);    // 设置暂停比率 

查询内存使用

int kbytes = lua_gc(L, LUA_GCCOUNT, 0);        // 已用内存(KB) 
int bytes  = lua_gc(L, LUA_GCCOUNTB, 0);       // 已用内存的余数(字节) 

总字节数 ≈ kbytes * 1024 + bytes

弱引用与 GC

通过 metatable 的 __mode 字段可以创建弱引用 table:

lua_newtable(L);                    // 创建 table 
lua_newtable(L);                    // 创建其 metatable 
lua_pushstring(L, "kv");            // key 和 value 都是弱引用 
lua_setfield(L, -2, "__mode");
lua_setmetatable(L, -2);            // 设置 metatable 

当 key 或 value 不再被其他地方引用时,它们会从弱引用 table 中自动移除。

__gc 与终结器

Full userdata 的 __gc 是 Lua GC 最重要的 C 交互点。注意以下几点:

  1. resurrection 循环:如果 __gc 中将 userdata 重新放入全局表,它会被复活,但下次 GC 不会再调用 __gc
  2. 顺序不确定:多个 userdata 的 __gc 调用顺序没有保证
  3. 不要在 __gc 中做复杂操作:避免在 __gc 中抛出错误或创建新对象

C 层面的对象生命周期管理

如果你有一个 C 库管理的外部资源池,可以将 Lua userdata 作为句柄:

typedef struct {
    int handle;
} Resource;

static int resource_gc(lua_State *L) {
    Resource *r = (Resource *)luaL_checkudata(L, 1, "ResourceMT");
    release_handle(r->handle);
    return 0;
}

这样 Lua 代码可以像普通对象一样使用资源,而 C 库保证资源最终被释放。

完整代码


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

#define BOX_MT "BoxMeta"

typedef struct {
    int id;
} Box;

static int box_gc(lua_State *L) {
    Box *b = (Box *)luaL_checkudata(L, 1, BOX_MT);
    printf("[gc] Box #%d collected\n", b->id);
    return 0;
}

static int box_new(lua_State *L) {
    int id = (int)luaL_checkinteger(L, 1);
    Box *b = (Box *)lua_newuserdatauv(L, sizeof(Box), 0);
    b->id = id;
    luaL_getmetatable(L, BOX_MT);
    lua_setmetatable(L, -2);
    return 1;
}

int luaopen_box(lua_State *L) {
    luaL_newmetatable(L, BOX_MT);
    lua_pushcfunction(L, box_gc);
    lua_setfield(L, -2, "__gc");
    lua_pop(L, 1);

    lua_newtable(L);
    lua_pushcfunction(L, box_new);
    lua_setfield(L, -2, "new");
    return 1;
}

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    luaopen_box(L);
    lua_setglobal(L, "box");

    int kbytes = lua_gc(L, LUA_GCCOUNT, 0);
    assert(kbytes > 0);

    const char *script = "local t = {}\n"
                         "for i = 1, 5 do\n"
                         "    t[i] = box.new(i)\n"
                         "end\n"
                         "print('created 5 boxes')\n"
                         "for i = 1, 5 do\n"
                         "    t[i] = nil\n"
                         "end\n"
                         "collectgarbage('collect')\n"
                         "print('after gc')\n";

    luaL_dostring(L, script);

    kbytes = lua_gc(L, LUA_GCCOUNT, 0);
    assert(kbytes > 0);

    lua_close(L);
    puts("10-gc: ok");
    return 0;
}

Upvalue 与 C 闭包

Lua C API 中的 lua_CFunction 本身是无状态的。如果希望 C 函数携带私有数据(类似 Lua 闭包中的 upvalue),需要使用 C 闭包(C closure)

核心 API

void lua_pushcclosure(lua_State *L, lua_CFunction fn, int n);
  • 将栈顶的 n 个值捕获为 upvalue
  • 然后压入一个 C 闭包(类型为 LUA_TFUNCTION
#define lua_upvalueindex(i)  (LUA_REGISTRYINDEX - (i))
  • 通过伪索引访问闭包的 upvalue
  • lua_upvalueindex(1) 访问第 1 个 upvalue,lua_upvalueindex(2) 访问第 2 个,以此类推

lua_pushcfunction(L, f) 只是 lua_pushcclosure(L, f, 0) 的宏,表示没有 upvalue。

最简单的示例:带前缀的日志函数

static int logger(lua_State *L) {
    const char *prefix = lua_tostring(L, lua_upvalueindex(1));
    const char *msg    = luaL_checkstring(L, 1);
    printf("[%s] %s\n", prefix, msg);
    return 0;
}

// 注册时 
lua_pushstring(L, "ERROR");
lua_pushcclosure(L, logger, 1);   // 捕获 1 个 upvalue 
lua_setglobal(L, "log_error");

Lua 中调用:

log_error("disk full")   -- 输出: [ERROR] disk full

Upvalue 是引用还是拷贝?

Upvalue 在创建闭包时按值捕获。如果 upvalue 是 table,捕获的是 table 引用;如果 upvalue 是 string/number,捕获的是当时的值。后续通过 lua_upvalueindex 读写的是闭包内部保存的那份值。

修改 Upvalue

可以通过 lua_pushvalue + lua_setupvalue 或直接在函数内通过栈操作修改:

static int counter_next(lua_State *L) {
    int n = lua_tointeger(L, lua_upvalueindex(1));
    n = n + 1;
    lua_pushinteger(L, n);
    lua_copy(L, -1, lua_upvalueindex(1));       // 写回 upvalue 
    lua_pop(L, 1);
    return 1;
}

lua_copy(L, fromidx, toidx)fromidx 的值复制到 toidx,不修改栈高度。

用 Upvalue 实现迭代器

这是 C 闭包最经典的用法之一:

static int iter_next(lua_State *L) {
    int i = lua_tointeger(L, lua_upvalueindex(1));
    int max = lua_tointeger(L, lua_upvalueindex(2));
    if (i > max) return 0;       // 结束迭代 
    lua_pushinteger(L, i);
    lua_pushinteger(L, i + 1);
    lua_copy(L, -1, lua_upvalueindex(1));       // 更新计数器 
    lua_pop(L, 1);
    return 1;
}

static int make_iter(lua_State *L) {
    int max = luaL_checkinteger(L, 1);
    lua_pushinteger(L, 1);         // upvalue 1: 当前值 
    lua_pushinteger(L, max);       // upvalue 2: 上限 
    lua_pushcclosure(L, iter_next, 2);
    return 1;
}

Lua 中使用:

for x in make_iter(5) do
    print(x)
end

与 Lua 闭包的区别

特性Lua 闭包C 闭包
upvalue 数量无限制最多 255 个(通常够用)
upvalue 类型任意 Lua 值任意 Lua 值
共享 upvalue支持(同一局部变量的多个闭包共享)不共享(每个 C 闭包独立拷贝)

C 闭包的 upvalue 在闭包创建时被复制到闭包内部。即使后续修改了原始栈上的值,已创建的闭包不受影响。

常见模式:模块级别的配置表

// 创建模块时,把整个配置 table 作为 upvalue 
lua_newtable(L);
// ... 填充配置 ... 
lua_pushcclosure(L, my_function, 1);  // 配置表作为 upvalue 1 

这样 my_function 内部无需通过全局变量或 registry 查找配置,直接通过 lua_upvalueindex(1) 访问,性能更好,也更封装。

完整代码


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

static int logger(lua_State *L) {
    const char *prefix = lua_tostring(L, lua_upvalueindex(1));
    const char *msg = luaL_checkstring(L, 1);
    printf("[%s] %s\n", prefix, msg);
    return 0;
}

static int counter_next(lua_State *L) {
    int step = (int)lua_tointeger(L, lua_upvalueindex(1));
    int cur = (int)lua_tointeger(L, lua_upvalueindex(2));
    int max = (int)lua_tointeger(L, lua_upvalueindex(3));

    if (cur > max)
        return 0;

    lua_pushinteger(L, cur);
    lua_pushinteger(L, cur + step);
    lua_copy(L, -1, lua_upvalueindex(2));
    lua_pop(L, 1);
    return 1;
}

static int make_counter(lua_State *L) {
    int start = (int)luaL_optinteger(L, 1, 1);
    int step = (int)luaL_optinteger(L, 2, 1);
    int max = (int)luaL_optinteger(L, 3, 10);

    lua_pushinteger(L, step);
    lua_pushinteger(L, start);
    lua_pushinteger(L, max);
    lua_pushcclosure(L, counter_next, 3);
    return 1;
}

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    lua_pushstring(L, "INFO");
    lua_pushcclosure(L, logger, 1);
    lua_setglobal(L, "log_info");

    lua_pushstring(L, "WARN");
    lua_pushcclosure(L, logger, 1);
    lua_setglobal(L, "log_warn");

    lua_pushcfunction(L, make_counter);
    lua_setglobal(L, "make_counter");

    const char *script = "log_info('system started')\n"
                         "log_warn('low memory')\n"
                         "print('counter 1 to 5:')\n"
                         "for v in make_counter(1, 1, 5) do\n"
                         "    print(' ', v)\n"
                         "end\n"
                         "print('counter 10 to 30 step 5:')\n"
                         "for v in make_counter(10, 5, 30) do\n"
                         "    print(' ', v)\n"
                         "end\n";

    luaL_dostring(L, script);

    lua_close(L);
    puts("11-upvalue: ok");
    return 0;
}

状态机与内存管理

除了 luaL_newstate() 这种开箱即用的方式,Lua C API 还提供了更底层的状态机控制接口。

自定义内存分配器

luaL_newstate() 内部使用标准 malloc/free。如果你需要自定义内存管理(如使用内存池、追踪泄漏、限制总量),可以使用 lua_newstate

lua_State *lua_newstate(lua_Alloc f, void *ud);

lua_Alloc 的签名:

typedef void * (*lua_Alloc)(void *ud, void *ptr, size_t osize, size_t nsize);
场景参数值行为
分配新块ptr=NULL, osize=0, nsize>0分配 nsize 字节
重新分配ptr!=NULL, osize=原大小, nsize>0调整为 nsize 字节
释放ptr!=NULL, nsize=0释放 ptr

osize 的值还暗示了分配用途(Lua 内部用此做统计):

  • osize == LUA_TSTRING 等:为特定类型分配
  • 其他值:原始字节数

Extra Space

每个 lua_State 在头部预留了一块额外空间(大小由 LUAI_EXTRASPACE 决定),可通过宏访问:

void *extra = lua_getextraspace(L);

常用于存储与状态机关联的自定义上下文指针,避免全局变量。

查询与替换分配器

lua_Alloc old = lua_getallocf(L, &old_ud);
lua_setallocf(L, my_alloc, my_ud);

栈转移

当有两个 lua_State*(如主线程和协程)时,可以用 lua_xmove 在它们之间转移栈值:

lua_xmove(from, to, n);   // 从 'from' 栈顶弹出 n 个值,压入 'to' 

lua_xmove 要求两个状态机共享同一个全局表(即由 lua_newthread 创建的协程)。任意两个独立状态机之间不能使用

lua_xmove

版本查询

lua_Number ver = lua_version(L);

返回 LUA_VERSION_NUM(如 504),可用于运行时版本检查。

Panic 函数

当 Lua 错误发生在没有任何保护帧的情况下(极其严重的编程错误),Lua 会调用 panic 函数

lua_CFunction old = lua_atpanic(L, my_panic);

默认 panic 函数打印错误信息并调用 abort()。嵌入到游戏引擎或 GUI 程序时,通常需要替换为自定义实现以优雅退出。

完整代码


#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static size_t total_allocated = 0;
static size_t total_freed = 0;
static size_t current_used = 0;

static void *my_alloc(void *ud, void *ptr, size_t osize, size_t nsize) {
    (void)ud;
    (void)osize;

    if (nsize == 0) {
        if (ptr) {
            total_freed += osize;
            current_used -= osize;
            free(ptr);
        }
        return NULL;
    } else {
        void *newp = realloc(ptr, nsize);
        if (newp && ptr == NULL) {

            total_allocated += nsize;
            current_used += nsize;
        } else if (newp && ptr != NULL) {

            current_used = current_used - osize + nsize;
            total_allocated += (nsize > osize) ? (nsize - osize) : 0;
        }
        return newp;
    }
}

static int my_panic(lua_State *L) {
    const char *msg = lua_tostring(L, -1);
    fprintf(stderr, "PANIC: unprotected error in call to Lua API (%s)\n",
            msg ? msg : "?");
    return 0;
}

int main(void) {

    lua_State *L = lua_newstate(my_alloc, NULL);
    if (!L) {
        fputs("cannot create state\n", stderr);
        return 1;
    }

    lua_atpanic(L, my_panic);

    assert(lua_version(L) == 504);

    luaL_openlibs(L);

    {
        int *ctx = (int *)lua_getextraspace(L);
        *ctx = 42;
        assert(*(int *)lua_getextraspace(L) == 42);
    }

    size_t before = current_used;
    (void)luaL_dostring(L, "local t = {} for i = 1, 1000 do t[i] = i end");
    assert(current_used > before);

    lua_State *co = lua_newthread(L);
    lua_pushstring(L, "hello from main");
    lua_xmove(L, co, 1);
    assert(strcmp(lua_tostring(co, -1), "hello from main") == 0);
    lua_pop(co, 1);

    void *ud;
    lua_Alloc allocf = lua_getallocf(L, &ud);
    assert(allocf == my_alloc);
    assert(ud == NULL);

    lua_close(L);
    assert(total_allocated > 0);
    assert(total_freed > 0);
    puts("12-state: ok");
    return 0;
}

算术、比较与字符串操作

Lua C API 提供了直接在栈上执行算术运算、比较和字符串操作的函数,无需在 Lua 代码中完成。

栈上算术运算

void lua_arith(lua_State *L, int op);

操作数必须从栈顶弹出:

  • 一元运算(UNMBNOT):弹出 1 个,压入结果
  • 二元运算(ADDSUB 等):弹出 2 个,压入结果

支持的操作码:

含义
LUA_OPADD加法 +
LUA_OPSUB减法 -
LUA_OPMUL乘法 *
LUA_OPMOD取模 %
LUA_OPPOW^
LUA_OPDIV除法 /
LUA_OPIDIV整除 //
LUA_OPBAND按位与 &
LUA_OPBOR按位或 |
LUA_OPBXOR按位异或 ~
LUA_OPSHL左移 <<
LUA_OPSHR右移 >>
LUA_OPUNM取负 -
LUA_OPBNOT按位非 ~

操作数类型必须兼容(整数或浮点),否则会触发错误。在 pcall 保护外使用需谨慎。

比较

int lua_compare(lua_State *L, int idx1, int idx2, int op);
int lua_rawequal(lua_State *L, int idx1, int idx2);
含义
LUA_OPEQ等于 ==
LUA_OPLT小于 <
LUA_OPLE小于等于 <=
  • lua_compare 触发元方法(__eq__lt__le
  • lua_rawequal 不触发元方法,直接比较原始值

返回值:1 表示比较成立,0 表示不成立。

连接

void lua_concat(lua_State *L, int n);

将栈顶 n 个值弹出,连接成一个字符串压回栈顶。如果值是 number,会自动转换为 string。会触发 __concat 元方法。

长度

void lua_len(lua_State *L, int idx);

#idx 的结果压入栈顶。对 table 会触发 __len 元方法。对 string 返回字节长度。

字符串转数字

size_t lua_stringtonumber(lua_State *L, const char *s);

将字符串 s 转换为 number/integer 并压入栈。返回转换消耗的字节数(0 表示失败)。

辅助库字符串函数

luaL_tolstring

const char *luaL_tolstring(lua_State *L, int idx, size_t *len);

调用 tostring(idx),将结果压入栈顶并返回字符串指针。与 lua_tolstring 不同,它会触发 __tostring 元方法。

luaL_gsub

const char *luaL_gsub(lua_State *L, const char *s,
                       const char *p, const char *r);

将字符串 s 中的所有 p 替换为 r,结果压入栈顶并返回指针。

完整代码


#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    lua_pushinteger(L, 10);
    lua_pushinteger(L, 3);
    lua_arith(L, LUA_OPADD);
    assert(lua_tointeger(L, -1) == 13);
    lua_pop(L, 1);

    lua_pushinteger(L, 10);
    lua_pushinteger(L, 3);
    lua_arith(L, LUA_OPIDIV);
    assert(lua_tointeger(L, -1) == 3);
    lua_pop(L, 1);

    lua_pushinteger(L, 7);
    lua_arith(L, LUA_OPBNOT);
    assert(lua_tointeger(L, -1) == -8);
    lua_pop(L, 1);

    lua_pushinteger(L, 5);
    lua_pushinteger(L, 10);
    assert(lua_compare(L, -2, -1, LUA_OPLT) == 1);
    assert(lua_rawequal(L, -2, -1) == 0);
    lua_pop(L, 2);

    lua_pushstring(L, "Hello");
    lua_pushstring(L, " ");
    lua_pushstring(L, "Lua");
    lua_concat(L, 3);
    assert(strcmp(lua_tostring(L, -1), "Hello Lua") == 0);
    lua_pop(L, 1);

    lua_pushinteger(L, 42);
    lua_pushstring(L, " apples");
    lua_concat(L, 2);
    assert(strcmp(lua_tostring(L, -1), "42 apples") == 0);
    lua_pop(L, 1);

    lua_pushstring(L, "hello");
    lua_len(L, -1);
    assert(lua_tointeger(L, -1) == 5);
    lua_pop(L, 2);

    lua_newtable(L);
    lua_pushinteger(L, 1);
    lua_seti(L, -2, 1);
    lua_pushinteger(L, 2);
    lua_seti(L, -2, 2);
    lua_pushinteger(L, 3);
    lua_seti(L, -2, 3);
    lua_len(L, -1);
    assert(lua_tointeger(L, -1) == 3);
    lua_pop(L, 2);

    size_t n = lua_stringtonumber(L, "3.14159");
    assert(n == 8);
    assert(lua_tonumber(L, -1) == 3.14159);
    lua_pop(L, 1);

    n = lua_stringtonumber(L, "not a number");
    assert(n == 0);

    lua_pushinteger(L, 12345);
    size_t len;
    const char *s = luaL_tolstring(L, -1, &len);
    assert(strcmp(s, "12345") == 0);
    assert(len == 5);
    lua_pop(L, 2);

    s = luaL_gsub(L, "hello world", "world", "lua");
    assert(strcmp(s, "hello lua") == 0);
    lua_pop(L, 1);

    lua_close(L);
    puts("13-arith: ok");
    return 0;
}

加载与字节码

除了 luaL_loadstringluaL_loadfile,Lua C API 提供了更底层的加载接口,支持从任意来源读取代码、加载预编译字节码、以及将 Lua 函数序列化为字节码。

底层加载:lua_load

int lua_load(lua_State *L, lua_Reader reader, void *data,
             const char *chunkname, const char *mode);
参数说明
reader回调函数,每次提供一块源代码/字节码
data用户数据,传给 reader
chunkname用于错误信息和调试信息的名字
mode"t"(文本)、"b"(字节码)、"bt"(两者)

返回值:LUA_OKLUA_ERRSYNTAXLUA_ERRMEM

Reader 回调

typedef const char * (*lua_Reader)(lua_State *L, void *ud, size_t *sz);
  • 返回指向数据块的指针,并通过 *sz 设置大小
  • 没有更多数据时返回 NULL

辅助库加载函数

int luaL_loadfilex(lua_State *L, const char *filename, const char *mode);
int luaL_loadbufferx(lua_State *L, const char *buff, size_t sz,
                     const char *name, const char *mode);
  • luaL_loadfilexluaL_loadfile 的可指定模式版本
  • luaL_loadbufferx 从内存缓冲区加载,可指定模式

出于安全考虑,默认模式通常是 "bt"。如果你只信任文本源码,可以强制 "t"

序列化:lua_dump

int lua_dump(lua_State *L, lua_Writer writer, void *data, int strip);

将栈顶的 Lua 函数序列化为字节码。strip 为真时移除调试信息(减小体积但错误信息会变差)。

Writer 回调

typedef int (*lua_Writer)(lua_State *L, const void *p, size_t sz, void *ud);

返回 0 表示成功,非 0 表示出错(会触发 lua_dump 返回错误码)。

典型应用场景

  1. 自定义加载器:从加密文件、网络流、内存映射文件加载 Lua 代码
  2. 字节码缓存:将常用脚本预编译为字节码,运行时直接加载二进制 buffer
  3. 沙箱:限制只允许加载文本源码("t"),防止加载不可信字节码

字节码兼容性

Lua 字节码不保证跨版本兼容。Lua 5.4 的字节码不能直接在 5.3 运行,反之亦然。即使是 5.4 的小版本之间,也建议重新编译。

完整代码


#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>

static char bytecode[4096];
static size_t bytecode_len = 0;

static int writer(lua_State *L, const void *p, size_t sz, void *ud) {
    (void)L;
    (void)ud;
    if (bytecode_len + sz > sizeof(bytecode)) {
        return 1;
    }
    memcpy(bytecode + bytecode_len, p, sz);
    bytecode_len += sz;
    return 0;
}

static const char *reader(lua_State *L, void *ud, size_t *sz) {
    (void)L;
    (void)ud;
    if (bytecode_len == 0) {
        *sz = 0;
        return NULL;
    }
    *sz = bytecode_len;
    bytecode_len = 0;
    return bytecode;
}

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    const char *code = "return function(x) return x * 2 end";
    luaL_loadbufferx(L, code, strlen(code), "inline", "t");
    lua_pcall(L, 0, 1, 0);
    lua_pushinteger(L, 21);
    lua_pcall(L, 1, 1, 0);
    assert(lua_tointeger(L, -1) == 42);
    lua_pop(L, 1);

    luaL_loadstring(L, "return function(a,b) return a+b end");
    lua_pcall(L, 0, 1, 0);

    bytecode_len = 0;
    lua_dump(L, writer, NULL, 0);
    assert(bytecode_len > 0);
    lua_pop(L, 1);

    lua_load(L, reader, NULL, "from_mem", "b");
    lua_pushinteger(L, 10);
    lua_pushinteger(L, 32);
    lua_pcall(L, 2, 1, 0);
    assert(lua_tointeger(L, -1) == 42);
    lua_pop(L, 1);

    lua_close(L);
    puts("14-load: ok");
    return 0;
}

引用系统

Lua C API 提供了一套轻量的引用机制,用于在 C 代码中安全地引用 Lua 值,防止其被 GC 回收。

核心概念

Registry 是一个全局 table(只能通过伪索引 LUA_REGISTRYINDEX 访问)。引用系统利用 Registry 中的某个子 table 来保存被引用的值:

int luaL_ref(lua_State *L, int t);
void luaL_unref(lua_State *L, int t, int ref);
常量含义
LUA_NOREF-2无效引用
LUA_REFNIL-1引用 nil(不存入 table)

使用流程

// 1. 把要引用的值压入栈 
lua_pushstring(L, "important data");

// 2. 创建引用(值会被弹出或移入 registry)
int ref = luaL_ref(L, LUA_REGISTRYINDEX);

// ... 之后任意时刻 ... 

// 3. 通过引用取回值 
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
// 栈顶是 "important data" 

// 4. 释放引用,允许 GC 回收 
luaL_unref(L, LUA_REGISTRYINDEX, ref);

引用表的自动复用

luaL_ref 使用一种简单但高效的策略:

  • 引用表的第一个元素(整数 key 0)保存下一个可用的空闲槽位
  • 当释放引用时,该槽位被链入空闲列表
  • 因此引用值不会无限增长

你可以自己创建任意 table 作为引用表(不一定是 Registry),只要遵守同样的约定即可。

典型应用场景

  1. C 对象持有 Lua 回调:C 结构体中保存一个 int ref,指向 Lua 函数。需要调用时通过 lua_rawgeti 取回。
  2. 反向映射:从 C 指针查找对应的 Lua userdata/table。
  3. 延迟处理:将事件相关的 Lua 值暂存,等事件触发时再取出。

与全局变量的区别

  • 全局变量(lua_setglobal)用字符串 key,容易被 Lua 脚本意外覆盖
  • 引用用整数 key,C 代码独占管理,更安全、更快

直接使用 Registry

除了 luaL_ref,也可以用 light userdata 作为 key 直接操作 Registry,适合存储少量 C 指针或私有数据:

static char MY_KEY;   // 静态变量地址在进程内唯一

// 存入
lua_pushlightuserdata(L, (void *)&MY_KEY);
lua_pushlightuserdata(L, (void *)my_ptr);
lua_rawset(L, LUA_REGISTRYINDEX);

// 取出
lua_pushlightuserdata(L, (void *)&MY_KEY);
lua_rawget(L, LUA_REGISTRYINDEX);
void *my_ptr = lua_touserdata(L, -1);
lua_pop(L, 1);

// 删除
lua_pushlightuserdata(L, (void *)&MY_KEY);
lua_pushnil(L);
lua_rawset(L, LUA_REGISTRYINDEX);

这种方式不需要管理整数 ref,key 天然唯一且不会被 Lua 脚本伪造。

完整代码


#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>

typedef struct {
    int ref;
    char name[32];
} Callback;

static Callback cb = {LUA_NOREF, "my_callback"};

static int set_callback(lua_State *L) {

    if (cb.ref != LUA_NOREF) {
        luaL_unref(L, LUA_REGISTRYINDEX, cb.ref);
    }

    luaL_checktype(L, 1, LUA_TFUNCTION);
    cb.ref = luaL_ref(L, LUA_REGISTRYINDEX);
    return 0;
}

static int fire_callback(lua_State *L) {
    if (cb.ref == LUA_NOREF || cb.ref == LUA_REFNIL) {
        return luaL_error(L, "no callback registered");
    }

    lua_rawgeti(L, LUA_REGISTRYINDEX, cb.ref);
    lua_pushstring(L, "event data");
    if (lua_pcall(L, 1, 1, 0) == LUA_OK) {
        lua_pop(L, 1);
        return 0;
    } else {
        return lua_error(L);
    }
}

static int clear_callback(lua_State *L) {
    (void)L;
    if (cb.ref != LUA_NOREF) {
        luaL_unref(L, LUA_REGISTRYINDEX, cb.ref);
        cb.ref = LUA_NOREF;
    }
    return 0;
}

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    lua_pushcfunction(L, set_callback);
    lua_setglobal(L, "set_callback");
    lua_pushcfunction(L, fire_callback);
    lua_setglobal(L, "fire_callback");
    lua_pushcfunction(L, clear_callback);
    lua_setglobal(L, "clear_callback");

    const char *script = "set_callback(function(msg)\n"
                         "    print('Lua callback got:', msg)\n"
                         "    return 'ack'\n"
                         "end)\n"
                         "fire_callback()\n"
                         "clear_callback()\n"
                         "-- 再次注册然后 fire\n"
                         "set_callback(function(msg) return 'second' end)\n"
                         "fire_callback()\n"
                         "clear_callback()\n";

    luaL_dostring(L, script);

    lua_close(L);
    puts("15-ref: ok");
    return 0;
}

辅助缓冲区(luaL_Buffer)

当需要在 C 代码中逐步构建一个长字符串时,反复使用 lua_pushstring + lua_concat 效率很低。Lua 辅助库提供了专门的 Buffer 机制。

基本用法

luaL_Buffer b;
luaL_buffinit(L, &b);              // 初始化 
luaL_addstring(&b, "Hello");       // 追加 
luaL_addchar(&b, ' ');
luaL_addstring(&b, "Lua");
luaL_pushresult(&b);               // 将结果压入栈 

Buffer 内部使用一块局部数组作为初始空间,超出后自动通过 Lua 分配器扩展。

常用 API

函数/宏说明
luaL_buffinit(L, B)初始化 Buffer
luaL_prepbuffsize(B, sz)确保剩余空间 ≥ sz,返回可写入的 char*
luaL_prepbuffer(B)同上,使用默认大小 LUAL_BUFFERSIZE
luaL_addchar(B, c)追加单个字符
luaL_addstring(B, s)追加 C 字符串
luaL_addlstring(B, s, l)追加指定长度的字符串
luaL_addvalue(B)将栈顶值弹出并追加到 buffer
luaL_pushresult(B)完成,将最终字符串压入栈
luaL_pushresultsize(B, sz)完成并指定结果大小
luaL_buffinitsize(L, B, sz)初始化并预分配 sz 空间
luaL_bufflen(B)当前已写入长度
luaL_buffaddr(B)当前缓冲区的 char* 地址

高级用法:直接写入缓冲区

luaL_Buffer b;
char *p = luaL_prepbuffsize(&b, 256);
// 直接向 p 写入最多 256 字节 
size_t written = sprintf(p, "value=%d", 42);
luaL_addsize(&b, written);   // 通知 buffer 实际写入量 
luaL_pushresult(&b);

将栈值追加到 buffer

luaL_addvalue 非常有用,可以将 Lua 值(如 tostring 后的数字)追加:

luaL_Buffer b;
luaL_buffinit(L, &b);
lua_pushinteger(L, 42);
luaL_tolstring(L, -1, NULL);   // 转换为 string 压栈 
luaL_addvalue(&b);             // 弹出并追加 
luaL_pushresult(&b);

注意:luaL_addvalue弹出栈顶值。

完整代码


#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    luaL_Buffer b;
    luaL_buffinit(L, &b);
    luaL_addstring(&b, "Hello");
    luaL_addchar(&b, ' ');
    luaL_addstring(&b, "World");
    luaL_pushresult(&b);
    assert(strcmp(lua_tostring(L, -1), "Hello World") == 0);
    lua_pop(L, 1);

    luaL_buffinit(L, &b);
    for (int i = 1; i <= 5; i++) {
        char *p = luaL_prepbuffsize(&b, 32);
        int n = snprintf(p, 32, "[%d] ", i);
        luaL_addsize(&b, (size_t)n);
    }
    luaL_pushresult(&b);
    assert(strcmp(lua_tostring(L, -1), "[1] [2] [3] [4] [5] ") == 0);
    lua_pop(L, 1);

    luaL_buffinit(L, &b);
    luaL_addstring(&b, "values: ");
    for (int i = 1; i <= 3; i++) {
        lua_pushinteger(L, i * 10);
        luaL_tolstring(L, -1, NULL);
        luaL_addvalue(&b);
        if (i < 3)
            luaL_addstring(&b, ", ");
    }
    luaL_pushresult(&b);
    assert(strcmp(lua_tostring(L, -1), "values: 10, 20, 30") == 0);
    lua_pop(L, 1);

    luaL_buffinit(L, &b);
    for (int i = 0; i < 1000; i++) {
        luaL_addstring(&b, "x");
    }
    luaL_pushresult(&b);
    size_t len;
    lua_tolstring(L, -1, &len);
    assert(len == 1000);
    lua_pop(L, 1);

    lua_close(L);
    puts("16-buffer: ok");
    return 0;
}

调试接口

Lua 提供了一套完整的 C 级别调试 API,允许宿主程序检查调用栈、设置钩子(hook)、操作局部变量和 upvalue。

Hook 机制

通过 lua_sethook 可以在特定事件发生时执行回调:

void lua_sethook(lua_State *L, lua_Hook f, int mask, int count);
事件掩码触发时机
LUA_MASKCALL调用函数时
LUA_MASKRET函数返回时
LUA_MASKLINE执行新行时
LUA_MASKCOUNT每执行 count 条指令时

Hook 函数签名:

typedef void (*lua_Hook)(lua_State *L, lua_Debug *ar);

ar->event 表示当前事件类型:LUA_HOOKCALLLUA_HOOKRETLUA_HOOKLINELUA_HOOKCOUNTLUA_HOOKTAILCALL

在 hook 中通常只能做安全检查、计数或轻量信息收集,不应执行复杂 Lua 操作。

获取调用栈信息

int lua_getstack(lua_State *L, int level, lua_Debug *ar);
int lua_getinfo(lua_State *L, const char *what, lua_Debug *ar);
  • lua_getstack 填充 ar 中的 i_ci(CallInfo 指针)
  • lua_getinfo 根据 what 字符串查询详细信息

what 字符含义:

字符获取信息
nnamenamewhat(函数名)
Ssourcesrclenlinedefinedlastlinedefinedwhatshort_src
lcurrentline
unupsnparamsisvararg
tistailcall
rftransferntransfer(尾调用传值信息)
L将活跃行号压入栈(table)
f将正在运行的函数压入栈

Lua 函数的 Upvalue 操作

这与第 11 章的 C 闭包 upvalue 不同,这里操作的是 Lua 函数的 upvalue

const char *lua_getupvalue(lua_State *L, int funcindex, int n);
const char *lua_setupvalue(lua_State *L, int funcindex, int n);
  • funcindex 指向栈上的 Lua 函数
  • n 是 upvalue 索引(从 1 开始)
  • 返回 upvalue 名称

辅助库:luaL_traceback

void luaL_traceback(lua_State *L, lua_State *L1,
                    const char *msg, int level);

L1 的调用栈回溯信息压入 L 的栈。这是生成错误堆栈最便捷的方式。

完整代码


#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>

static int hook_calls = 0;
static int hook_lines = 0;

static void my_hook(lua_State *L, lua_Debug *ar) {
    (void)L;
    if (ar->event == LUA_HOOKCALL)
        hook_calls++;
    else if (ar->event == LUA_HOOKLINE)
        hook_lines++;
}

static int traceback_msgh(lua_State *L) {
    luaL_traceback(L, L, lua_tostring(L, 1), 1);
    return 1;
}

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    lua_sethook(L, my_hook, LUA_MASKCALL | LUA_MASKLINE, 0);

    luaL_dostring(L, "function fact(n)\n"
                    "    if n <= 1 then return 1 end\n"
                    "    return n * fact(n - 1)\n"
                    "end\n"
                    "return fact(5)\n");
    assert(hook_calls > 0);
    assert(hook_lines > 0);
    lua_pop(L, 1);
    lua_sethook(L, NULL, 0, 0);

    lua_getglobal(L, "fact");
    lua_Debug ar;
    assert(lua_getinfo(L, ">nuS", &ar) == 1);
    assert(ar.linedefined == 1);
    assert(ar.nups == 1);
    assert(ar.nparams == 1);
    assert(ar.isvararg == 0);

    lua_pushcfunction(L, traceback_msgh);
    luaL_loadstring(L, "function a() b() end\n"
                      "function b() c() end\n"
                      "function c() error('boom') end\n"
                      "a()\n");
    assert(lua_pcall(L, 0, 0, -2) != LUA_OK);
    assert(strstr(lua_tostring(L, -1), "boom") != NULL);
    lua_pop(L, 1);
    lua_pop(L, 1);

    luaL_dostring(L, "local x = 10\n"
                    "local f = function() return x end\n"
                    "return f\n");
    const char *name = lua_getupvalue(L, -1, 1);
    assert(name != NULL);
    assert(strcmp(name, "x") == 0);
    assert(lua_tointeger(L, -1) == 10);
    lua_pop(L, 1);

    lua_pushinteger(L, 99);
    name = lua_setupvalue(L, -2, 1);
    assert(name != NULL);
    lua_pcall(L, 0, 1, 0);
    assert(lua_tointeger(L, -1) == 99);
    lua_pop(L, 1);

    if (lua_gettop(L) > 0 && lua_isfunction(L, -1))
        lua_pop(L, 1);

    lua_close(L);
    puts("17-debug: ok");
    return 0;
}

警告系统与杂项

Lua 5.4 引入了警告系统,允许 Lua 核心和 C 代码发出非致命警告信息。此外还有一些零散的实用 API。

警告系统

设置警告处理函数

void lua_setwarnf(lua_State *L, lua_WarnFunction f, void *ud);

lua_WarnFunction 签名:

typedef void (*lua_WarnFunction)(void *ud, const char *msg, int tocont);
  • tocont == 0:这是一条完整警告的结束
  • tocont != 0:后续还有片段,应继续拼接

发出警告

void lua_warning(lua_State *L, const char *msg, int tocont);

C 代码或绑定库可以用此向宿主程序报告非致命问题。

默认行为(未设置警告函数时)警告被忽略。嵌入到 IDE 或游戏引擎时,建议设置自定义函数将警告路由到日志系统。

字符串转数字(栈操作)

size_t lua_stringtonumber(lua_State *L, const char *s);

将字符串 s 解析为 Lua number/integer 并压入栈。返回成功解析的字符数(0 表示失败)。

关闭槽位(to-be-closed)

Lua 5.4 支持 <close> 变量。C API 中对应:

void lua_toclose(lua_State *L, int idx);
void lua_closeslot(lua_State *L, int idx);
  • lua_toclose 将栈位置 idx 标记为 to-be-closed。当该位置被弹出(如作用域结束)时,如果值有 __close 元方法,Lua 会调用它。
  • lua_closeslot 强制立即关闭指定槽位。

典型用途:在 C 函数中创建需要确保清理的资源对象(如临时锁、事务句柄),即使后续发生错误也能正确释放。

分配器查询与替换

lua_Alloc lua_getallocf(lua_State *L, void **ud);
void lua_setallocf(lua_State *L, lua_Alloc f, void *ud);

用于在运行时替换内存分配器。例如,某些场景下需要临时切换为带统计的分配器。

C 栈深度限制

int lua_setcstacklimit(lua_State *L, unsigned int limit);

设置 C 调用栈的使用上限(以字节计)。防止 C 代码递归过深导致栈溢出。返回之前的限制值。

完整代码


#include <assert.h>
#include <lauxlib.h>
#include <lua.h>
#include <lualib.h>
#include <stdio.h>
#include <string.h>

static char warn_buffer[256];
static int warn_len = 0;

static void my_warn(void *ud, const char *msg, int tocont) {
    (void)ud;
    size_t n = strlen(msg);
    if ((size_t)warn_len + n < sizeof(warn_buffer)) {
        memcpy(warn_buffer + warn_len, msg, n);
        warn_len += (int)n;
    }
    if (!tocont) {
        warn_buffer[warn_len] = '\0';
        printf("[WARN] %s\n", warn_buffer);
        warn_len = 0;
    }
}

static int close_resource(lua_State *L) {
    const char *name = lua_tostring(L, 1);
    printf("[close] resource '%s' is being closed\n", name);
    return 0;
}

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    lua_setwarnf(L, my_warn, NULL);

    lua_warning(L, "something", 1);
    lua_warning(L, " odd happened", 0);

    (void)luaL_dostring(L, "@on");
    (void)luaL_dostring(L, "warn('deprecated API used')");
    (void)luaL_dostring(L, "@off");

    size_t n = lua_stringtonumber(L, "42");
    assert(n == 3);
    assert(lua_tointeger(L, -1) == 42);
    lua_pop(L, 1);

    n = lua_stringtonumber(L, "3.14extra");
    assert(n == 0);

    lua_newtable(L);
    lua_pushstring(L, "my_resource");
    lua_setfield(L, -2, "name");

    lua_newtable(L);
    lua_pushcfunction(L, close_resource);
    lua_setfield(L, -2, "__close");
    lua_setmetatable(L, -2);

    lua_toclose(L, -1);
    lua_pop(L, 1);

    void *ud;
    lua_Alloc allocf = lua_getallocf(L, &ud);
    assert(allocf != NULL);

    lua_close(L);
    puts("18-warn: ok");
    return 0;
}

标准库加载

Lua 的核心解释器本身只提供虚拟机、垃圾回收和 C API。所有内置函数(printmath.sinstring.format 等)都来自标准库,由独立的 C 模块实现。

lualib.h

头文件 lualib.h 声明了所有标准库的打开函数:

函数提供的 Lua 模块/全局
luaopen_base(L)_G、assert、print、type、pairs 等基础函数
luaopen_coroutine(L)coroutine
luaopen_table(L)table
luaopen_io(L)io
luaopen_os(L)os
luaopen_string(L)string
luaopen_utf8(L)utf8
luaopen_math(L)math
luaopen_debug(L)debug
luaopen_package(L)packagerequire

打开全部标准库

#include <lualib.h>

luaL_openlibs(L);   // 打开所有标准库 

这等价于依次调用上述所有 luaopen_* 函数。

按需加载

在沙箱或内存受限环境中,通常只加载必要的库:

luaopen_base(L);
luaopen_math(L);
luaopen_string(L);
// 不加载 io/os/debug,限制文件系统和环境访问 

自定义 package 搜索器

luaopen_package 注册了 require 机制和默认搜索路径。你可以:

  1. 修改 package.path / package.cpath 指向自定义目录
  2. 插入自定义搜索器package.searchers
  3. 完全替换 require 的实现(手动注册同名全局函数)

预加载模块

lauxlib.h 中定义了 Registry key:

#define LUA_PRELOAD_TABLE "_PRELOAD"

可以通过在预加载表中注册函数,让 require 在未找到文件时调用你的加载器:

luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_PRELOAD_TABLE);
lua_pushcfunction(L, my_custom_loader);
lua_setfield(L, -2, "mymodule");
lua_pop(L, 1);

之后在 Lua 中 require("mymodule") 就会调用 my_custom_loader

完整代码


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

static int open_mymodule(lua_State *L) {
    lua_newtable(L);
    lua_pushstring(L, "hello from preload");
    lua_setfield(L, -2, "msg");
    return 1;
}

int main(void) {
    lua_State *L = luaL_newstate();

    luaopen_base(L);
    luaopen_math(L);
    lua_setglobal(L, "math");
    luaopen_string(L);
    lua_setglobal(L, "string");

    luaL_dostring(L, "print('math.pi =', math.pi)");
    luaL_dostring(L, "print('string.upper =', string.upper('hi'))");
    luaL_dostring(L, "print(io)");

    luaL_getsubtable(L, LUA_REGISTRYINDEX, LUA_PRELOAD_TABLE);
    lua_pushcfunction(L, open_mymodule);
    lua_setfield(L, -2, "mymodule");
    lua_pop(L, 1);

    luaopen_package(L);
    luaL_dostring(L, "local m = require('mymodule')\n"
                    "print('mymodule.msg =', m.msg)\n");

    luaL_openlibs(L);
    luaL_dostring(L, "print('os.time =', os.time())");

    lua_close(L);
    puts("19-stdlib: ok");
    return 0;
}