FFI 概述
LuaJIT FFI(Foreign Function Interface)库允许 Lua 代码直接调用 C 函数和使用 C 数据结构,无需编写传统的 Lua C 扩展。相比 Lua 原生 C API,FFI 提供了更简洁的接口和更好的性能(JIT 编译器可以内联 FFI 调用)。
local ffi = require("ffi")
-- 直接调用 C 标准库函数
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("Hello from FFI!\n")
基本类型映射
FFI 在 C 类型和 Lua 类型之间自动转换:
| C 类型 | Lua 类型 | 说明 |
|---|---|---|
bool, int, double | number | 数值自动转换 |
const char* | string | 字符串自动转换 |
void* | cdata | 指针类型 |
struct | cdata | 结构体类型 |
function pointer | cdata | 函数指针 |
local ffi = require("ffi")
-- C 基本类型
local x = ffi.new("int", 42)
local y = ffi.new("double", 3.14)
print(x, y) -- 42 3.14
-- 类型转换
local n = ffi.cast("int", 3.7)
print(n) -- 3
使用 C 标准库函数
local ffi = require("ffi")
-- 声明 C 函数
ffi.cdef[[
// 数学函数
double sqrt(double x);
double pow(double base, double exp);
// 字符串函数
size_t strlen(const char *s);
char *strcpy(char *dest, const char *src);
int strcmp(const char *s1, const char *s2);
// 内存函数
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
void free(void *ptr);
void *memcpy(void *dest, const void *src, size_t n);
]]
-- 调用数学函数
print(ffi.C.sqrt(144)) -- 12.0
print(ffi.C.pow(2, 10)) -- 1024.0
-- 调用字符串函数
print(ffi.C.strlen("hello")) -- 5
结构体操作
local ffi = require("ffi")
-- 定义结构体
ffi.cdef[[
typedef struct {
float x;
float y;
float z;
} Vec3;
typedef struct {
char name[64];
int age;
float score;
} Student;
]]
-- 创建结构体实例
local v = ffi.new("Vec3", 1.0, 2.0, 3.0)
print(v.x, v.y, v.z) -- 1.0 2.0 3.0
-- 修改字段
v.x = 10.0
print(v.x) -- 10.0
-- 使用初始化器
local s = ffi.new("Student", {name = "张三", age = 20, score = 95.5})
print(ffi.string(s.name), s.age, s.score)
-- 结构体数组
local points = ffi.new("Vec3[3]")
points[0].x = 1; points[0].y = 0; points[0].z = 0
points[1].x = 0; points[1].y = 1; points[1].z = 0
points[2].x = 0; points[2].y = 0; points[2].z = 1
数组和指针
local ffi = require("ffi")
-- 创建数组
local arr = ffi.new("int[10]")
for i = 0, 9 do
arr[i] = i * i
end
-- 遍历数组
for i = 0, 9 do
io.write(arr[i] .. " ")
end
print() -- 0 1 4 9 16 25 36 49 64 81
-- 带初始化的数组
local arr2 = ffi.new("int[5]", {10, 20, 30, 40, 50})
-- 变长数组(VLA)
local n = 100
local buf = ffi.new("char[?]", n)
-- 指针操作
local p = ffi.new("int[1]", 42)
print(p[0]) -- 42
-- ffi.cast 转换指针类型
local vp = ffi.cast("void*", p)
local ip = ffi.cast("int*", vp)
print(ip[0]) -- 42
封装 C 库
以下示例展示如何封装一个 C 库为 Lua 模块:
local ffi = require("ffi")
-- 声明 C 接口
ffi.cdef[[
typedef struct {
double x;
double y;
} Point;
double point_distance(Point *a, Point *b);
void point_rotate(Point *p, double angle);
]]
-- 内联 C 代码(用于演示,实际项目中应链接外部库)
local lib = ffi.load("m") -- 加载数学库
-- 用 Lua 实现 C 函数(演示用)
local function point_distance(a, b)
local dx = a.x - b.x
local dy = a.y - b.y
return math.sqrt(dx * dx + dy * dy)
end
-- 创建 Lua 封装模块
local Point = {}
Point.__index = Point
local Point_mt = ffi.metatype("Point", {
__index = Point,
__tostring = function(p)
return string.format("(%.2f, %.2f)", p.x, p.y)
end,
__add = function(a, b)
return ffi.new("Point", a.x + b.x, a.y + b.y)
end,
__sub = function(a, b)
return ffi.new("Point", a.x - b.x, a.y - b.y)
end,
__mul = function(p, scalar)
return ffi.new("Point", p.x * scalar, p.y * scalar)
end
})
function Point.new(x, y)
return ffi.new("Point", x or 0, y or 0)
end
function Point:distance(other)
return point_distance(self, other)
end
function Point:length()
return math.sqrt(self.x * self.x + self.y * self.y)
end
function Point:normalize()
local len = self:length()
if len > 0 then
return ffi.new("Point", self.x / len, self.y / len)
end
return ffi.new("Point", 0, 0)
end
-- 使用
local p1 = Point.new(3, 4)
local p2 = Point.new(6, 8)
print(p1) -- (3.00, 4.00)
print(p2) -- (6.00, 8.00)
print(p1 + p2) -- (9.00, 12.00)
print(p1:distance(p2)) -- 5.0
print(p1:length()) -- 5.0
print(p1:normalize()) -- (0.60, 0.80)
调用系统 API
Linux/macOS 系统调用
local ffi = require("ffi")
ffi.cdef[[
// 时间函数
typedef long time_t;
time_t time(time_t *t);
// 文件操作
typedef struct { void *ptr; } FILE;
FILE *fopen(const char *path, const char *mode);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
int fclose(FILE *stream);
// 进程信息
int getpid(void);
int getppid(void);
]]
-- 获取当前时间
local t = ffi.C.time(nil)
print("Unix 时间戳:", tonumber(t))
-- 获取进程 ID
print("PID:", ffi.C.getpid())
print("PPID:", ffi.C.getppid())
-- 文件写入
local f = ffi.C.fopen("/tmp/ffi_test.txt", "w")
if f ~= nil then
local msg = "Hello from LuaJIT FFI!\n"
ffi.C.fwrite(msg, 1, #msg, f)
ffi.C.fclose(f)
print("文件写入成功")
end
内存管理
FFI 对象的内存管理需要特别注意:
local ffi = require("ffi")
-- ffi.new 创建的对象由 GC 自动管理
local buf = ffi.new("char[1024]") -- GC 会自动释放
-- ffi.gc 注册终结器
local resource = ffi.gc(
ffi.new("int[1]"),
function(p)
print("资源被释放")
end
)
-- ffi.C.malloc 需要手动释放
ffi.cdef[[
void *malloc(size_t size);
void free(void *ptr);
]]
local ptr = ffi.C.malloc(1024)
-- 注册自动释放
ptr = ffi.gc(ffi.cast("char*", ptr), ffi.C.free)
-- 当 ptr 不再被引用时,GC 会自动调用 free
回调函数
FFI 支持将 Lua 函数作为回调传递给 C 函数:
local ffi = require("ffi")
ffi.cdef[[
typedef int (*compare_fn)(const void *, const void *);
void qsort(void *base, size_t nmemb, size_t size, compare_fn compar);
]]
-- 创建回调函数
local compare_ints = ffi.cast("compare_fn", function(a, b)
local ia = ffi.cast("int*", a)[0]
local ib = ffi.cast("int*", b)[0]
if ia < ib then return -1 end
if ia > ib then return 1 end
return 0
end)
-- 排序数组
local arr = ffi.new("int[5]", {5, 3, 1, 4, 2})
ffi.C.qsort(arr, 5, ffi.sizeof("int"), compare_ints)
for i = 0, 4 do
io.write(arr[i] .. " ")
end
print() -- 1 2 3 4 5
-- 重要:防止回调被 GC
-- 保持 compare_ints 的引用,否则 GC 可能回收它
_G._keep_ref = compare_ints
性能对比
local ffi = require("ffi")
-- 对比 Lua 表和 FFI 结构体的性能
ffi.cdef[[
typedef struct {
float x, y, z;
} FFIPoint;
]]
local N = 1000000
-- Lua 表
local start = os.clock()
local sum = 0
for i = 1, N do
local p = {x = i, y = i * 2, z = i * 3}
sum = sum + p.x + p.y + p.z
end
print(string.format("Lua 表: %.3f 秒", os.clock() - start))
-- FFI 结构体
local start = os.clock()
sum = 0
for i = 1, N do
local p = ffi.new("FFIPoint", i, i * 2, i * 3)
sum = sum + p.x + p.y + p.z
end
print(string.format("FFI 结构体: %.3f 秒", os.clock() - start))
-- FFI 数组(最佳性能)
local start = os.clock()
sum = 0
local points = ffi.new("FFIPoint[?]", N)
for i = 0, N - 1 do
points[i].x = i + 1
points[i].y = (i + 1) * 2
points[i].z = (i + 1) * 3
end
for i = 0, N - 1 do
sum = sum + points[i].x + points[i].y + points[i].z
end
print(string.format("FFI 数组: %.3f 秒", os.clock() - start))
注意事项
使用 LuaJIT FFI 时需要注意以下要点:
- FFI 仅在 LuaJIT 中可用,标准 Lua 不支持
- FFI 回调函数必须保持引用,防止被 GC 回收
ffi.cast不会进行类型检查,错误的类型转换会导致崩溃ffi.string用于将char*转换为 Lua 字符串- FFI 对象不是 Lua 表,不能用
pairs/ipairs遍历 ffi.sizeof返回 C 类型的大小,不同于 Lua 的#操作符- 线程安全:FFI 调用可能在任意时刻被中断,共享数据需要同步
- 在 macOS 上加载系统库可能需要指定完整路径
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。