LuaJIT FFI外部函数接口详解

全面掌握LuaJIT FFI库,学习如何直接调用C函数、使用C数据结构和编写高性能Lua扩展。

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, doublenumber数值自动转换
const char*string字符串自动转换
void*cdata指针类型
structcdata结构体类型
function pointercdata函数指针
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 上加载系统库可能需要指定完整路径

继续阅读

探索更多技术文章

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

全部文章 返回首页