其他分享
首页 > 其他分享> > luajit开发文档中文版(二)LuaJIT扩展

luajit开发文档中文版(二)LuaJIT扩展

作者:互联网

2022年6月10日15:33:04

 

LuaJIT 完全向上兼容 Lua 5.1。它支持所有 标准 Lua 库函数和全套 Lua/C API 函数

LuaJIT 在链接器/动态加载器级别也与 Lua 5.1 完全 ABI 兼容。这意味着您可以针对标准 Lua 头文件编译 C 模块并从 Lua 或 LuaJIT 加载相同的共享库。

LuaJIT 使用新功能扩展了标准 Lua VM,并添加了几个扩展模块。请注意,此页面仅涉及 功能增强,而不是性能增强,例如优化的 VM、更快的解释器或 JIT 编译器。

扩展模块

LuaJIT 带有几个内置的扩展模块:

bit.* - 按位运算

LuaJIT 支持Lua BitOp 定义的所有按位运算 :

bit.tobit bit.tohex bit.bnot bit.band bit.bor bit.bxor
bit.lshift bit.rshift bit.arshift bit.rol bit.ror bit.bswap

这个模块是 LuaJIT 内置的——你不需要下载或安装 Lua BitOp。Lua BitOp 站点包含所有 Lua BitOp API 函数的完整文档。

请确保在使用其任何功能之前 需要该模块:

local bit = require("bit")

LuaJIT 会忽略已安装的 Lua BitOp 模块。通过这种方式,您可以在共享安装中同时使用 Lua 和 LuaJIT 的位操作。

ffi.* — FFI 库

FFI 库 允许从纯 Lua 代码调用外部 C 函数和使用 C 数据结构。

jit.* — JIT 编译器控制

此模块中的函数 控制 JIT 编译器引擎的行为

C API 扩展

LuaJIT 为 Lua/C API 添加了一些额外的功能

增强的标准库函数

xpcall(f, err [,args...])传递参数

与 Lua 5.1 中的标准实现不同,xpcall() 将错误函数之后的任何参数传递给在受保护上下文中调用的函数。

loadfile()等处理 UTF-8 源代码

Lua 源代码解析器透明地处理非 ASCII 字符。这允许在标识符和字符串中使用 UTF-8 字符。UTF-8 BOM 在源代码的开头被跳过。

tostring()等规范化 NaN 和 ±Inf

所有数字到字符串的转换在所有平台上始终将非有限数字转换为相同的字符串。NaN 产生"nan",正无穷大产生"inf",负无穷大产生"-inf"

tonumber()等使用内置字符串到数字的转换

在所有平台上,所有字符串到数字的转换都一致地将整数和浮点输入转换为十进制和十六进制。 strtod()不再使用,这避免了许多与糟糕的 C 库实现有关的问题。内置转换函数根据 IEEE-754 标准提供全精度,它独立于当前语言环境工作,并且支持十六进制浮点数(例如0x1.5p-3)。

string.dump(f [,strip])生成可移植字节码

string.dump() 添加了一个额外的参数。如果设置为 true,则生成没有调试信息的“剥离”字节码。这加快了以后的字节码加载并减少了内存使用。另请参见 -b命令行选项

生成的字节码是可移植的,可以加载到 LuaJIT 支持的任何架构上,与字长或字节序无关。但是字节码兼容版本必须匹配。字节码与 dot 版本 (xy0 → xy1) 保持兼容,但可能会随着主要或次要版本 (2.0 → 2.1) 或任何 beta 版本之间发生变化。外部字节码(例如来自 Lua 5.1)不兼容,无法加载。

math.random()的增强 PRNG

LuaJIT 使用周期为 2^223 的 Tausworthe PRNG 来实现 math.random()math.randomseed()。与使用特定平台的 ANSI rand() 的标准 Lua 实现相比,PRNG 结果的质量要好得多。

PRNG 从所有平台上的相同种子生成相同的序列,并利用种子参数中的所有位。 不带参数的math.random()为每次调用生成 52 个伪随机位。结果均匀分布在 0.0 和 1.0 之间。对于math.random(n [,m]) ,它已正确按比例放大和四舍五入以保持一致性。

重要提示:这个和任何其他基于简单 math.random() API 的 PRNG 都不适合加密使用。

io.*函数处理 64 位文件偏移量

标准io.*库 中的文件 I/O 函数处理 64 位文件偏移。特别是这意味着可以打开大于 2 GB 的文件并重新定位或获取超过 2 GB 的偏移量的当前文件位置(fp:seek()方法)。

debug.*函数识别元方法

debug.getinfo()lua_getinfo()还返回有关调用的元方法的信息。namewhat字段设置为 “metamethod”  name字段具有相应元方法的名称(例如“__index”)。

完全可恢复的虚拟机

LuaJIT VM 是完全可恢复的。这意味着您甚至可以跨上下文从协程中产生,而这在标准 Lua 5.1 VM 中是不可能的:例如,您可以跨pcall() 和xpcall()、跨迭代器和跨元方法产生。

Lua 5.2 的扩展

LuaJIT 支持 Lua 5.2 中的一些语言和库扩展。无条件启用不太可能破坏现有代码的功能:

仅当 LuaJIT 是使用-DLUAJIT_ENABLE_LUA52COMPAT 构建时,才会启用其他功能 :

注意:这仅在语言和 Lua 库级别提供与 Lua 5.2 的部分兼容性。LuaJIT 与 Lua 5.1 是 API+ABI 兼容的,这可以防止实现会破坏 Lua/C API 和 ABI 的功能(例如_ENV)。

C++ 异常互操作性

LuaJIT 具有与 C++ 异常互操作的内置支持。可用的功能范围取决于目标平台和用于编译 LuaJIT 的工具链:

平台 编译器 互操作性
POSIX/x64,DWARF2 展开 海合会 4.3+ 满的
其他平台,DWARF2 展开 海合会 有限的
视窗/x64 MSVC 满的
视窗/x86 任何
其他平台 其他编译器

完全互操作性意味着:

有限的互操作性意味着:

没有互操作性意味着:

 

FFI Library

FFI 库允许调用外部 C 函数并 使用纯 Lua 代码中的 C 数据结构

FFI 库在很大程度上消除了用 C 编写繁琐的手动 Lua/C 绑定的需要。无需学习单独的绑定语言——它解析简单的 C 声明!这些可以从 C 头文件或参考手册中剪切粘贴。它可以完成绑定大型库的任务,而无需处理脆弱的绑定生成器。

FFI 库紧密集成到 LuaJIT 中(它不能作为单独的模块使用)。JIT 编译器生成的用于从 Lua 代码访问 C 数据结构的代码与 C 编译器生成的代码相当。与通过经典 Lua/C API 绑定的函数的调用不同,可以在 JIT 编译的代码中内联对 C 函数的调用。

本页简要介绍了 FFI 库的使用。 请使用导航栏中的 FFI 子主题了解更多信息。

激励示例:调用外部 C 函数

调用外部 C 库函数真的很容易:

local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("Hello %s!", "world")

所以,让我们把它分开:

①加载 FFI 库。

②为函数添加 C 声明。双括号内的部分(绿色)只是标准的 C 语法。

③调用命名的 C 函数——没错,就是这么简单!

实际上,幕后发生的事情远非简单:③利用标准 C 库命名空间ffi.C使用符号名称 ( "printf" )索引此命名空间会自动将其绑定到标准 C 库。结果是一种特殊的对象,当它被调用时,它会运行printf函数。传递给此函数的参数会自动从 Lua 对象转换为相应的 C 类型。

