Lua 与 C 语言的结合:嵌入与扩展实战

详解 Lua 与 C 语言的双向交互:通过 C API 在 C 程序中嵌入 Lua 虚拟机、用 C 编写 Lua 扩展模块、栈操作与数据交换的核心机制,附完整代码示例。

Lua 与 C 的结合是一套双向交互机制:「嵌入」(embedding)指 C 程序内嵌 Lua 虚拟机、加载并调用 Lua 脚本;「扩展」(extending)指用 C 编写函数和模块注册给 Lua 调用。两个方向都通过 Lua 的 C API 完成,而一个虚拟栈是两个语言之间交换数据的唯一通道。

Lua 栈:两种语言的交换通道

Lua 和 C 之间不直接共享变量,所有数据都通过一个「Lua 栈」传递:C 把值压入栈,Lua 从栈上取参数;Lua 函数返回时把结果压回栈,C 再读取。每个 lua_State 都有自己独立的栈。

栈索引的正负含义

栈可以用正数或负数索引访问:

索引方式含义示例
正数从栈底向上数,1 是第一个元素1 = 栈底第一个值
负数从栈顶向下数,-1 是栈顶-1 = 栈顶,-2 = 栈顶下面一个

例如 C 函数被 Lua 调用时,参数依次在栈底:1 是第一个参数,2 是第二个,-1 则是最后一个参数。压栈和弹栈时索引会变化,这是新手最常犯错的地方——lua_settable(L, -3) 这类调用前一定要在心里推演栈的布局。

常用 C API 速查表

系列函数作用
压栈 lua_pushXlua_pushnil / lua_pushboolean / lua_pushinteger / lua_pushnumber / lua_pushstring / lua_pushlstring / lua_pushcfunction把 C 值压入栈顶
取值 lua_toXlua_toboolean / lua_tointeger / lua_tonumber / lua_tostring / lua_tolstring把栈上某个索引的值转为 C 类型(不弹栈)
检查 luaL_checkXluaL_checkinteger / luaL_checknumber / luaL_checkstring读取参数并做类型检查,类型不符直接抛出 Lua 错误
类型判断lua_isnil / lua_istable / lua_isfunction / lua_type判断栈上值的类型
表操作lua_newtable / lua_getfield / lua_setfield / lua_gettable / lua_settable / lua_next创建、读写、遍历 table
栈管理lua_gettop / lua_settop / lua_pop / lua_pushvalue查询栈深、裁剪栈、弹栈、复制元素
调用lua_call / lua_pcall调用栈上的函数,后者是保护模式

方向一:Lua 调用 C 函数(扩展)

步骤 1: 编写 C 扩展

首先,我们需要编写一个 C 扩展,它将定义一个 C 函数,该函数可以在 Lua 中调用。

// 文件名:example.c
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

// 定义一个 C 函数,该函数将被 Lua 调用
static int my_c_function(lua_State* L) {
    // 假设我们从 Lua 栈中获取一个数字参数
    int param = luaL_checkinteger(L, 1);
    // 在 C 中处理这个参数
    int result = param * 2; // 示例:将参数乘以 2
    // 将结果压入 Lua 栈并返回结果的个数
    lua_pushinteger(L, result);
    return 1;
}

// 定义一个数组,包含要注册的 C 函数
static const luaL_Reg my_lib[] = {
    {"myFunction", my_c_function},
    {NULL, NULL}  // 注册表结束标志
};

// 初始化 C 库供 Lua 使用
int luaopen_mylibrary(lua_State* L) {
    luaL_newlib(L, my_lib);  // 创建新的 Lua 表,并将其作为库注册
    return 1;
}

C 函数的约定非常统一:参数从栈上取(1 起),返回值压栈后用 return n 告诉 Lua 返回了几个值。

步骤 2: 编译 C 扩展

使用 C 编译器编译上面的 C 代码,并生成一个可以被 Lua 调用的动态链接库(例如,在 Linux 上是 .so 文件,在 Windows 上是 .dll 文件)。编译命令可能如下:

gcc -Wall -shared -o libmylibrary.so example.c -I /usr/local/include/lua/5.3

这里 -I 选项指定了 Lua 头文件的位置,根据您的 Lua 安装情况可能有所不同。

步骤 3: 在 Lua 脚本中使用 C 扩展

一旦 C 扩展编译完成,您可以在 Lua 脚本中加载和使用它。

