【LuaJIT & FFI】优雅的与C交互

文章介绍了LuaJIT的Just-In-Time编译器如何提升性能,并详细讲解了FFI库如何让Lua直接调用C函数和数据结构,简化了Lua与C的交互。同时,提到了cdata对象的垃圾回收机制以及避免内存泄漏的重要性。
摘要由CSDN通过智能技术生成

前言

LuaJIT和FFI是两个非常强大的工具,它们可以帮助开发人员在Lua中实现高性能的代码。

LuaJIT

LuaJIT is a Just-In-Time Compilerfor the Lua programming language。

LuaJIT 的运行时环境包括一个用手写汇编实现的 Lua 解释器和一个可以直接生成机器代码的 JIT 编译器。

一开始的时候,Lua 字节码总是被 LuaJIT 的解释器解释执行。LuaJIT 的解释器会在执行字节码时同时记录一些运行时的统计信息,比如每个 Lua 函数调用入口的实际运行次数,还有每个 Lua 循环的实际执行次数。当这些次数超过某个预设的阈值时,便认为对应的 Lua 函数入口或者对应的 Lua 循环足够的“热”,这时便会触发 JIT 编译器开始工作。

JIT 编译器会从热函数的入口或者热循环的某个位置开始尝试编译对应的 Lua 代码路径。编译的过程是把 LuaJIT 字节码先转换成 LuaJIT 自己定义的中间码(IR),然后再生成针对目标体系结构的机器码。

如果当前 Lua 代码路径上的所有的操作都可以被 JIT 编译器顺利编译,则这条编译过的代码路径便被称为一个“trace”,在物理上对应一个 trace 类型的 GC 对象(即参与 Lua GC 的对象)。

优点:

  1. 更快的执行速度

LuaJIT使用Just-In-Time(JIT)编译器将Lua代码编译成本地机器码,这使得执行速度比传统的解释型语言更快。与其他语言的JIT编译器相比,LuaJIT的执行速度非常快,甚至可以与C语言相媲美。

  1. 更好的内存管理

LuaJIT具有更好的内存管理功能,可以有效地处理大量的数据结构和对象。它使用了一种称为“增量式垃圾回收”的技术,可以在不中断程序执行的情况下回收内存。

  1. 更好的调试工具

LuaJIT提供了一些非常有用的调试工具,包括源代码跟踪、分析堆栈、查看变量和函数等。这些工具可以帮助开发人员快速定位和修复代码中的错误。

  1. 更好的兼容性

LuaJIT与标准的Lua解释器兼容,因此可以使用现有的Lua代码库。此外,LuaJIT还支持大多数Lua扩展库,包括luasocket、LuaXML和LuaFileSystem等。

  1. 更好的可移植性

LuaJIT可以在多个平台上运行,包括Windows、Linux、Mac OS X和Android等。这使得它成为一个非常灵活和可移植的工具,可以满足各种不同的应用程序需求。


FFI

FFI(Foreign Function Interface)是一种Lua扩展,它允许Lua代码直接调用C语言库中的函数和访问数据结构,从而提高了程序的性能和灵活性。

FFI 库,是 LuaJIT 中最重要的一个扩展库。它允许从纯 Lua 代码调用外部 C 函数,使用 C 数据结构。有了它,就不用再像 Lua 标准 math 库一样,编写 Lua 扩展库。把开发者从开发 Lua 扩展 C 库(语言/功能绑定库)的繁重工作中释放出来。

简单解释一下 Lua 扩展 C 库,对于那些能够被 Lua 调用的 C 函数来说,它的接口必须遵循 Lua 要求的形式,就是 typedef int (*lua_CFunction)(lua_State* L),这个函数包含的参数是 lua_State 类型的指针 L 。可以通过这个指针进一步获取通过 Lua 代码传入的参数。这个函数的返回值类型是一个整型,表示返回值的数量。需要注意的是,用 C 编写的函数无法把返回值返回给 Lua 代码,而是通过虚拟栈来传递 Lua 和 C 之间的调用参数和返回值。不仅在编程上开发效率变低,而且性能上比不上 FFI 库调用 C 函数。

