LuaJIT 之 FFI

1. FFI 教程

原文: FFI Tutorial
相关链接:OpenResty 最佳实践之 FFI

加载 FFI 库

FFI 库时默认编译进 LuaJIT 中的,但是不会默认加载或初始化。因此,当需要使用 FFI 库时,需要在 Lua 文件的开头添加如下语句:

local ffi = require("ffi")

访问标准系统函数

如下示例显示了如何访问标准系统函数。

local ffi = require("ffi")
ffi.cdef[[
    void Sleep(int ms);
    int poll(struct pollfd *fds, unsigned long nfds, int timeout);
]]

local sleep
if ffi.os == "Windows" then
    function sleep(s)
        ffi.C.Sleep(s*1000)
    end
else
    function sleep(s)
        ffi.C.poll(nil, 0, s*1000)
    end
end

for i = 1, 160 do
    io.write("."); io.flush()
    sleep(0.01)
end
io.write("\n")

访问 zlib 压缩库

如下示例显示了如果在 Lua 代码中访问 zlib 压缩库。

local ffi = require("ffi")
-- 定义由 zlib 提供的 C 函数
ffi.cdef[[
    unsigned long compressBound(unsigned long sourceLen);
    int compress2(uint8_t *dest, unsigned long *destLen, 
                  const uint8_t *source, unsigned long sourceLen, int level);
    int uncompress(uint8_t *dest, unsigned long *destLen, 
                   const uint8_t *source, unsigned long sourceLen);
]]
-- 加载 zlib 共享库。在 POSIX 系统上,名为 libz.so,通常是预安装的。
-- 因为 ffi.load() 会自动添加缺失的标准前缀/后缀,因此可以简单地加载 "z" 库。
local zlib = ffi.load(ffi.os == "Windows" and "zlib1" or "z")