-- 文件名:example.lua
-- 加载 C 扩展
local mylibrary = require("mylibrary")

-- 调用 C 扩展中的函数
local result = mylibrary.myFunction(10)
print("Result from C function:", result)

步骤 4: 运行 Lua 脚本

在命令行中运行 Lua 脚本,确保 Lua 解释器的路径和动态链接库的路径正确。

lua example.lua

这将输出:

Result from C function: 20

这个例子展示了如何在 C 中定义一个简单的函数,编译成 Lua 可以调用的扩展,然后在 Lua 脚本中调用这个函数。

方向二:C 调用 Lua 函数(嵌入)

与上面的方向相反,宿主 C 程序也可以创建 Lua 虚拟机、加载脚本并调用其中的函数。下面的示例与上一节形成对照:

// 文件名:embed.c
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

int main(void) {
    // 创建 Lua 虚拟机
    lua_State* L = luaL_newstate();
    luaL_openlibs(L);  // 打开标准库

    // 加载并执行脚本文件
    if (luaL_dofile(L, "script.lua") != LUA_OK) {
        fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1));
        lua_close(L);
        return 1;
    }

    // 取得全局函数 add 并压栈
    lua_getglobal(L, "add");
    lua_pushinteger(L, 10);  // 第一个参数
    lua_pushinteger(L, 32);  // 第二个参数

    // 保护模式调用:2 个参数,1 个返回值
    if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
        fprintf(stderr, "Call error: %s\n", lua_tostring(L, -1));
        lua_pop(L, 1);
    } else {
        printf("Result from Lua: %d\n", (int)lua_tointeger(L, -1));
        lua_pop(L, 1);  // 弹出返回值
    }

    lua_close(L);  // 销毁虚拟机
    return 0;
}

对应的 Lua 脚本:

-- 文件名:script.lua
function add(a, b)
    return a + b
end

编译运行(以 Lua 5.4 为例):

gcc -o embed embed.c -I /usr/local/include/lua5.4 -llua5.4
./embed
# 输出:Result from Lua: 42

两个方向对比记忆:Lua 调 C 用 luaL_checkX 取参、lua_pushX 返回;C 调 Lua 用 lua_getglobal + lua_pushX 传参、lua_pcall 调用、lua_toX 取结果

复杂数据结构操作

使用 Lua 的 C API 进行更复杂的数据结构操作,涉及到对 Lua 栈的深入理解和操作。Lua 的 C API 提供了丰富的函数来创建、访问、修改和遍历各种 Lua 数据结构,包括但不限于表(table)、字符串、函数、线程等。

创建和操作表(Table)

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

void createTable(lua_State* L) {
    // 创建一个新的空表
    lua_newtable(L);
    
    // 向表中添加元素
    lua_pushstring(L, "key1");  // 将字符串 "key1" 压入栈
    lua_pushinteger(L, 123);    // 将整数 123 压入栈
    lua_settable(L, -3);        // 将 "key1" 与 123 关联,并从栈中弹出这两个元素

    // 将创建的表留在栈顶
}

void accessTable(lua_State* L) {
    // 假设栈顶元素是一个表
    lua_getfield(L, -1, "key1");  // 获取表中 "key1" 对应的值
    if (!lua_isnil(L, -1)) {
        int value = lua_tointeger(L, -1);  // 转换并获取值
        printf("Value of key1: %d\n", value);
    }
    lua_pop(L, 1);  // 弹出栈顶元素
}

遍历表

void iterateTable(lua_State* L) {
    if (!lua_istable(L, -1)) {
        return;
    }

    lua_pushnil(L);  // 将 nil 压入栈,作为遍历的初始键
    while (lua_next(L, -2) != 0) {
        // 栈现在包含键、值、表
        const char* key = lua_tostring(L, -2);
        int value = lua_tointeger(L, -1);
        printf("Key: %s, Value: %d\n", key, value);

        lua_pop(L, 1);  // 弹出值,留下键,为下次 lua_next 做准备
    }
}

操作字符串

void manipulateString(lua_State* L) {
    const char* str = "Hello, World!";
    lua_pushstring(L, str);  // 将字符串压入栈

    // 获取字符串长度
    size_t len;
    const char* luaStr = lua_tolstring(L, -1, &len);
    printf("String length: %zu\n", len);

    // 截取字符串的一部分
    lua_pushlstring(L, luaStr + 7, 5);  // 从索引 7 开始,长度为 5 的子字符串
}

