lua调用C/C++的方法详解
Lua通过虚拟栈与C/C++交互,将C函数封装为接口供其调用。调用时,C从栈中获取参数并返回结果,参数传递与结果返回均通过栈操作完成,从而实现了Lua对C能力的高效、便捷调用,使得Lua能无缝利用C语言的高性能库。
1 lua vs C/C++
Lua 和 C/C++ 的取舍,其实是一道经典的选择题。Lua 作为脚本语言,上手门槛低,支持热更新,开发效率高——但代价是性能。C/C++ 编译型,性能是它的底气,可门槛也摆在那里:写不好的人,产出的代码可能还不如 Lua 快,而且动不动就 core dump,更别提热更新了。
不过话说回来,Lua 本身就是用 C 实现的,这套血缘关系让两者之间打通“任督二脉”并不复杂——只需要把 C 的能力封装成 Lua 能调用的接口就行。

2 C/C++如何给lua提供接口
翻看一个典型 Lua 模块的源代码会发现,它往往既包含 Lua 代码也包含 C 代码。C 代码负责干重活:获取参数、调用系统 API、返回结果,然后编译成 .so 供 Lua 调用。Lua 代码则对 C 暴露出来的底层接口进行再封装,让调用更顺手。所以,Lua 调用 C 函数的关键,就集中在 C 函数里如何接收参数、如何返回结果上。
下面以 linotify 项目为例来拆解这个过程。
2.1 lua模块的查找
当你在 Lua 里写下 require("inotify") 时,它怎么知道上哪儿找这个模块?是 .lua 脚本还是 .so 动态库?
和其他脚本语言类似,Lua 通过变量来控制搜索路径:package.path 负责 Lua 脚本,package.cpath 负责 .so 动态库。搜索顺序是:先找 Lua 脚本,再找 .so。顺着这个流程,它在当前目录找到了 inotify.so。
2.2 so的入口
调用 inotify.so 里的函数,本质还是动态库那套机制:dlopen 装载,dlsym 定位函数。当执行 require("inotify") 时,如果还没加载过这个 .so,Lua 会先调用 dlopen 把它拉进内存。
随后,Lua 中执行 local wd = handle:addwatch('/home/rob/', inotify.IN_CREATE, inotify.IN_MOVE) 时,实际触发的是 C 函数 handle_add_watch()。
2.3 参数获取
Lua 和 C 之间通过虚拟栈(virtual stack)来传递数据。C 函数拿到的第一个参数是 lua_State * 指针——可以把它理解成 Lua 虚拟机的状态句柄。
调用 C 函数的第一步,就是从栈里取出参数。举个例子,inotify.so 中的参数获取代码长这样:
fd = get_inotify_handle(L, 1);
path = luaL_checkstring(L, 2);
top = lua_gettop(L);
for (i = 3; i <= top; i++) {
mask |= (uint32_t)luaL_checkinteger(L, i);
}
解释一下:get_inotify_handle(L, 1) 取栈中第一个参数(自定义句柄),luaL_checkstring(L, 2) 取第二个参数,并校验它是字符串。然后通过 lua_gettop(L) 获取参数总数,循环处理剩下的参数,依次取整数并通过位或合并到 mask 中。这样就把 addwatch() 的三个参数全部剥离出来了。之后调用真正的系统接口 inotify_add_watch() 完成监听注册。
2.4 返回值
业务逻辑跑完,怎么把结果传回 Lua?还是走栈,只不过这次是入栈。
比如 inotify_add_watch() 返回一个监听描述符 wd。成功时,调用 lua_pushinteger(L, wd) 入栈;失败时,则返回三个值:
static int handle_error(lua_State *L)
{
lua_pushnil(L);
lua_pushstring(L, strerror(errno));
lua_pushinteger(L, errno);
return 3;
}
第一个是 nil,第二个是错误描述字符串,第三个是错误码。所以 Lua 端的调用就可以写成:
local wd, err_info, errno = handle:addwatch('/home/rob/', inotify.IN_CREATE, inotify.IN_MOVE)
if wd == nil then
print("ERROR=", err_info)
end
注意 C 函数 handle_add_watch() 最后的返回值——它告诉 Lua 这个函数返回了几个值。成功时返回 1(只有描述符),失败时返回 3(nil、错误信息、错误码)。
2.5 一个小的demo
有了上面的基础,自己动手写一个简单的示例吧。假设我们要实现一个加法:实际计算在 C 里完成,Lua 只管调用。
#include#include static int handle_add(lua_State *L) { int a, b, c; a = luaL_checkinteger(L, 1); b = luaL_checkinteger(L, 2); c = a + b; lua_pushinteger(L, c); return 1; } static luaL_Reg funcs[] = { {"add", handle_add}, {NULL, NULL} }; int luaopen_demo(lua_State *L) { lua_createtable(L, 0, sizeof(funcs)/sizeof(luaL_Reg) - 1); luaL_setfuncs(L, funcs, 0); return 1; }
入口函数是 luaopen_demo(),里面先 lua_createtable 创建一张新表,然后把函数注册进去。编译成 .so:
gcc demo.c -fPIC -shared -o demo.so
Lua 端调用:
local demo = require("demo")
print(demo.add(2, 3))
输出 5,完美。
3 lua FFI
用 C API 写 Lua 模块,需要你熟悉虚拟栈这套模式,写起来多少有些别扭。FFI(Foreign Function Interface)提供了一种更直接的方式——你可以在 Lua 脚本里直接声明并调用 C 函数。
3.1 一个小例子
local ffi = require("ffi")
ffi.cdef[[
int printf(const char*fmt, ...);
]]
ffi.C.printf("hello %s", "world");
先加载 FFI 模块,然后用 cdef 声明 C 函数的签名——这很像 C 语言的头文件。接着就能通过 ffi.C 直接调用 printf。用 LuaJIT 运行:luajit hell.lua,就能看到输出。
3.2 调用so
上面调用的是标准库函数,如果想调用自己编译的 .so 呢?
先写一个简单的 C 文件 libtest.c:
// libtest.c #includeint show(char *str) { int ret = 0; if(str == NULL) { ret = -1; } else { printf("input: %sn", str); } return ret; }
编译成 .so:
gcc -shared -fPIC libtest.c -o libtest.so
然后在 Lua 中通过 FFI 加载并调用:
local ffi = require("ffi")
-- 加载libtest.so
local myffi = ffi.load("test")
-- 声明函数原型
ffi.cdef[[
int show(char *str);
]]
local str1 = "hello"
-- 将字符串类型转换为char*
local str2 = ffi.cast("char *",str1)
-- 调用libtest.so中的show函数
print(myffi.show(str2))
注意这里需要 ffi.cast 类型转换,因为 Lua 的字符串和 C 的 char * 并非直接等价。
4 C API vs FFI
FFI 最大的优势是直观、性能高(尤其是 LuaJIT 下的优化),但兼容性是个问题——它高度依赖 LuaJIT,标准 Lua 解释器并不原生支持。而 C API 是 Lua 官方提供的标准接口,稳定性久经考验,不挑运行环境。怎么选,取决于你的项目对性能和跨平台可靠性的权重。
5 参考文档
- FFI Tutorial
Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。
极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。
















