文章

Lua FFI

简介

对 libffi 的介绍可以看 [这里],简单来说它就是提供了动态调用任意 C 函数的功能。

FFI库允许调用外部C函数并使用来自纯Lua代码的C数据结构。 FFI库基本上避免了在C中编写繁琐的手动Lua / C绑定的需要。

libffi主要的功能包括两个:

  • 动态调用
  • 动态定义

动态调用,在运行时动态调用一个函数。

例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <ffi.h>

int main()
{
  ffi_cif cif;
  ffi_type *args[1];
  void *values[1];
  char *s;
  ffi_arg rc;

  /* Initialize the argument info vectors */    
  /* 初始化参数信息 */
  args[0] = &ffi_type_pointer;
  values[0] = &s;

  /* Initialize the cif */
  /* 初始化cif */
  if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 1, 
               &ffi_type_sint, args) == FFI_OK)
    {
      s = "Hello World!";
      ffi_call(&cif, puts, &rc, values);
      /* rc now holds the result of the call to puts */

      /* values holds a pointer to the function's arg, so to 
         call puts() again all we need to do is change the 
         value of s */
      s = "This is cool!";
      ffi_call(&cif, puts, &rc, values);
    }

  return 0;
}

例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int testFunc(int m, int n) {
  printf("params: %d %d \n", n, m);
  return n+m;
}
int main() {
    //拿函数指针
    void* functionPtr = dlsym(RTLD_DEFAULT, "testFunc");
    int argCount = 2;
    //按ffi要求组装好参数类型数组
    ffi_type **ffiArgTypes = alloca(sizeof(ffi_type *) *argCount);
    ffiArgTypes[0] = &ffi_type_sint;
    ffiArgTypes[1] = &ffi_type_sint;
    //按ffi要求组装好参数数据数组
    void **ffiArgs = alloca(sizeof(void *) *argCount);
    void *ffiArgPtr = alloca(ffiArgTypes[0]->size);
    int *argPtr = ffiArgPtr;
    *argPtr = 1;
    ffiArgs[0] = ffiArgPtr;
    void *ffiArgPtr2 = alloca(ffiArgTypes[1]->size);
    int *argPtr2 = ffiArgPtr2;
    *argPtr2 = 2;
    ffiArgs[1] = ffiArgPtr2;
    //生成 ffi_cfi 对象,保存函数参数个数/类型等信息,相当于一个函数原型
    ffi_cif cif;
    ffi_type *returnFfiType = &ffi_type_sint;
    ffi_status ffiPrepStatus = ffi_prep_cif_var(&cif, FFI_DEFAULT_ABI, (unsigned int)0, (unsigned int)argCount, returnFfiType, ffiArgTypes);
    if (ffiPrepStatus == FFI_OK) {
        //生成用于保存返回值的内存
        void *returnPtr = NULL;
        if (returnFfiType->size) {
            returnPtr = alloca(returnFfiType->size);
        }
        //根据cif函数原型,函数指针,返回值内存指针,函数参数数据调用这个函数
        ffi_call(&cif, functionPtr, returnPtr, ffiArgs);
        //拿到返回值
        int returnValue = *(int *)returnPtr;
        printf("ret: %d \n", returnValue);
    }
}

上面代码展示,只需要知道

  • 函数指针(puts)
  • 包括参数和返回值信息的函数原型(cif)
  • 调用参数(values)
  • 保存返回值的指针(rc)

就可以在运行时动态地调用一个函数。

动态定义,运行时增加新的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdio.h>
#include <ffi.h>

/* Acts like puts with the file given at time of enclosure. */
/* 新的函数实体 */
void puts_binding(ffi_cif *cif, void *ret, void* args[],
                  void *stream)
{
  *(ffi_arg *)ret = fputs(*(char **)args[0], (FILE *)stream);
}

/* 函数原型 */
typedef int (*puts_t)(char *);

int main()
{
  ffi_cif cif;
  ffi_type *args[1];
  ffi_closure *closure;

  void *bound_puts;
  int rc;

  /* Allocate closure and bound_puts */
  /* 给闭包和bound_puts分配内存 */
  closure = ffi_closure_alloc(sizeof(ffi_closure), &bound_puts);

  if (closure)
    {
      /* Initialize the argument info vectors */
      /* 初始化参数信息 */
      args[0] = &ffi_type_pointer;

      /* Initialize the cif */
      /* 初始化cif */
      if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 1,
                       &ffi_type_sint, args) == FFI_OK)
        {
          /* Initialize the closure, setting stream to stdout */
          /* 初始化闭包,设置stream参数为stdout */
          if (ffi_prep_closure_loc(closure, &cif, puts_binding,
                                   stdout, bound_puts) == FFI_OK)
            {
              rc = ((puts_t)bound_puts)("Hello World!");
              /* rc now holds the result of the call to fputs */
              /* rc 保存了fputs的返回值 */
            }
        }
    }

  /* Deallocate both closure, and bound_puts */
  /* 释放闭包和bound_puts函数指针 */
  ffi_closure_free(closure);

  return 0;
}