好的,所以printf()的使用可能不是一个特别的例子。您也可以使用io.write()和 string.format()来做到这一点。但你明白了...

所以这里有一些东西可以在 Windows 上弹出一个消息框:

local ffi = require("ffi")
ffi.cdef[[
int MessageBoxA(void *w, const char *txt, const char *cap, int type);
]]
ffi.C.MessageBoxA(nil, "Hello world!", "Test", 0)

冰!再说一次,这太容易了,不是吗?

将此与使用经典 Lua/C API 绑定该函数所需的工作进行比较:创建一个额外的 C 文件,添加一个 C 函数来检索和检查从 Lua 传递的参数类型并调用实际的 C 函数,添加一个模块列表函数及其名称,添加luaopen_*函数并注册所有模块函数,编译并将其链接到共享库(DLL)中,将其移动到正确的路径,添加加载模块 aaaand 的 Lua 代码......最后调用绑定功能。呸!

激励示例:使用 C 数据结构

FFI 库允许您创建和访问 C 数据结构。当然,它的主要用途是与 C 函数交互。但它们也可以单独使用。

Lua 建立在高级数据类型之上。它们是灵活的、可扩展的和动态的。这就是为什么我们都非常喜欢 Lua。唉,这对于某些您确实需要低级数据类型的任务可能效率低下。例如,需要用一个包含许多小表的大表来实现一个固定结构的大数组。这会带来大量的内存开销和性能开销。

这是一个对彩色图像和一个简单基准进行操作的库的草图。首先,普通的 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

这将创建一个具有 160.000 像素的表格,每个像素都是一个包含 0-255 范围内的四个数值的表格。首先创建一个带有绿色渐变的图像(为简单起见为 1D),然后将图像转换为 1000 次灰度。是的,这很愚蠢,但我需要一个简单的例子......

这是 FFI 版本。修改部分已用粗体标出:

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

好的,所以这并不太难:

①首先,加载FFI库并声明低级数据类型。这里我们选择一个 包含四个字节字段的结构,一个用于 4x8 位 RGBA 像素的每个组件。

②用ffi.new()创建数据结构很简单—— '?' 是可变长度数组元素数量的占位符。

③ C 数组是从零开始的,因此索引必须从0到 n-1。人们可能希望再分配一个元素来简化转换遗留代码。

④由于ffi.new() 默认对数组进行零填充,所以我们只需要设置 green 和 alpha 字段即可。

⑤此处可以省略对math.floor()的调用 ,因为浮点数在转换为整数时已经被截断为零。当数字存储在每个像素的字段中时,这会隐式发生。

现在让我们看看更改的影响:首先,图像的内存消耗从 22 MB 下降到 640 KB(400*400*4 字节)。这是 35 倍以下的因素!所以,是的,表确实有明显的开销。顺便说一句:原始程序在普通 Lua 中(在 x64 上)将消耗 40 兆字节。

接下来是性能:纯 Lua 版本在我的机器上运行时间为 9.57 秒(使用 Lua 解释器为 52.9 秒),而 FFI 版本在我的机器上运行时间为 0.48 秒(YMMV)。这比 Lua 解释器快 20 倍(比 Lua 解释器快 110 倍)。

狂热的读者可能会注意到,将纯 Lua 版本转换为使用颜色的数组索引([1]代替 .red[2]代替.green等)应该更紧凑和更快。这当然是正确的(大约 1.7 倍)。切换到数组结构也会有所帮助。

但是,生成的代码将不那么惯用,而且更容易出错。而且它仍然没有接近 FFI 版本代码的性能。此外,高级数据结构不能轻易地传递给其他 C 函数,尤其是 I/O 函数,而没有过度的转换惩罚。

 

FFI 教程

本页旨在通过介绍一些用例和指南,让您大致了解 FFI 库的功能。

不过,此页面并未尝试解释所有 FFI 库。您需要查看ffi.* API 函数参考FFI 语义以了解更多信息。

加载 FFI 库

FFI 库默认内置在 LuaJIT 中,但默认不加载和初始化。使用 FFI 库的建议方法是将以下内容添加到每个需要其功能之一的 Lua 文件的开头:

local ffi = require("ffi")

请注意,这并没有在全局变量表中定义ffi变量——您确实需要使用局部变量。require函数确保库 只加载一次。

注意:如果您想从命令行可执行文件的交互式提示中试验 FFI,请省略local,因为它不会跨行保留局部变量。

访问标准系统函数

以下代码解释了如何访问标准系统函数。我们通过在每个点之后休眠 10 毫秒来慢慢打印两行点:

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")

这是分步说明:

①这定义了我们将要使用的 C 库函数。双括号内的部分(绿色)只是标准的 C 语法。您通常可以从 C 头文件或每个 C 库或 C 编译器提供的文档中获取此信息。

②我们这里面临的困难,是有不同的标准可供选择。Windows 有一个简单的Sleep()函数。在其他系统上,有多种功能可用于实现亚秒级睡眠,但没有明确的共识。值得庆幸的是poll()也可以用于此任务,并且它存在于大多数非 Windows 系统上。对ffi.os的检查确保我们仅在 Windows 系统上使用 Windows 特定功能。

③这里我们将对 C 函数的调用包装在 Lua 函数中。这不是绝对必要的,但仅在代码的一部分中处理特定于系统的问题会很有帮助。我们包装它的方式确保了对操作系统的检查只在初始化期间完成,而不是每次调用。

④更微妙的一点是,我们将sleep()函数(为了这个例子)定义为接受秒数,但接受小数秒。将其乘以 1000 得到毫秒,但这仍然是一个 Lua 数字,它是一个浮点值。唉, Sleep()函数只接受一个整数值。对我们来说幸运的是,FFI 库在调用函数时自动执行转换(将 FP 值截断为零,就像在 C 中一样)。

有些读者会注意到Sleep()是 KERNEL32.DLL的一部分,也是一个stdcall函数。那么这怎么可能起作用呢?FFI 库提供了ffi.C 默认 C 库命名空间,它允许从默认库集中调用函数,就像 C 编译器一样。此外,FFI 库会自动检测stdcall函数,因此您不需要这样声明它们。

⑤ poll() 函数需要几个我们不会使用的参数。您可以简单地使用nil来传递NULL指针和0 来传递nfds参数。请注意 ,与 C++ 不同,数字0 不会转换为指针值。您确实必须将指向指针参数的指针和数字传递给数字参数。

FFI 语义 页面包含有关Lua 对象和 C 类型之间转换的所有血腥细节 。在大多数情况下,您不必处理它,因为它是自动执行的,并且经过精心设计以弥合 Lua 和 C 之间的语义差异。

⑥现在我们已经定义了自己的sleep()函数,我们可以从普通的 Lua 代码中调用它。那还不错吧?把这些无聊的动画点变成一个引人入胜的畅销游戏留给读者作为练习。:-)

访问 zlib 压缩库

下面的代码展示了如何从 Lua 代码中访问zlib压缩库。我们将定义两个方便的包装函数,它们接受一个字符串并将其压缩或解压缩为另一个字符串:

 
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)

这是分步说明:

①这里定义了zlib提供的一些C函数。为了这个例子,一些类型间接被减少,它使用预定义的固定大小整数类型,同时仍然遵守 zlib API/ABI。