C 边界的错误处理

Lua 的错误(error()、运行时异常)是基于 longjmp 的「异常」,绝不能让 Lua 错误穿越未做保护的 C 代码,否则宿主程序会直接崩溃。规则只有一条:凡是调用 Lua 代码的地方,都用 lua_pcall(保护调用)代替 lua_call

void handleError(lua_State* L) {
    // 假设栈顶是要调用的函数,无参数
    if (lua_pcall(L, 0, 0, 0) != LUA_OK) {
        // 出错时错误信息在栈顶
        const char* errorMsg = lua_tostring(L, -1);
        printf("Error: %s\n", errorMsg);
        lua_pop(L, 1);  // 清除错误信息
    }
}

lua_pcall 返回 LUA_OK 表示成功,其他值(如 LUA_ERRRUNLUA_ERRMEM)表示不同类型的失败,此时栈顶是错误对象。关于 Lua 侧错误处理(pcall / xpcall / error)的完整讨论,见 Lua 错误处理机制

注册 C 函数到 Lua

static const luaL_Reg my_lib[] = {
    {"createTable", createTable},
    {"accessTable", accessTable},
    {"iterateTable", iterateTable},
    {"manipulateString", manipulateString},
    {NULL, NULL}
};

int luaopen_mylibrary(lua_State* L) {
    luaL_newlib(L, my_lib);
    return 1;
}

这些例子展示了如何在 C 中创建和操作 Lua 表、遍历表、操作字符串、处理错误以及如何将 C 函数注册到 Lua。这些操作是 Lua C API 的基础,通过它们,您可以构建更复杂的数据结构和功能。在使用 Lua C API 时,始终要记得操作完数据结构后要正确地管理 Lua 栈,避免内存泄漏或栈溢出。

免绑定层的替代方案:LuaJIT FFI

如果你的项目使用 LuaJIT,还有一条绕过 C API 的路径:FFI(外部函数接口)允许直接在 Lua 代码里声明 C 函数签名并调用,无需编写任何绑定胶水代码:

local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("Hello from FFI: %d\n", 42)

FFI 省掉了「写 C 函数 → 压栈取参 → 注册」的整个流程,调用开销也更低,适合快速对接现成的 C 库;缺点是绑定 LuaJIT、对复杂结构体的声明较繁琐。详细内容见 LuaJIT FFI 详解。想了解嵌入技术的典型落地场景,可以参考 Lua 在游戏开发中的应用

常见问题(FAQ)

Lua 和 C 之间传数据为什么用栈?

栈是一个简单、统一且语言无关的中转层:C 不关心 Lua 值的内部表示,只需按约定压栈/取栈;Lua 虚拟机也能精确追踪栈上的每个值,配合垃圾回收保证安全。相比「直接共享内存」,栈模型避免了所有权和生命周期问题,这正是 Lua 能被安全嵌入成千上万宿主程序的关键设计。

LuaJIT 还需要 C API 吗?

需要,但可以少写很多。LuaJIT 完全兼容 Lua 5.1 的 C API,现有绑定层照常工作;同时 LuaJIT 独有的 FFI 允许免绑定直接调用 C 函数。简单原则是:对接已有 C 库、追求开发效率用 FFI;写需要跨 Lua 版本分发的模块,仍用标准 C API。

C 调用 Lua 性能开销大吗?

开销主要在「参数压栈/取栈 + 虚拟机调用约定」的固定成本上,对单次调用来说是微秒级以下的量级,绝大多数业务无感。真正要避免的是在高频热循环里做细粒度的 C↔Lua 往返调用——正确做法是把批量数据一次性传入,让 Lua 侧完成循环,减少跨界次数。

lua_pcall 和 lua_call 有什么区别?

lua_call 遇到错误会直接把错误抛出(longjmp),宿主程序若没做保护就会崩溃;lua_pcall 在保护模式下执行,出错时捕获错误并返回错误码,错误信息留在栈顶。嵌入场景下调用任何 Lua 代码都应该使用 lua_pcall

Lua 可以嵌入多线程程序吗?

可以,但每个线程应使用独立的 lua_State(或在加锁保护下共享)。同一个 lua_State 不是线程安全的;Lua 的协程(coroutine)是协作式的单线程调度,与 OS 线程是两个概念,不要混淆。

相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「lua」更多文章