FFI 库最大限度的省去了使用 C 手工编写繁重的 Lua/C 绑定的需要。不需要学习一门独立/额外的绑定语言——它解析普通 C 声明。这样可以从 C 头文件或参考手册中,直接剪切,粘贴。它的任务就是绑定很大的库,但不需要捣鼓脆弱的绑定生成器。

FFI 紧紧的整合进了 LuaJIT(几乎不可能作为一个独立的模块)。JIT 编译器在 C 数据结构上所产生的代码,等同于一个 C 编译器应该生产的代码。在 JIT 编译过的代码中,调用 C 函数,可以被内连处理,不同于基于 Lua/C API 函数调用。


ffi 库 词汇
nounExplanation
cdeclA definition of an abstract C type(actually, is a lua string)
ctypeC type object
cdataC data object
ctC type format, is a template object, may be cdecl, cdata, ctype
cbcallback object
VLAAn array of variable length
VLSA structure of variable length

ffi.* API

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

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

local ffi = require "ffi"
ffi.cdef

语法ffi.cdef(def)

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

ffi.cdef[[
typedef struct foo { int a, b; } foo_t;  /* Declare a struct and typedef.   */
int printf(const char *fmt, ...);        /* Declare a typical printf function. */
]]

注意: 所有使用的库函数都要对其进行声明,这和我们写 C 语言时候引入 .h 头文件是一样的。


并不是所有的 C 标准函数都能满足我们的需求,使用 第三方库函数 或 自定义的函数,如下示例:

main.lua

local ffi = require "ffi"
local C = ffi.C

ffi.cdef[[
    int printf(const char *fmt, ...);
    int find_max(int *arr, int len);
    
    typedef struct {int x, y;} Point;
    Point* newpoint(int x, int y);

    int gcd(int a, int b);
]]

-- ffi.C是FFI库提供的一个特殊表,它包含了C标准库中的所有函数和常量。
-- 但并不包含动态链接库定义的其他函数,如果要使用ffi.C来调用自定义函数
-- 需要在load(, [,global])参数置为true即可。
ffi.C.printf("Hello, World\n")

local lib = ffi.load("./mylib.so", true);
local arr = ffi.new("int[?]", 5, {1, 3, 4, 2, 0})
local res = lib.find_max(arr, 5)
print(res)

local p = ffi.cast("Point*", lib.newpoint(1, 2))
print(p.x, p.y)

local res = C.gcd(4, 6)
print(res)

mylib.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
        int x, y;
} Point;

Point* newpoint(int x, int y) {
        Point *p = (Point *)malloc(sizeof(Point));
        p->x = x;
        p->y = y;
        return p;
}

int find_max(int *arr, int len) {
        int max = arr[0];
        for (int i = 1; i < len; i++)
                if (max < arr[i])
                        max = arr[i];
        return max;
}

int gcd(int a, int b) {
    return b ? gcd(b, a % b) : a;
}

makefile

CC=gcc
CFLAGS=-Wall -g
LDFLAGS=-shared -fPIC

all: mylib.so

mylib.so: mylib.c
        $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $<

clean:
        rm -f mylib.so

.PHONY: all clean

在 Lua 代码中:

ffi.load(name [,global])

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


ffi.typeof

语法ctype = ffi.typeof(ct)

功能: 创建一个 ctype 对象,会解析一个抽象的 C 类型定义。

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[?]")
local ffi = require "ffi"
local C = ffi.C

ffi.cdef [[
]]

local int_t = ffi.typeof("int")
print(int_t) -- ctype<int>
local n = int_t(1)
print(n) -- cdata<int>: 0x40c13d50
print(tonumber(n)) -- 1
print(string.format("%p", n)) -- 0x40c13d50

ffi.new

语法cdata = ffi.new(ct [,nelem] [,init...])
功能: 开辟空间,第一个参数为 ctype 对象,ctype 对象最好通过 ctype = ffi.typeof(ct) 构建。

ffi.newffi.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() 函数在这个 C 指针的 cdata 对象上面注册自己的析构函数,这个析构函数里面你可以再调用 ffi.C.free,这样的话当 C 指针所对应的 cdata 对象被 Luajit GC管理器垃圾回收时候,也会自动调用你注册的那个析构函数来执行 C 级别的内存释放。

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)
local ffi = require "ffi"
local C = ffi.C