②这会加载 zlib 共享库。在 POSIX 系统上,它被命名为libz.so并且通常是预先安装的。由于ffi.load()自动添加任何缺少的标准前缀/后缀,我们可以简单地加载 “z”库。在 Windows 上,它被命名为zlib1.dll,您必须先从 zlib 站点下载它。检查 ffi.os确保我们将正确的名称传递给 ffi.load()

③首先,以未压缩字符串的长度调用zlib.compressBound函数得到压缩缓冲区的最大大小 。下一行分配这个大小的字节缓冲区。类型规范中的[?]表示可变长度数组 (VLA)。该数组的实际元素数作为ffi.new()的第二个参数给出。

④ 乍一看可能很奇怪,但看看zlib 中compress2 函数的声明:目标长度定义为指针!这是因为您传入最大缓冲区大小并取回实际使用的长度。

在 C 中,您将传入局部变量 ( &buflen ) 的地址。但是由于 Lua 中没有地址操作符,我们只需要传入一个单元素数组。方便的是,它可以一步初始化为最大缓冲区大小。调用实际的zlib.compress2函数就很简单了。

⑤我们希望将压缩后的数据作为 Lua 字符串返回,所以我们将使用ffi.string()。它需要一个指向数据开头和实际长度的指针。长度已在buflen数组中返回,因此我们将从那里获取它。

请注意,由于函数现在返回,buf和 buflen变量最终将被垃圾回收。这很好,因为ffi.string()已将内容复制到新创建的(内部)Lua 字符串中。如果您计划多次调用此函数,请考虑重用缓冲区和/或将结果返回缓冲区而不是字符串。这将减少垃圾收集和字符串实习的开销。

⑥ uncompress功能与compress功能 完全相反。压缩后的数据不包括原始字符串的大小,所以这个需要传入。否则这里就不奇怪了。

⑦使用我们刚刚定义的函数的代码只是普通的 Lua 代码。它不需要知道关于 LuaJIT FFI 的任何信息——便利的包装函数完全隐藏了它。

LuaJIT FFI 的一个主要优点是您现在可以在 Lua中编写这些包装器。使用 Lua/C API 创建额外的 C 模块所需的时间只是其中的一小部分。许多更简单的 C 函数可能可以直接从您的 Lua 代码中使用,而无需任何包装器。

旁注:zlib API 使用long类型来传递长度和大小。但所有这些 zlib 函数实际上只处理 32 位值。对于公共 API 来说,这是一个不幸的选择,但可以用 zlib 的历史来解释——我们只需要处理它。

首先,您应该知道long是 64 位类型,例如在 POSIX/x64 系统上,但在 Windows/x64 和 32 位系统上是 32 位类型。因此,结果可以是一个普通的 Lua 数字,也可以是一个装箱的 64 位整数 cdata 对象,具体取决于目标系统。

好的,所以ffi.*函数通常在您想要使用数字的任何地方接受 cdata 对象。这就是为什么我们可以将n传递给上面的ffi.string()。但是其他 Lua 库函数或模块不知道如何处理这个问题。因此,为了获得最大的可移植性,需要在传递它们之前对返回 的长结果使用tonumber() 。否则,应用程序可能在某些系统上运行,但在 POSIX/x64 环境中会失败。

为 C 类型定义元方法

以下代码解释了如何为 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

这是分步说明:

①这定义了二维点对象的 C 类型。

②我们必须先声明保存点构造函数的变量,因为它是在元方法内部使用的。

③让我们定义一个__add 元方法,它添加两个点的坐标并创建一个新的点对象。为简单起见,此函数假定两个参数都是点。但它可以是任何对象的混合,如果至少一个操作数是所需的类型(例如,添加一个点加一个数字,反之亦然)。我们的__len元方法返回一个点到原点的距离。

④如果我们用完了运算符,我们也可以定义命名方法。这里__index表定义了一个 面积函数。对于自定义索引需求,可能需要定义__index__newindex 函数

⑤这将元方法与我们的 C 类型相关联。这只需要执行一次。为方便起见, ffi.metatype()返回一个构造函数。不过,我们不需要使用它。原始的 C 类型仍可用于创建点数组等。元方法自动适用于这种类型的任何和所有用途。

请注意,与元表的关联是永久 的,以后不得修改元表!__index 表也是 如此

⑥以下是点类型及其预期结果的一些简单使用示例。预定义的操作(例如ax)可以与新定义的元方法自由混合。注意area是一个方法,必须使用 Lua 语法调用方法:a:area(),而不是 a.area()

C 类型元方法机制在与以面向对象风格编写的 C 库结合使用时最有用。创建者返回一个指向新实例的指针,方法将实例指针作为第一个参数。有时您只需将 __index指向库名称空间,将__gc指向析构函数即可。但通常你会想要添加方便的包装器,例如返回实际的 Lua 字符串或返回多个值时。

一些 C 库仅将实例指针声明为不透明的 void *类型。在这种情况下,您可以对所有声明使用假类型,例如,指向命名(不完整)结构的指针可以: typedef struct foo_type *foo_handle。C 端不知道你用 LuaJIT FFI 声明了什么,但只要底层类型兼容,一切仍然有效。

翻译成C的语法 

以下是常见的 C 习语列表及其对 LuaJIT FFI 的翻译:

Idiom C code Lua code
Pointer dereference
int *p;
x = *p;
*p = y;
x = p[0]
p[0] = y
Pointer indexing
int i, *p;
x = p[i];
p[i+1] = y;
x = p[i]
p[i+1] = y
Array indexing
int i, a[];
x = a[i];
a[i+1] = y;
x = a[i]
a[i+1] = y
struct/union dereference
struct foo s;
x = s.field;
s.field = y;
x = s.field
s.field = y
struct/union pointer deref.
struct foo *sp;
x = sp->field;
sp->field = y;
x = s.field
s.field = y
Pointer arithmetic
int i, *p;
x = p + i;
y = p - i;
x = 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 习惯用法,例如:

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

这用(更快)直接使用本地或上值替换了几个哈希表查找。这对 LuaJIT 来说不太重要,因为 JIT 编译器对哈希表查找进行了很多优化,甚至能够将其中的大部分从内部循环中提升出来。但是,它不能消除 所有这些,并且它为常用功能节省了一些输入。因此,即使使用 LuaJIT,它仍然有一席之地。

通过 FFI 库调用 C 函数时,情况略有不同。JIT 编译器具有特殊的逻辑来消除从 C 库名称空间解析的函数的所有查找开销!因此,像这样缓存单个 C 函数是没有帮助的,实际上会适得其反:

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

这会将它们变成间接调用并生成更大更慢的机器代码。相反,您需要缓存命名空间本身并依靠 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(...)。请注意,不需要将其复制到函数范围内的局部变量中。

 

ffi.* API 函数

本页详细介绍了 FFI 库提供的 API 函数。建议先通读 介绍和 FFI 教程

词汇表

声明和访问外部符号

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

ffi.cdef(def)

为类型或外部符号(命名变量或函数)添加多个 C 声明。def必须是 Lua 字符串。建议对字符串参数使用语法糖,如下所示:

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 声明,用分号分隔。可以省略单个声明的尾随分号。

请注意,外部符号仅被声明,但它们尚未绑定到任何特定地址。绑定是通过 C 库命名空间实现的(见下文)。

C 声明尚未通过 C 预处理器传递。除了#pragma pack之外,不允许使用任何预处理器标记 。用enumstatic const 或typedef替换现有 C 头文件中的#define和/或通过外部 C 预处理器传递文件(一次)。注意不要包含来自无关头文件的不需要或多余的声明。

ffi.C

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

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

