nature

nature is api gateway, just show you for how to bulid api gateway

View on GitHub

FFI

什么是 FFI?

FFI 全称是 Foreign Function Interface,是一个可以在某种计算机语言中调用其它语言的接口。

由于现实中很多应用程序是由不同编程语言开发的,必然会涉及到 跨语言调用,比如 A 语言写的函数如果想在 B 语言里面调用,这时一般有两种解决方案:

前者需要至少两个独立的进程才能实现,而后者直接将其它语言的接口内嵌到本语言中,所以调用效率比前者高。

FFI 库与 Lua 扩展 C 库

FFI 库词汇

noun Explanation
cdecl A definition of an abstract C type(actually, is a lua string)
ctype C type object
cdata C data object
ct C type format, is a template object, may be cdecl, cdata, ctype
cb callback object
VLA An array of variable length
VLS A structure of variable length

ffi.* API

功能: Lua ffi 库的 API,与 LuaJIT 不可分割。

毫无疑问,在 lua 文件中使用 ffi 库的时候,必须 要有下面的一行。

local ffi = require("ffi")

cdata 对象的垃圾回收

所有由显式的 ffi.new()ffi.cast() etc. 或者隐式的 accessors 所创建的 cdata 对象都是能被垃圾回收的,当他们被使用的时候,你需要确保有在 Lua stackupvalue 或者 Lua table 上保留有对 cdata 对象的有效引用,一旦最后一个 cdata 对象的有效引用失效了,那么垃圾回收器将自动释放内存(在下一个 GC 周期结束时候)。另外如果你要分配一个 cdata 数组给一个指针的话,你必须保持持有这个数据的 cdata 对象活跃,下面给出一个官方的示例:

ffi.cdef[[
typedef struct { int *a; } foo_t;
]]

local s = ffi.new("foo_t", ffi.new("int[10]")) -- WRONG!

local a = ffi.new("int[10]") -- OK
local s = ffi.new("foo_t", a)
-- Now do something with 's', but keep 'a' alive until you're done.

相信看完上面的 API 你已经很累了,再坚持一下吧!休息几分钟后,让我们来看看下面对官方文档中的示例做剖析,希望能再加深你对 ffi 的理解。

调用 C 函数

真的很用容易去调用一个外部 C 库函数,示例代码:

local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);    -- 标准 C 语法,添加 printf() 函数声明。
]]
ffi.C.printf("Hello %s!", "world")   -- 调用声明的 printf() 函数

以上操作步骤,如下:

事实上,背后的实现远非如此简单:第 3 步使用标准 C 库的命名空间 ffi.C。通过符号名 printf 索引这个命名空间,自动绑定标准 C 库。索引结果是一个特殊类型的对象,当被调用时,执行 printf 函数。传递给这个函数的参数,从 Lua 对象自动转换为相应的 C 类型。

再来一个源自官方的示例代码:

local ffi = require("ffi")
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);
]]
local zlib = ffi.load(ffi.os == "Windows" and "zlib1" or "z")

local function compress(txt)
  local n      = zlib.compressBound(#txt)
  local buf    = ffi.new("uint8_t[?]", n)
  local buflen = ffi.new("unsigned long[1]", n)
  local res    = zlib.compress2(buf, buflen, txt, #txt, 9)
  assert(res == 0)
  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 数据结构

cdata 类型用来将任意 C 数据保存在 Lua 变量中。这个类型相当于一块原生的内存,除了赋值和相同性判断,Lua 没有为之预定义任何操作。然而,通过使用 metatable(元表),程序员可以为 cdata 自定义一组操作。

cdata 不能在 Lua 中创建出来,也不能在 Lua 中修改。这样的操作只能通过 C API。这一点保证了宿主程序完全掌管其中的数据。

我们将 C 语言类型与 metamethod(元方法)关联起来,这个操作 只用做一次ffi.metatype 会返回一个该类型的构造函数。原始 C 类型也可以被用来创建数组,元方法会被自动地应用到每个元素。

尤其需要指出的是metatable 与 C 类型的 关联是永久的,而且 不允许被修改__index 元方法也是。

下面是一个使用 C 数据结构的实例:

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

附表:Lua 与 C 语言语法对应关系

Idiom C code Lua code
Pointer dereference x = *p x = p[0]
int *p *p = y p[0] = y
Pointer indexing x = p[i] x = p[i]
int i, *p p[i+1] = y p[i+1] = y
Array indexing x = a[i] x = a[i]
int i, a[] a[i+1] = y a[i+1] = y
struct/union dereference x = s.field x = s.field
struct foo s s.field = y s.field = y
struct/union pointer deref x = sp->field x = sp.field
struct foo *sp sp->field = y s.field = y
int i, *p y = p - i y = p - i
Pointer dereference x = p1 - p2 x = p1 - p2
Array element pointer x = &a[i] x = a + i

小心内存泄漏

所谓“能力越大,责任越大”,FFI 库在允许我们调用 C 函数的同时,也把内存管理的重担压到我们的肩上。 还好 FFI 库提供了很好用的 ffi.gc 方法。 该方法允许给 cdata 对象注册在 GC 时调用的回调,它能让你在 Lua 领域里完成 C 手工释放资源的事。

C++ 提倡用一种叫 RAII 的方式管理你的资源。简单地说,就是 创建对象时获取,销毁对象时释放。 我们可以在 LuaJIT 的 FFI 里借鉴同样的做法,在调用 resource = ffi.C.xx_create 等申请资源的函数之后,立即补上一行 ffi.gc(resource, ...) 来注册释放资源的函数。

尽量避免尝试手动释放资源!即使不考虑 error 对执行路径的影响,在每个出口都补上一模一样的逻辑会够你受的(用 goto 也差不多,只是稍稍好一点)。

有些时候,ffi.C.xx_create 返回的不是具体的 cdata,而是整型的 handle。这会儿需要用 ffi.metatypeffi.gc 包装一下:

local resource_type = ffi.metatype("struct {int handle;}", {
    __gc = free_resource
})

local function free_resource(handle)
    ...
end

resource = ffi.new(resource_type)
resource.handle = ffi.C.xx_create()

注意: 如果你 没能把申请资源和释放资源的步骤放一起,那么 内存泄露 多半会在前方等你。写代码的时候切记这一点。

lua 语言目录

总目录