ffi.cdef [[
    size_t strlen(const char *s);
]]

local char_t = ffi.typeof("char[?]")
local name = "cauchy"
local len = C.strlen(name) + 1 -- '\0'
local str = char_t(len)
ffi.copy(str, name, len)
print(ffi.string(str)) -- cauchy

local point_t = ffi.typeof("struct { int x, y; }")
local point = ffi.new(point_t, { x = 1, y = 2})
print(point.x, point.y) -- 1 2

ffi.fill

语法ffi.fill(dst, len [,c])

功能: 填充数据,此函数和 memset(dst, c, len) 类似,注意参数的顺序。

ffi.fill(self.bucket_v, ffi_sizeof(int_t, bucket_sz), 0)
ffi.fill(q, ffi_sizeof(queue_type, size + 1), 0)

ffi.cast

语法cdata = ffi.cast(ct, init)

功能: 创建一个 scalar cdata 对象。

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

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

ffi.metatype

语法ctype = ffi.metatype(ct, metatable)
功能ffi.metatype 会返回一个该类型的构造函数。

local ffi = require "ffi"
local C = ffi.C

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, #a, a:area())

local b = a + point(1, 1)
print(#b)

cdata 类型用来将任意 C 数据保存在 Lua 变量中。这个类型相当于一块原生的内存,除了赋值和相同性判断,Lua 没有为之预定义任何操作。然而,通过使用 metatable(元表),程序员可以为 cdata 自定义一组操作。cdata 不能在 Lua 中创建出来,也不能在 Lua 中修改。这样的操作只能通过 C API。这一点保证了宿主程序完全掌管其中的数据。

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

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


cdata 对象的垃圾回收

所有由显式的 ffi.new(), ffi.cast() etc. 或者隐式的 accessors 所创建的 cdata 对象都是能被垃圾回收的,当他们被使用的时候,你需要确保有在 Lua stack,upvalue,或者 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.
小心内存泄漏

所谓“能力越大,责任越大”,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 与 C 语言语法对应关系

IdiomC codeLua code
Pointer dereferencex = *px = p[0]
int *p*p = yp[0] = y
Pointer indexingx = p[i]x = p[i]
int i, *pp[i+1] = yp[i+1] = y
Array indexingx = a[i]x = a[i]
int i, a[]a[i+1] = ya[i+1] = y
struct/union dereferencex = s.fieldx = s.field
struct foo ss.field = ys.field = y
struct/union pointer derefx = sp->fieldx = sp.field
struct foo *spsp->field = ys.field = y
int i, *py = p - iy = p - i
Pointer dereferencex = p1 - p2x = p1 - p2
Array element pointerx = &a[i]x = a + i

参考:OpenResty最佳实践

### 回答1: 可以使用luajit ffi库的相关函数来读取文件路径下的文件名,以下是一个简单的示例代码: ``` local ffi = require("ffi") -- 定义readdir系统调用函数 ffi.cdef[[ typedef struct dirent { long d_ino; off_t d_off; unsigned short d_reclen; unsigned char d_type; char d_name[256]; } dirent; typedef struct DIR { int fd; dirent ent; } DIR; DIR *opendir(const char *name); dirent *readdir(DIR *dirp); int closedir(DIR *dirp); ]] -- 读取文件路径下的文件名 function list_files(path) local dir = ffi.C.opendir(path) local files = {} if dir ~= nil then repeat local entry = ffi.C.readdir(dir) if entry ~= nil then local name = ffi.string(entry.d_name) if name ~= "." and name ~= ".." then table.insert(files, name) end end until entry == nil ffi.C.closedir(dir) end return files end -- 测试 local files = list_files(".") for i, file in ipairs(files) do print(i, file) end ``` 在这个示例中,我们使用ffi.cdef()函数定义了readdir、opendir和closedir系统调用函数。接下来我们定义了一个list_files()函数,该函数使用opendir()函数打开指定路径下的目录,使用readdir()函数读取目录下的文件列表,并使用closedir()函数关闭目录。最后,我们遍历文件列表,将每个文件的文件名添加到一个数组中,并返回该数组。 要使用这个函数,只需将要读取的文件路径传递给list_files()函数,它将返回一个包含该目录下所有文件名的数组。在本例中,我们传递了"."作为路径,它将返回当前目录下的所有文件名。 ### 回答2: 在LuaJIT中使用ffi库读取文件路径下的文件名相对简单,只需要用到ffi库的C语言接口和Lua的文件操作函数就可以完成。以下是一个可以实现该功能的示例代码: ```lua local ffi = require("ffi") -- 定义C函数readdir实现读取文件路径下的文件名 ffi.cdef[[ typedef struct dirent { char d_name[256]; } dirent; typedef struct DIR DIR; DIR* opendir(const char* path); dirent* readdir(DIR* dirp); int closedir(DIR* dirp); ]] -- 定义读取文件路径下文件名的函数 function getFilenames(path) local dir = ffi.C.opendir(path) -- 打开目录 if dir == nil then return nil end local filenames = {} -- 存储文件名的数组 local entry = ffi.C.readdir(dir) -- 读取第一个文件 while entry ~= nil do local filename = ffi.string(entry.d_name) -- 转换为Lua字符串 if filename ~= "." and filename ~= ".." then table.insert(filenames, filename) -- 加入数组 end entry = ffi.C.readdir(dir) -- 读取下一个文件 end ffi.C.closedir(dir) -- 关闭目录 return filenames end -- 示例使用: local path = "./folder" -- 文件路径 local filenames = getFilenames(path) -- 获取文件名数组 if filenames == nil then print("无法打开目录") else for i, filename in ipairs(filenames) do print(filename) end end ``` 以上示例代码使用LuaJITffi库调用了C语言的函数opendir、readdir和closedir来实现读取文件路径下的文件名。首先,通过调用opendir打开指定路径的目录;然后使用readdir循环读取目录中的所有文件,并将文件名加入到一个Lua数组中;最后,调用closedir关闭目录并返回获取到的文件名数组。示例中也处理了当前目录(".")和上层目录(".. ")两个特殊文件名的情况。 你可以将示例代码保存在一个Lua文件中,并根据自己的实际需求修改文件路径,然后运行该Lua文件,即可看到打印出的文件名列表。 ### 回答3: 使用LuaJITffi库可以很方便地调用C函数,进而实现读取文件路径下的文件名的功能。下面是一个示例代码: ```lua -- 引入ffi库 local ffi = require("ffi") -- 定义C函数 ffi.cdef[[ typedef struct { void *d; } DIR; DIR *opendir(const char *filename); void closedir(DIR *dirp); const char *readdir(DIR *dirp); ]] -- 遍历文件路径下的文件名 function getFilenames(filepath) local dir = ffi.C.opendir(filepath) -- 打开目录 if dir ~= nil then local filenames = {} local dirent = ffi.C.readdir(dir) while dirent ~= nil do local name = ffi.string(dirent) -- 排除当前目录和上级目录 if name ~= '.' and name ~= '..' then table.insert(filenames, name) end dirent = ffi.C.readdir(dir) end ffi.C.closedir(dir) -- 关闭目录 return filenames end return nil end -- 测试 local path = "/path/to/directory" local filenames = getFilenames(path) if filenames ~= nil then for i, filename in ipairs(filenames) do print(filename) end else print("Failed to open directory") end ``` 上述代码首先使用ffi.cdef定义了C的DIR结构和相关函数,其中opendir用于打开目录,readdir用于读取目录中的文件名,closedir用于关闭目录。 然后,在getFilenames函数中,我们首先使用ffi.C.opendir打开指定的文件路径,然后循环使用ffi.C.readdir读取目录中的文件名,将有效的文件名加入到filenames表中,最后使用ffi.C.closedir关闭目录。 最后,在测试的部分,我们指定一个文件路径并调用getFilenames函数获取文件名列表,然后遍历打印每个文件名。如果无法打开目录,会打印"Failed to open directory"。 以上便是使用LuaJITffi库实现读取文件路径下的文件名的代码。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ღCauchyོꦿ࿐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值