在 Windows 系统上,这绑定到从 *.exelua51.dll(即 LuaJIT 本身提供的 Lua/C API)、与 LuaJIT 链接的 C 运行时库 ( msvcrt*.dll )、kernel32.dll导出的符号, user32.dllgdi32.dll

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

这将加载由名称给出的动态库并返回一个绑定到其符号的新 C 库命名空间。在 POSIX 系统上,如果globaltrue,则库符号也会加载到全局命名空间中。

如果name是路径,则从该路径加载库。否则name以系统相关的方式规范化,并在动态库的默认搜索路径中搜索:

在 POSIX 系统上,如果名称不包含点, 则附加扩展名.so 。此外,如有必要,还会添加lib前缀。因此ffi.load("z") 在默认共享库搜索路径中 查找“libz.so” 。

在 Windows 系统上,如果名称不包含点, 则会附加扩展名.dll 。因此ffi.load("ws2_32")在默认的 DLL 搜索路径中 查找 “ws2_32.dll” 。

创建 cdata 对象

以下 API 函数创建 cdata 对象(type() 返回"cdata")。所有创建的 cdata 对象都被 垃圾回收

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

为给定的ct 创建一个 cdata 对象。VLA/VLS 类型需要nelem参数。第二种语法使用 ctype 作为构造函数,在其他方面完全等效。

cdata 对象根据 initializers 的 规则进行初始化,使用可选的init参数。过多的初始化程序会导致错误。

性能注意事项:如果你想创建许多同一种对象,只解析一次 cdecl 并使用 ffi.typeof()获取它的 ctype 。然后反复使用 ctype 作为构造函数。

请注意,每次您将 匿名结构声明用于ffi.new()时,都会隐式创建一个新的和可区分的 ctype 。这可能不是您想要的,尤其是当您创建多个 cdata 对象时。C 标准不认为不同的匿名 结构是赋值兼容的,即使它们可能具有相同的字段!此外,它们被 JIT 编译器认为是不同的类型,这可能会导致过多的跟踪。 强烈建议使用ffi.cdef()声明命名结构typedef ,或者使用ffi.typeof()为匿名结构 创建单个 ctype 对象.

ctype = ffi.typeof(ct)

为给定的ct 创建一个 ctype 对象。

这个函数对于只解析一次 cdecl 然后使用生成的 ctype 对象作为构造函数特别有用。

cdata = ffi.cast(ct, init)

为给定的ct 创建一个标量 cdata 对象。cdata 对象通过init使用C 类型转换规则的“cast”变体进行初始化。

此函数主要用于覆盖指针兼容性检查或将指针转换为地址,反之亦然。

ctype = ffi.metatype(ct, metatable)

为给定的ct 创建一个 ctype 对象并将其与元表相关联。只允许结构/联合类型、复数和向量。如果需要, 其他类型可以包装在 struct中。

与元表的关联是永久的,以后不能更改。元表的内容和__index表(如果有的话)的内容都不能在之后被修改。关联的元表自动适用于这种类型的所有用途,无论对象是如何创建的或它们来自何处。请注意,对类型的预定义操作具有优先权(例如,声明的字段名称不能被覆盖)。

实现了所有标准 Lua 元方法。这些是直接调用的,没有快捷方式,并且可以使用任何类型的组合。对于二元运算,首先检查左操作数是否存在有效的 ctype 元方法。__gc元方法仅适用于struct / union类型, 并 在创建实例期间 执行隐式ffi.gc()调用。

cdata = ffi.gc(cdata, finalizer)

将终结器与指针或聚合 cdata 对象相关联。cdata 对象原封不动地返回。

此功能允许将非托管资源安全地集成到 LuaJIT 垃圾收集器的自动内存管理中。典型用法:

local p = ffi.gc(ffi.C.malloc(n), ffi.C.free)
...
p = nil -- Last reference to p is gone.
-- GC will eventually run finalizer: ffi.C.free(p)

cdata 终结器的工作方式类似于 userdata 对象的__gc元方法:当对 cdata 对象的最后一个引用消失时,将调用关联的终结器,并将 cdata 对象作为参数。终结器可以是 Lua 函数或 cdata 函数或 cdata 函数指针。可以通过设置nil 终结器来删除现有的终结器,例如在显式删除资源之前:

ffi.C.free(ffi.gc(p, nil)) -- Manually free the memory.

C Type Information

以下 API 函数返回有关 C 类型的信息。它们对于检查 cdata 对象最有用。

size = ffi.sizeof(ct [,nelem])

返回ct的大小(以字节为单位)。如果大小未知(例如对于“void”或函数类型) ,则返回nil 。VLA/VLS 类型需要nelem,cdata 对象除外

align = ffi.alignof(ct)

返回ct所需的最小对齐(以字节为单位)。

ofs [,bpos,bsize] = ffi.offsetof(ct, field)

返回字段相对于 ct 开头 的偏移量(以字节为单位),ct必须是struct。另外返回位字段的位置和字段大小(以位为单位)。

status = ffi.istype(ct, obj)

如果obj具有ct给定的 C 类型, 则 返回true。否则 返回false 。

C 类型限定符(const等)被忽略。使用标准指针兼容性规则检查指针,但对void *没有任何特殊处理。如果ct指定一个 struct / union,那么指向该类型的指针也被接受。否则,类型必须完全匹配。

注意:这个函数接受所有类型的 Lua 对象作为 obj参数,但对于非 cdata 对象 总是返回false 。

Utility Functions

err = ffi.errno([newerr])

返回由指示错误条件的最后一个 C 函数调用设置的错误号。如果存在可选的newerr参数,则将错误号设置为新值并返回先前的值。

此功能提供了一种可移植且独立于操作系统的方式来获取和设置错误号。请注意,只有一些C 函数设置错误号。只有当函数实际指示错误条件(例如返回值为-1或 NULL)时,它才有意义。否则,它可能包含也可能不包含任何先前设置的值。

建议您仅在需要时调用此函数,并且在相关 C 函数返回后尽可能接近。errno值在挂钩、内存分配、JIT 编译器的调用和其他内部 VM 活动中保留。 这同样适用于 Windows 上GetLastError()返回的值,但您需要自己声明和调用它。

str = ffi.string(ptr [,len])

从ptr 指向的数据创建一个实习 Lua 字符串 。

如果缺少 可选参数len ,则ptr将转换为“char *”,并假定数据以零结尾。字符串的长度是用 strlen()计算的。

否则ptr被转换为“void *”, len给出数据的长度。数据可能包含嵌入的零并且不需要是面向字节的(尽管这可能会导致字节序问题)。

此函数主要用于将 C 函数返回的(临时) “const char *”指针转换为 Lua 字符串并将它们存储或传递给其他需要 Lua 字符串的函数。Lua 字符串是数据的(内部)副本,不再与原始数据区域相关。Lua 字符串是 8 位干净的,可用于保存任意的非字符数据。

性能注意事项:如果已知,则传递字符串的长度会更快。例如,当长度由 sprintf()之类的 C 调用返回时。

ffi.copy(dst, src, len)
ffi.copy(dst, str)

将src 指向的数据复制到dst。 dst被转换为"void *"并且src 被转换为"const void *"

在第一种语法中,len给出了要复制的字节数。警告:如果src是 Lua 字符串,则len不得超过#src+1