local function compress(txt)
    -- 首先,通过使用未压缩字符串的长度来调用 zlib.compressBoud 来获取
    -- 压缩缓存区的最大大小.
    local n = zlib.compressBound(#txt)
    -- 分配这个 n 大小的字节缓存区,类型规范中的 [?] 表示可变长度数组(VLA).
    -- 该数组的实际元素个数由 ffi.new 的第二个参数给出.
    local buf = ffi.new("uint8_t[?]", n)
    -- 看上面 compress2 的函数声明可知,destLen 被定义为一个指针。这是因为
    -- 传入的是最大缓存区的大小并返回实际使用的长度.
    -- 在 C 中可以通过传入一个本地变量的地址 (即 &buflen),但是在 Lua 中没有
    -- 地址操作,因此传入的是只有一个元素的数组。
    local buflen = ffi.new("unsigned long[1]", n)
    local res = zlib.compress2(buf, buflen, txt, #txt, 9)
    assert(res == 0)
    -- 将压缩数据作为 Lua 字符串返回,因此使用 ffi.string(),它需要指向
    -- 数据开头和实际长度的指针,这个长度已经通过 buflen 数组返回了
    return ffi.string(buf, buflen[0])
end

local function uncompress(comp, n)
    local buf = ffi.new("uint8_t[?]", n)
    local buflen = ffi.new("unsigned long[1]", n)
    local res = zlib.uncompress(buf, buflen, comp, #comp)
    assert(res == 0)
    return ffi.string(buf, buflen[0])
end

-- Simple test code.
local txt = string.rep("abcd", 1000)
print("Uncompressed size: ", #txt)
local c = compress(txt)
print("Compressed size: ", #c)
local txt2 = uncompress(c, #txt)
assert(txt2 == txt)

为 C Type 定义 Metamethods

local ffi = require("ffi")
ffi.cdef[[
typedef struct { double x, y; } point_t;
]]

local point 
local mt = {
    __add = function(a, b) return point(a.x+b.x, a.y+b.y) end,
    __len = function(a) return math.sqrt(a.x*a.x + a.y*a.y) end,
    __index = {
        area = function(a) return a.x*a.x + a.y*a.y end,
    },
}
point = ffi.metatype("point_t", mt)

local a = point(3, 4)
print(a.x, a.y) --> 3  4
print(#a)       --> 5
print(a:area()) -- 25
local b = a + point(0.5, 8)
print(#b)       --> 12.5

C 和 LuaJIT 相互转化

如下列表显示了如何将常见的 C 语言转化为 LuaJIT FFI:

缓存或不缓存

将库函数缓存在 local 变量或 upvalues 中是一种常见的用法,如下示例

local byte, char = string.byte, string.char
local function foo(x)
    return char(byte(x) + 1)
end

这个可以通过(更快的)直接使用 local 变量或 upvalue 来替换多次哈希表查找。这对于 LuaJIT 来说不是那么重要,因为 JIT 编译器大量优化哈希表查找,甚至能将大部分内容从内循环中提升出来。但是它并不能消除所有这些。

通过 FFI 库调用 C 函数有一点不同。JIT 编译器有特殊的逻辑来消除从 C 库命名空间中解析的函数的所有查找开销。因此,缓存单个 C 函数是没有用的,实际上是适得其反:

local funca, funcb = ffi.funca, ffi.C.funcb -- Not helpful
local function foo(x, n)
    for i = 1, n do funcb(funca(x, i), 1) end
end

这会将它们变成间接调用,并生成更大更慢的机器代码。相反,需要缓存的是命令空间本身并依赖 JIT 编译器来消除查找:

local C = ffi.C         -- Instead use this
local function foo(x, n)
    for i = 1, n do C.funcb(C.funca(x, i), 1) end
end

这会生成更短更快的代码。因此不要缓存 C 函数,但要缓存命名空间。大多数情况下,命名空间已经位于外部作用域的本地变量中。如来自 local lib = ffi.load(...)。注意,不需要将其复制到函数范围的本地变量中。

2. ffi.* API

词汇表

  • cdecl:抽象 C 类型声明(Lua 字符串)。
  • ctype:C 类型对象。由 ffi.typeof() 返回的一种特殊的 cdata,当被调用时是作为 cdata 的构造函数。
  • ct:一种类型规范,可用于大多数 API 函数。cdecl,ctype 或 cdata 作为模板类型。
  • cb:一个回调对象。这是一个包含特殊函数指针的 C 数据对象。从 C 代码调用此函数会运行关联的 Lua 函数。
  • VLA:通过 [?] 代替元素个数值声明的一个可变长度数组,如 "int[?]"。当创建的时候必须给出元素个数。
  • VLS:可变长度结构体是一个 C 类型的结构体,最后一个元素是 VLA。适用于声明和创建的相同规则。

2.1 声明和访问外部符号

必须首先声明外部符号,然后可以通过索引 C 库命名空间来访问外部符号,该命名空间自动将符号绑定到特定库。

2.1.1 ffi.cdef(def)

声明 C 函数或者 C 的数据结构,数据结构可以是结构体、枚举或者是联合体,函数可以是 C 标准函数,或者第三方库函数,也可以是自定义的函数,注意这里只是函数的声明,并不是函数的定义。声明的函数应该要和原来的函数保持一致。

ffi.cdef[[
typedef struct foo { int a, b; } foo_t;  /* Declare a struct and typedef. */
int dofoo(foo_t *f, int n);              /* Declare an external C function */
]]

注意,外部符号仅被声明,但它们并不受任何特定地址的约束。使用 C 库命名空间实现绑定.此外所有使用的库函数都要对其进行声明。

如何使用自定义的函数?

如下示例,创建一个 myffi.c,内容:

int add(int x, int y)
{
    return x + y;
}

接着在 Linux 下生成动态链接库:

gcc -g -o libmyffi.so -fpic -shared myffi.c

在 LD_LIBRARY_PATH 环境变量中添加生成库的路径:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:your_lib_path

在 Lua 代码中增加如下行:

ffi.load(name, [,global])

ffi.load 会通过给定的 name 加载动态库,返回一个绑定到这个库符号的新的 C 库命名空间,在 POSIX 系统中,如果 global 被设置为 true,这个库符号被加载到一个全局命名空间。另外这个 name 可以是一个动态库的路径,那么会根据路径来查找,否则的话会在默认的搜索路径中去找动态库。在 POSIX 系统中,如果在 name 这个字段中没有写上点符号 .,那么 .so 将会被自动添加进去,例如 ffi.load("z") 会在默认的共享库搜寻路径中去查找 libz.so,在 windows 系统,如果没有包含点号,那么 .dll 会被自动加上。

local ffi = require("ffi")
local myffi = ffi.load("myffi")

ffi.cdef[[
int add(int x, int y); /* don't forget to declare */
]]

local res = myffi.add(1, 2)
print(res)  -- output: 3   Note: please use luajit to run this script.

此外,可以使用 ffi.C (调用 ffi.cdef 中声明的系统函数)来直接调用 add 函数(注:要在 ffi.load 中加上参数 true,如 ffi.load('myffi', true))。

local ffi = require"ffi"
ffi.load('myffi', true)

ffi.cdef[[
int add(int x, int y);   /* don't forget to declare */
]]

local res = ffi.C.add(1, 2)
print(res) -- output: 3   Note: please use luajit to run this script.

2.1.2 ffi.C

这是默认的 C 库命名空间--注意为大写的 C。它绑定到目标系统上的默认符号集或库。这些或多或少与 C 编译器默认提供的相同,而不指定额外的链接库。

在 POSIX 系统中,它绑定到默认或全局命名空间中的符号。这包括可执行文件中的所有导出符号以及加载到全局命名空间中的任意库。这至少包括 libc,libm,libdl(在 Linux 中),libgcc(如果使用 GCC 编译器),以及 LuaJIT 本身提供的 Lua/C API 中的任何导出符号。

2.1.3 clib = ffi.load(name [, global])

这将加载由 name 指定的动态库,并返回一个绑定到其符号的新 C 库命名空间。在 POSIX 系统中,如果 global 为 true,这个库的符号将会加载到全局命名空间中。

如果 name 是路径,该库将会从该路径中加载。否则,name 将以与系统相关的方式进行规范化,并按默认搜索路径来搜索动态库:在 POSIX 系统上,如果 name 不包含 '.',则追加扩展名 .so。此外,如果需要,还会添加库的前缀。所以 ffi.load("z") 在默认的共享库路径中搜索 "libz.so"。

2.2 创建 cdata 对象

2.2.1 ffi.typeof

ctype = ffi.typeof(ct)

创建一个 ctype 对象,会解析一个抽象的 C 类型定义。该函数仅用于解析 cdecl 一次,然后使用生成的 ctype 对象作为构造函数。

local uintptr_t = ffi.typeof("uintptr_t")
local c_str_t = ffi.typeof("const char*")
local int_t = ffi.typeof("int")
local int_array_t = ffi.typeof("int[?]")

2.2.2 ffi.new

如下 API 函数创建 cdata 对象(ctype() 返回 "cdata")。所有创建的对象都是垃圾回收的。

cdata = ffi.new(ct [,nelem] [,init...])
cdata = ctype([nelem,] [init...])

ffi.new 开辟空间,第一个参数为 ctype 对象,ctype 对象最好通过 ctype = ffi.typeof(ct) 构建。

ffi.new 和 ffi.C.malloc 的区别?

如果使用 ffi.new 分配的 cdata 对象指向的内存块是由垃圾回收器 LuaJIT GC 自动管理的,所有不需要用户去释放内存。

如果使用 ffi.C.malloc 分配的空间便不再使用 LuaJIT 自己的分配器了,所以不是由 LuaJIT GC 来管理的,但是,要注意的是 ffi.C.malloc 返回的指针本身所对应的 cdata 对象还是由 LuaJIT GC 来管理的,也就是这个指针的 cdata 对象指向的是用 ffi.C.malloc 分配的内存空间。这个时候,你应该通过 ffi.gc() 函数在这个指针的 cdata 对象上面注册自己的析构函数,这个析构函数里面可以再调用 ffi.C.free,这样的话当 C 指针所对应的 cdata 对象被 LuaJIT GC 管理器垃圾回收的时候,也会自动调用你注册的那个析构函数来执行 C 级别的内存释放。

请尽可能使用最新版本的 LuaJIT,x86_64 上由 LuaJIT GC 管理的内存已经由 1G->2G,虽然管理的内存变大了,但是如果要使用很大的内存,还是用 ffi.C.malloc 来分配会比较好,避免耗尽了 LuaJIT GC 管理内存的上限。

local int_array_t = ffi.typeof("int[?]")
local bucket_v = ffi.new(int_array_t, bucket_sz)

local queue_arr_type = ffi.typeof("lrucache_pureffi_queue_t[?]")
local q = ffi.new(queue_arr_type, size + 1)

2.2.3 ffi.cast

cdata = ffi.cast(ct, init)

创建一个 scalar cdata 对象。

local c_str_t = ffi.typeof("const char*")
local c_str = ffi.case(c_str_t, str)  -- 转换为指针地址

local uintptr_t ffi.typeof("uintptr_t")
tonumber(ffi.cast(uintptr_t, c_str)   -- 转换为数字

2.2.4 ffi.metatype

ctype = ffi.metatype(ct, metatable)

为给定的 ct 创建一个 ctype 对象,并将其与 metatable 相关联。仅允许使用 struct/union 类型,复数和向量。如果需要,其他类型可以封装在 struct 中。

与 metatable 的关联是永久性的,之后不可更改。之后,metatable 的内容和 __index 表(如果有的话)的内容都不能被修改。无论对象如何创建或源自何处,相关地元表都会自动应用于此类型的所有用途。注意,对类型的预定义操作具有优先权(如,声明的字段名称不能被覆盖)。

posted @ 2018-08-04 10:08  季末的天堂  阅读(5813)  评论(0编辑  收藏  举报