这个例子,动态定义了一个bound_puts函数指针,并绑定到puts_binding函数实体上。

动态定义时使用的api是ffi_prep_closure_loc函数,需要准备好closure,函数原型(cif),函数实体(puts_binding),透传的userInfo(stdout),函数指针(bound_puts)

closure是ffi_closure类型对象,用于把其它各个参数关联到一起。

入门 “Hello world”

调用外部C函数

1
2
3
4
5
local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("Hello %s!\n", "world")

使用C数据结构

FFI库允许创建和访问C数据结构

简单Lua版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
--cdata_plain.lua
local floor = math.floor

local function image_ramp_green(n)
  local img = {}
  local f = 255/(n-1)
  for i=1,n do
    img[i] = { red = 0, green = floor((i-1)*f), blue = 0, alpha = 255 }
  end
  return img
end

local function image_to_grey(img, n)
  for i=1,n do
    local y = floor(0.3*img[i].red + 0.59*img[i].green + 0.11*img[i].blue)
    img[i].red = y; img[i].green = y; img[i].blue = y
  end
end

local N = 400*400
local img = image_ramp_green(N)
for i=1,1000 do
  image_to_grey(img, N)
end

运行

1
2
3
4
5
time ./luajit cdata_plain.lua 

real    0m4.570s
user    0m4.547s
sys     0m0.020s

FFI版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
--cdata_ffi.lua
local ffi = require("ffi")
ffi.cdef[[
typedef struct { uint8_t red, green, blue, alpha; } rgba_pixel;
]]

local function image_ramp_green(n)
  local img = ffi.new("rgba_pixel[?]", n)
  local f = 255/(n-1)
  for i=0,n-1 do
    img[i].green = i*f
    img[i].alpha = 255
  end
  return img
end

local function image_to_grey(img, n)
  for i=0,n-1 do
    local y = 0.3*img[i].red + 0.59*img[i].green + 0.11*img[i].blue
    img[i].red = y; img[i].green = y; img[i].blue = y
  end
end

local N = 400*400
local img = image_ramp_green(N)
for i=1,1000 do
  image_to_grey(img, N)
end

运行

1
2
3
4
5
time ./luajit cdata_ffi.lua 

real    0m0.576s
user    0m0.575s
sys     0m0.001s

性能对比

1
4.570/0.576 = 7.93403

FFI版本的耗时只有简单Lua版本的 1/8

进阶 常用的操作

IdiomC codeLua code
Pointer dereferencex = *p;x = p[0]
int *p;*p = y;p[0] = y
Pointer indexingx = p[i];x = p[i]
int i, *p;p[i+1] = y;p[i+1] = y
Array indexingx = a[i];x = a[i]
int i, a[];a[i+1] = y;a[i+1] = y
struct/union dereferencex = s.field;x = s.field
struct foo s;s.field = y;s.field = y
struct/union pointer deref.x = sp->field;x = s.field
struct foo *sp;sp->field = y;s.field = y
Pointer arithmeticx = p + i;x = p + i
int i, *p;y = p - i;y = p - i
Pointer difference
int p1, p2;
x = p1 - p2;x = p1 - p2
Array element pointer
int i, a[];
x = &a[i];x = a+i
Cast pointer to address
int *p;
x = (intptr_t)p;x = tonumber(ffi.cast(“intptr_t”,p))
Functions with outargs
void foo(int *inoutlen);
int len = x;
foo(&len);
y = len;
local len =ffi.new(“int[1]”, x)
foo(len)
y = len[0]
Vararg conversions
int printf(char *fmt, …);
printf(“%g”, 1.0);
printf(“%d”, 1);
printf(“%g”, 1);
printf(“%d”,ffi.new(“int”, 1))

缓存或者不缓存

普通Lua

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

这将用(更快)直接使用 localvalue 或 upvalue 来替换几个哈希表查找。

FFI

这种情况与通过FFI库的C函数调用有点不同。缓存单个C函数没有帮助,实际上是反效果

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

应该缓存命名空间

1
2
3
4
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
本文由作者按照 CC BY 4.0 进行授权