在第二种语法中,副本的来源必须是 Lua 字符串。字符串的所有字节加上一个零终止符都被复制到 dst(即#src+1个字节)。

性能注意事项:ffi.copy()可用作 C 库函数 memcpy()strcpy()strncpy()的更快(内联)替代品。

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

用c给出的 len 常量字节填充dst指向 的数据。如果省略c,则数据填充为零。

性能注意事项:ffi.fill()可用作 C 库函数 memset(dst, c, len)的更快(内联)替代品。请注意参数的不同顺序!

Target-specific Information

status = ffi.abi(param)

如果参数(Lua 字符串)适用于目标 ABI(应用程序二进制接口),则 返回true 。 否则返回false 。当前定义了以下参数:

Parameter Description
32bit 32 bit architecture
64bit 64 bit architecture
le Little-endian architecture
be Big-endian architecture
fpu Target has a hardware FPU
softfp softfp calling conventions
hardfp hardfp calling conventions
eabi EABI variant of the standard ABI
win Windows variant of the standard ABI

ffi.os

包含目标操作系统名称。与jit.os内容相同 。

ffi.arch

包含目标架构名称。与jit.arch内容相同 。

Methods for Callbacks

The C types for callbacks have some extra methods:

cb:free()

释放与回调关联的资源。关联的 Lua 函数是未锚定的,可能会被垃圾回收。回调函数指针不再有效并且不能再被调用(它可能被随后创建的回调重用)。

cb:set(func)

将新的 Lua 函数与回调关联。回调的C类型和回调函数指针不变。

此方法可用于动态切换回调的接收者,而无需每次都创建新的回调并再次注册(例如使用 GUI 库)

扩展标准库函数

以下标准库函数已扩展为使用 cdata 对象:

n = tonumber(cdata)

将数字 cdata 对象转换为精度数并将其作为 Lua 数字返回。这对于装箱的 64 位整数值特别有用。警告:这种转换可能会导致精度损失。

s = tostring(cdata)

返回 64 位整数("nnn LL"或"nnn ULL")或复数("re±im i")值的字符串表示形式。否则返回 ctype 对象 ("ctype< type >") 或 cdata 对象 ("cdata< type >:  address") 的 C 类型的字符串表示形式,除非您使用__tostring元方法覆盖它(参见 ffi.metatype( ))。

iter, obj, start = pairs(cdata)
iter, obj, start = ipairs(cdata)

调用相应 ctype 的__pairs__ipairs元方法。

Lua 解析器的扩展

Lua 源代码的解析器将带有后缀LLULL的数字文字视为有符号或无符号 64 位整数。大小写无关紧要,但为了便于阅读,建议使用大写。它处理十进制(42LL)和十六进制(0x2aLL)文字。

复数的虚部可以通过在数字文字后面加上iI来指定,例如12.5i。警告:您需要使用1i来获得值为 1 的虚部,因为i本身仍然引用名为i的变量。

 

FFI 语义

本页描述了 FFI 库的详细语义及其与 Lua 和 C 代码的交互。

鉴于 FFI 库旨在与 C 代码交互,并且声明可以用纯 C 语法编写,因此它尽可能地遵循 C 语言语义。为了与 Lua 语言语义进行更顺畅的互操作,需要做出一些小的让步。

请不要被此页面的内容所淹没——这是一个参考,如果有疑问,您可能需要查阅它。浏览此页面并没有什么坏处,但大多数语义“正常工作”,正如您所期望的那样。对于具有 C 或 C++ 背景的开发人员来说,使用 LuaJIT FFI 编写应用程序应该很简单。

C 语言支持

FFI 库有一个内存占用最少的内置 C 解析器。ffi.* 库函数使用它来声明 C 类型或外部符号。

它的唯一目的是解析 C 声明,例如在 C 头文件中找到的。尽管它确实计算常量表达式,但它不是C 编译器。内联 C 函数定义 的主体被简单地忽略。

此外,这不是一个验证 C 解析器。它期望并接受格式正确的 C 声明,但它可能会选择忽略错误的声明或显示相当通用的错误消息。如果有疑问,请对照您最喜欢的 C 编译器检查输入。

C 解析器符合C99 语言标准以及以下扩展:

以下 C 类型由 C 解析器预定义(如typedef,除了重新声明将被忽略):

鼓励您优先使用这些类型,而不是特定于编译器的扩展或依赖于目标的标准类型。例如, char的符号不同,long的大小不同,这取决于目标架构和平台 ABI。

支持 以下 C 功能:

C 类型转换规则

从 C 类型到 Lua 对象的转换

这些转换规则适用于对 C 类型的读取访问:索引指针、数组或 结构/联合类型;读取外部变量或常量值;从 C 调用中检索返回值:

 

Input Conversion Output
int8_tint16_t sign-ext int32_t → double number
uint8_tuint16_t zero-ext int32_t → double number
int32_tuint32_t → double number
int64_tuint64_t boxed value 64 bit int cdata
doublefloat → double number
bool 0 → false, otherwise true boolean
enum boxed value enum cdata
Complex number boxed value complex cdata
Vector boxed value vector cdata
Pointer boxed value pointer cdata
Array boxed reference reference cdata
struct/union boxed reference reference cdata

 

引用类型在转换发生之前被取消引用——转换应用于引用指向的 C 类型。

从 Lua 对象到 C 类型的转换

这些转换规则适用于对 C 类型的写访问:索引指针、数组或 结构/联合类型;初始化 cdata 对象;强制转换为 C 类型;写入外部变量;将参数传递给 C 调用:

 

Input Conversion Output
number double
boolean false → 0, true → 1 bool
nil NULL → (void *)
lightuserdata lightuserdata address → (void *)
userdata userdata payload → (void *)
io.* file get FILE * handle → (void *)
string match against enum constant enum
string copy string data + zero-byte int8_t[]uint8_t[]
string string data → const char[]
function create callback → C function type
table table initializer Array
table table initializer struct/union
cdata cdata payload → C type

 

如果此转换的结果类型与目标的 C 类型不匹配, 则应用C 类型之间的转换规则 。

引用类型在初始化后是不可变的(“没有重新定位引用”)。出于初始化目的或将值传递给引用参数时,它们被视为指针。请注意,与 C++ 不同,没有办法在 Lua 语言语义下实现变量的自动引用生成。如果要调用带有引用参数的函数,则需要显式传递一个单元素数组。

C 类型之间的转换

这些转换规则或多或少与标准 C 转换规则相同。一些规则仅适用于强制转换,或要求指针或类型兼容:

Input Conversion Output
Signed integer narrow or sign-extend Integer
Unsigned integer narrow or zero-extend Integer
Integer round doublefloat
doublefloat trunc int32_t →narrow (u)int8_t(u)int16_t
doublefloat trunc (u)int32_t(u)int64_t
doublefloat round floatdouble
Number n == 0 → 0, otherwise 1 bool
bool false → 0, true → 1 Number
Complex number convert real part Number
Number convert real part, imag = 0 Complex number
Complex number convert real and imag part Complex number
Number convert scalar and replicate Vector
Vector copy (same size) Vector
struct/union take base address (compat) Pointer
Array take base address (compat) Pointer
Function take function address Function pointer
Number convert via uintptr_t (cast) Pointer
Pointer convert address (compat/cast) Pointer
Pointer convert address (cast) Integer
Array convert base address (cast) Integer
Array copy (compat) Array
struct/union copy (identical type) struct/union

 

 

位域或枚举类型被视为它们的基础类型。

上面未列出的转换将引发错误。例如,不能将指针转换为复数,反之亦然。

可变参数 C 函数参数的转换

将 Lua 对象传递给 vararg C 函数的可变参数部分时,以下默认转换规则适用:

Input Conversion Output
number double
boolean false → 0, true → 1 bool
nil NULL → (void *)
userdata userdata payload → (void *)
lightuserdata lightuserdata address → (void *)
string string data → const char *
float cdata double
Array cdata take base address Element pointer
struct/union cdata take base address struct/union pointer
Function cdata take function address Function pointer
Any other cdata no conversion C type

 

要将除 cdata 对象之外的 Lua 对象作为特定类型传递,您需要重写转换规则:使用构造函数或强制转换创建临时 cdata 对象,并使用要传递的值对其进行初始化:

假设x是一个 Lua 数字,下面是如何将它作为整数传递给 vararg 函数:

ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("integer value: %d\n", ffi.new("int", x))

如果不这样做,则应用默认的 Lua 编号 →双重 转换规则。期望整数的 vararg C 函数将看到乱码或未初始化的值。

初始化器

使用ffi.new()或等效的构造函数语法 创建 cdata 对象 也总是初始化其内容。根据可选初始化程序的数量和涉及的 C 类型,适用不同的规则:

表初始化器

如果 Lua 表用于初始化 Array 或struct / union ,则以下规则适用:

例子:

local ffi = require("ffi")

ffi.cdef[[
struct foo { int a, b; };
union bar { int i; double d; };
struct nested { int x; struct foo y; };
]]

ffi.new("int[3]", {})            --> 0, 0, 0
ffi.new("int[3]", {1})           --> 1, 1, 1
ffi.new("int[3]", {1,2})         --> 1, 2, 0
ffi.new("int[3]", {1,2,3})       --> 1, 2, 3
ffi.new("int[3]", {[0]=1})       --> 1, 1, 1
ffi.new("int[3]", {[0]=1,2})     --> 1, 2, 0
ffi.new("int[3]", {[0]=1,2,3})   --> 1, 2, 3
ffi.new("int[3]", {[0]=1,2,3,4}) --> error: too many initializers

ffi.new("struct foo", {})            --> a = 0, b = 0
ffi.new("struct foo", {1})           --> a = 1, b = 0
ffi.new("struct foo", {1,2})         --> a = 1, b = 2
ffi.new("struct foo", {[0]=1,2})     --> a = 1, b = 2
ffi.new("struct foo", {b=2})         --> a = 0, b = 2
ffi.new("struct foo", {a=1,b=2,c=3}) --> a = 1, b = 2  'c' is ignored

ffi.new("union bar", {})        --> i = 0, d = 0.0
ffi.new("union bar", {1})       --> i = 1, d = ?
ffi.new("union bar", {[0]=1,2}) --> i = 1, d = ?    '2' is ignored
ffi.new("union bar", {d=2})     --> i = ?, d = 2.0

ffi.new("struct nested", {1,{2,3}})     --> x = 1, y.a = 2, y.b = 3
ffi.new("struct nested", {x=1,y={2,3}}) --> x = 1, y.a = 2, y.b = 3

对 cdata 对象的操作

所有标准 Lua 运算符都可以应用于 cdata 对象或 cdata 对象和另一个 Lua 对象的混合。以下列表显示了预定义的操作。

引用类型执行以下每个操作之前被取消引用——该操作应用于引用指向的 C 类型。

总是先尝试预定义的操作,然后再推迟到相应 ctype 的元方法或索引表(如果有)(__new 除外。如果元方法查找或索引表查找失败,则会引发错误。

索引 cdata 对象

ctype 对象也可以使用字符串键进行索引。唯一的预定义操作是读取 结构/联合类型的范围常量。所有其他访问都遵循相应的元方法或索引表(如果有)。

注意:由于(故意)没有地址操作符,因此持有值类型的 cdata 对象在初始化后实际上是不可变的。在应用某些优化时,JIT 编译器会从这一事实中受益。

因此,复数和向量的元素是不可变的。但是当然可以修改包含这些类型的聚合的元素。即您不能分配给 foo.c.im,但您可以分配一个(新创建的)复数给foo.c

JIT 编译器实现了严格的别名规则:对不同类型的访问没有别名,除了符号不同(这甚至适用于char指针,与 C99 不同)。明确检测并允许通过联合进行类型双关。

调用 cdata 对象

cdata 对象的算术运算

cdata对象的比较

cdata 对象作为表键

Lua 表可能被 cdata 对象索引,但这并没有提供任何有用的语义——cdata 对象不适合作为表键!

cdata 对象被视为任何其他垃圾收集对象,并通过其地址进行散列和比较以进行表索引。由于没有 cdata 值类型的实习,相同的值可能被装箱在具有不同地址的不同 cdata 对象中。因此 t[1LL+1LL]t[2LL]通常指向同一个哈希槽,它们当然也不指向与t[2]相同的哈希槽。

如果要为按值散列和与 Lua 表的比较添加额外的处理,它将严重提高实现的复杂性并减慢常见情况。鉴于它们在 VM 中的普遍使用,这是不可接受的。

如果您确实需要使用 cdata 对象作为键,则有三种可行的选择:

参数化类型

为了便于一些抽象, ffi.typeof和 ffi.cdef这两个函数在 C 声明中支持参数化类型。注意:其他采用 cdecl 的 API 函数都不允许这样做。

任何可以在声明中写typedef name、 标识符数字的地方,都可以写成 $(美元符号)。这些占位符按出现顺序替换为 cdecl 字符串后面的参数:

-- 声明一个带有参数化字段类型和名称的结构:
ffi.cdef([[
typedef struct { $ $; } foo_t;
]], type1, name1)

-- Anonymous struct with dynamic names:
local bar_t = ffi.typeof("struct { int $, $; }", name1, name2)
-- Derived pointer type:
local bar_ptr_t = ffi.typeof("$ *", bar_t)

-- Parameterized dimensions work even where a VLA won't work:
local matrix_t = ffi.typeof("uint8_t[$][$]", width, height)

警告:这不是简单的文本替换!传递的 ctype 或 cdata 对象被视为基础类型,传递的字符串被视为标识符,数字被视为数字。你不能把它混为一谈:例如,将“int”作为字符串传递不能代替类型,你需要使用ffi.typeof("int")代替。

参数化类型的主要用途是实现抽象数据类型(示例)的库,类似于使用 C++ 模板元编程可以实现的功能。另一个用例是匿名结构的派生类型,它避免了对全局结构命名空间的污染。

请注意,参数化类型是一个很好的工具,对于某些用例来说是必不可少的。但是您会希望在常规代码中谨慎地使用它们,例如,当所有类型实际上都已修复时。

cdata 对象的垃圾收集

所有显式(ffi.new()ffi.cast()等)或隐式(访问器)创建的 cdata 对象都会被垃圾回收。您需要确保在 Lua 堆栈、上值或 Lua 表中的某处保留对 cdata 对象的有效引用,而它们仍在使用中。一旦对 cdata 对象的最后一个引用消失,垃圾收集器将自动释放它使用的内存(在下一个 GC 周期结束时)。

请注意,指针本身是 cdata 对象,但是垃圾收集器不会跟随它们。因此,例如,如果将 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.

类似的规则适用于隐式转换为 "const char *"的 Lua 字符串:字符串对象本身必须在某处引用,否则最终将被垃圾收集。然后指针将指向可能已经被覆盖的陈旧数据。请注意,只要包含它的函数(实际上是它的原型)没有被垃圾回收 ,字符串文字就会自动保持活动状态。

作为参数传递给外部 C 函数的对象保持活动状态,直到调用返回。因此,在参数列表中创建临时 cdata 对象通常是安全的。这是将特定 C 类型传递给可变参数函数的常用习惯用法。

C 函数返回的内存区域(例如从malloc())必须手动管理,当然(或使用 ffi.gc())。指向 cdata 对象的指针与 C 函数返回的指针无法区分(这是 GC 无法跟踪它们的原因之一)。

回调

每当 Lua 函数转换为 C 函数指针时,LuaJIT FFI 会自动生成特殊的回调函数。这将生成的回调函数指针与函数指针的 C 类型和 Lua 函数对象(闭包)相关联。

由于通常的转换,这可能会隐式发生,例如将 Lua 函数传递给函数指针参数时。或者您可以使用 ffi.cast()将 Lua 函数显式转换为 C 函数指针。

目前只有某些 C 函数类型可以用作回调函数。既不支持 C vararg 函数,也不支持具有按值传递聚合参数或结果类型的函数。可以从回调中调用的 Lua 函数的种类没有限制——不检查参数的正确数量。Lua 函数的返回值将被转换为结果类型,无效转换会抛出错误。

允许在回调调用中抛出错误,但一般不建议这样做。仅当您知道调用回调的 C 函数处理强制堆栈展开并且不泄漏资源时才这样做。

不允许的一件事是让对 C 函数的 FFI 调用得到 JIT 编译,这反过来又调用回调,再次调用 Lua。通常这种尝试首先被解释器捕获,并且 C 函数被列入黑名单以进行编译。

但是,这种启发式方法在特定情况下可能会失败:例如,消息轮询函数可能不会立即运行 Lua 回调,并且调用会被 JIT 编译。如果它后来碰巧回调到 Lua 中(例如一个很少调用的错误回调),你会得到一个带有消息 "bad callback"的 VM PANIC 。然后,您需要使用jit.off()为调用此类消息轮询函数(或类似函数)的周围 Lua 函数 手动关闭 JIT 编译 。

回调资源处理

回调占用资源 - 您只能同时拥有有限数量的回调(500 - 1000,具体取决于架构)。关联的 Lua 函数也被锚定以防止垃圾收集。

由于隐式转换而导致的回调是永久性的!无法猜测它们的生命周期,因为 C 端可能会存储函数指针以供以后使用(通常用于 GUI 工具包)。在终止之前无法回收关联的资源:

ffi.cdef[[
typedef int (__stdcall *WNDENUMPROC)(void *hwnd, intptr_t l);
int EnumWindows(WNDENUMPROC func, intptr_t l);
]]

-- Implicit conversion to a callback via function pointer argument.
local count = 0
ffi.C.EnumWindows(function(hwnd, l)
  count = count + 1
  return true
end, 0)
-- The callback is permanent and its resources cannot be reclaimed!
-- Ok, so this may not be a problem, if you do this only once.

注意:此示例表明您必须在 Windows/x86 系统上正确声明 __stdcall回调。与对Windows 函数 的__stdcall调用不同,无法自动检测调用约定 。

对于某些用例,需要释放资源或动态重定向回调。对 C 函数指针使用显式强制转换并保留生成的 cdata 对象。然后在 cdata 对象上 使用cb:free() 或cb:set()方法:

-- Explicitly convert to a callback via cast.
local count = 0
local cb = ffi.cast("WNDENUMPROC", function(hwnd, l)
  count = count + 1
  return true
end)

-- Pass it to a C function.
ffi.C.EnumWindows(cb, 0)
-- EnumWindows doesn't need the callback after it returns, so free it.

cb:free()
-- The callback function pointer is no longer valid and its resources
-- will be reclaimed. The created Lua closure will be garbage collected.

回调性能

回调很慢!首先,C 到 Lua 的转换本身有一个不可避免的成本,类似于lua_call()或 lua_pcall()。参数和结果编组增加了成本。最后,C 编译器和 LuaJIT 都不能跨语言障碍内联或优化,并将重复计算从回调函数中提升出来。

不要对性能敏感的工作使用回调:例如考虑一个数值积分例程,它需要一个用户定义的函数来积分。从 C 代码调用用户定义的 Lua 函数数百万次是个坏主意。回调开销对性能绝对有害。

在 Lua 中编写数值积分例程本身要快得多——JIT 编译器将能够内联用户定义的函数并与其调用上下文一起优化它,具有非常有竞争力的性能。

作为一般准则:由于现有的 C API,仅在必须时使用回调。例如,回调性能与 GUI 应用程序无关,无论如何,它大部分时间都在等待用户输入。

对于新设计,请避免使用推送式 API:C 函数为每个结果重复调用回调。而是使用拉式 API:重复调用 C 函数以获得新结果。通过 FFI 从 Lua 到 C 的调用比反过来要快得多。大多数设计良好的库已经使用拉式 API(读/写、获取/放置)。

C 库命名空间

AC 库命名空间是一种特殊的对象,它允许访问包含在共享库或默认符号命名空间中的符号。加载 FFI 库时会自动创建默认的 ffi.C命名空间。可以使用 ffi.load() API 函数为特定共享库创建 C 库命名空间。

使用符号名称(Lua 字符串)索引 C 库名称空间对象会自动将其绑定到库。首先解析符号类型——它必须是用 ffi.cdef声明的。然后通过在关联的共享库或默认符号命名空间中搜索符号名称来解析符号地址。最后,符号名称、符号类型及其地址之间的最终绑定被缓存。缺少符号声明或不存在的符号名称会导致错误。

这是对不同类型的符号 进行读取访问 时发生的情况:

这是写访问 时发生的情况:

C 库名称空间本身就是垃圾收集对象。如果对命名空间对象的最后一个引用消失了,垃圾收集器最终将释放共享库引用并删除与命名空间关联的所有内存。由于这可能会触发从正在运行的进程的内存中删除共享库,因此如果命名空间对象可能未被引用,则使用从库中获取的函数 cdata 对象 通常是不安全的。

性能注意事项:JIT 编译器专门用于命名空间对象的标识以及用于对其进行索引的字符串。这有效地将函数 cdata 对象转换为常量。显式缓存这些函数对象(例如local strlen = ffi.C.strlen)没有用,实际上会适得其反。OTOH 缓存命名空间本身很有用,例如local C = ffi.C

不需要手动!

FFI 库被设计为低级库。目标是以最少的开销与 C 代码和 C 数据类型交互。这意味着您可以在 C 中做任何可以做的事情:访问所有内存、覆盖内存中的任何内容、在任何内存地址调用机器代码等等。

与常规 Lua 代码不同 ,FFI 库不提供内存安全。它将很高兴地允许您取消引用NULL 指针、越界访问数组或错误声明 C 函数。如果你犯了一个错误,你的应用程序可能会崩溃,就像等效的 C 代码一样。

这种行为是不可避免的,因为目标是提供与 C 代码的完全互操作性。添加额外的安全措施,如边界检查,将是徒劳的。无法检测 C 函数的错误声明,因为共享库仅提供符号名称,但不提供类型信息。同样,无法推断返回指针的有效索引范围。

同样:FFI 库是一个低级库。这意味着它需要小心使用,但它的灵活性和性能往往超过了这个问题。如果您是 C 或 C++ 开发人员,那么应用您现有的知识会很容易。OTOH 为 FFI 库编写代码不适合胆小的人,对于没有 Lua、C 或 C++ 经验的人来说,这可能不应该是第一个练习。

作为上述推论,FFI 库对于不受信任的 Lua 代码使用是不安全的。如果您对不受信任的 Lua 代码进行沙盒处理,您绝对不想让此代码访问 FFI 库或任何cdata 对象(64 位整数或复数除外)。任何设计合理的 Lua 沙箱都需要为许多标准 Lua 库函数提供安全包装器——也需要为 FFI 数据类型的高级操作编写类似的包装器。

当前状态

FFI 库的初始版本有一些限制,并且缺少一些功能。其中大部分将在未来的版本中修复。

C 语言支持目前不完整:

JIT 编译器已经处理了所有 FFI 操作的一个大子集。对于未实现的操作,它会自动回退到解释器(您可以使用-jv命令行选项检查这一点 )。以下操作当前未编译并且可能表现出次优性能,尤其是在内部循环中使用时:

其他缺失的功能:

 

jit.* Library

此内置模块中的函数控制 JIT 编译器引擎的行为。请注意,JIT 编译是全自动的——除非您有特殊需要,否则您可能不需要使用以下任何功能。

jit.on()
jit.off()

打开(默认)或关闭整个 JIT 编译器。

这些函数通常与命令行选项 -j on-j off一起使用。

jit.flush()

刷新已编译代码的整个缓存。

jit.on(func|true [,true|false])
jit.off(func|true [,true|false])
jit.flush(func|true [,true|false])

jit.on为 Lua 函数启用 JIT 编译(这是默认设置)。

jit.off禁用 Lua 函数的 JIT 编译,并从代码缓存中刷新任何已编译的代码。

jit.flush刷新代码,但不影响启用/禁用状态。

当前函数,即调用这个库函数的 Lua 函数,也可以通过传递true作为第一个参数来指定。

如果第二个参数为true,则 JIT 编译也会为函数的所有子函数启用、禁用或以递归方式刷新。如果设置为false,则仅影响子​​功能。

jit.onjit.off函数只设置一个标志, 该标志在函数即将编译时检查。它们不会触发立即编译。

典型用法是模块主块中的jit.off(true, true)以关闭整个模块的 JIT 编译以进行调试。

jit.flush(tr)

从缓存中刷新由其编号指定的根跟踪及其所有侧跟踪。只要有任何其他跟踪链接到它,跟踪的代码就会被保留。

status, ... = jit.status()

返回 JIT 编译器的当前状态。如果 JIT 编译器打开或关闭,第一个结果要么为,要么为假。其余结果是特定于 CPU 的功能和启用的优化的字符串。

jit.version

包含 LuaJIT 版本字符串。

jit.version_num

包含 LuaJIT 核心的版本号。版本 xx.yy.zz 由十进制数 xxyyzz 表示。

jit.os

包含目标操作系统名称:“Windows”、“Linux”、“OSX”、“BSD”、“POSIX”或“Other”。

jit.arch

包含目标架构名称:“x86”、“x64”、“arm”、“ppc”、“ppcspe”或“mips”。

jit.opt.* — JIT 编译器优化控制

该子模块为-O命令行选项提供后端。

您也可以通过编程方式使用它,例如:

jit.opt.start(2) -- 与 -O2 相同
jit.opt.start("-dce")
jit.opt.start("hotloop=10", "hotexit=2")

与 LuaJIT 1.x 不同的是,该模块是内置的,并且 默认开启优化! 不再需要运行require("jit.opt").start(),这是启用优化的方法之一。

jit.util.* — JIT 编译器自省

该子模块包含自省字节码、生成的跟踪、IR 和生成的机器代码的功能。此模块提供的功能仍在不断变化,因此未记录在案。

调试模块-jbc-jv-jdump广泛使用了这些功能。如果您想了解更多信息,请查看他们的源代码



Lua/C API 扩展

LuaJIT 为标准 Lua/C API 添加了一些扩展。LuaJIT 包含目录必须在编译器搜索路径(-I路径)中才能包含 C 代码所需的头文件:

#include “luajit.h”

或者对于 C++ 代码:

#include “lua.hpp”

luaJIT_setmode(L, idx, mode) — 控制虚拟机

这是一个 C API 扩展,允许从 C 代码控制 VM。LuaJIT_setmode的完整原型是:

LUA_API int luaJIT_setmode(lua_State *L, int idx, int mode);

返回的状态是成功 ( 1 ) 或失败 ( 0 )。第二个参数是0或堆栈索引(类似于其他 Lua/C API 函数)。

第三个参数指定模式,它与标志进行“或”运算。该标志可以是LUAJIT_MODE_OFF关闭功能, LUAJIT_MODE_ON打开功能,或 LUAJIT_MODE_FLUSH刷新缓存代码。

定义了以下模式:

luaJIT_setmode(L, 0, LUAJIT_MODE_ENGINE|flag)

打开或关闭整个 JIT 编译器或刷新已编译代码的整个缓存。

luaJIT_setmode(L, idx, LUAJIT_MODE_FUNC|flag)
luaJIT_setmode(L, idx, LUAJIT_MODE_ALLFUNC|flag)
luaJIT_setmode(L, idx, LUAJIT_MODE_ALLSUBFUNC|flag)

这会在堆栈索引idx或调用函数的父级 ( idx = 0 ) 处设置函数的模式。它可以为函数启用 JIT 编译,禁用它并刷新任何已编译的代码或仅刷新已编译的代码。这递归地适用于具有 LUAJIT_MODE_ALLFUNC的函数的所有子函数,或者仅适用于具有 LUAJIT_MODE_ALLSUBFUNC的子函数。

luaJIT_setmode(L, trace,
  LUAJIT_MODE_TRACE|LUAJIT_MODE_FLUSH)

从缓存中刷新指定的根跟踪及其所有边跟踪。只要有任何其他跟踪链接到它,跟踪的代码就会被保留。

luaJIT_setmode(L, idx, LUAJIT_MODE_WRAPCFUNC|flag)

此模式为调用 C 函数定义了一个包装函数。如果使用LUAJIT_MODE_ON调用,则idx处的堆栈索引 必须是一个lightuserdata对象,其中包含指向包装函数的指针。从现在开始,所有 C 函数都通过包装函数调用。如果使用LUAJIT_MODE_OFF 调用此模式,则会关闭所有 C 函数并直接调用。

包装函数可用于调试目的或捕获和转换外部异常。但请先阅读 C++ 异常互操作性部分 。推荐的用法可以在这个 C++ 代码摘录中看到:

#include <exception>
#include "lua.hpp"

// Catch C++ exceptions and convert them to Lua error messages.
// Customize as needed for your own exception classes.
static int wrap_exceptions(lua_State *L, lua_CFunction f)
{
  try {
    return f(L);  // Call wrapped function and return result.
  } catch (const char *s) {  // Catch and convert exceptions.
    lua_pushstring(L, s);
  } catch (std::exception& e) {
    lua_pushstring(L, e.what());
  } catch (...) {
    lua_pushliteral(L, "caught (...)");
  }
  return lua_error(L);  // Rethrow as a Lua error.
}

static int myinit(lua_State *L)
{
  ...
  // Define wrapper function and enable it.
  lua_pushlightuserdata(L, (void *)wrap_exceptions);
  luaJIT_setmode(L, -1, LUAJIT_MODE_WRAPCFUNC|LUAJIT_MODE_ON);
  lua_pop(L, 1);
  ...
}

请注意,您只能定义一个全局包装函数,因此在多个 C++ 模块中使用此机制时要小心。另请注意,此机制并非没有开销。

 
 

标签:__,luajit,ffi,中文版,Lua,函数,cdata,local,LuaJIT
来源: https://www.cnblogs.com/zx-admin/p/16363431.html