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_pushX | lua_pushnil / lua_pushboolean / lua_pushinteger / lua_pushnumber / lua_pushstring / lua_pushlstring / lua_pushcfunction | 把 C 值压入栈顶 |
取值 lua_toX | lua_toboolean / lua_tointeger / lua_tonumber / lua_tostring / lua_tolstring | 把栈上某个索引的值转为 C 类型(不弹栈) |
检查 luaL_checkX | luaL_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_ERRRUN、LUA_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 线程是两个概念,不要混淆。
相关阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。