buffer: |
字符串缓冲区库允许对类字符串数据进行高性能操作。
与 Lua 字符串(常量)不同,字符串缓冲区是可变的 8 位(二进制透明)字符序列。数据可以存储、格式化并编码到字符串缓冲区中,之后再转换、提取或解码。
便捷的字符串缓冲区 API 简化了常见的字符串处理任务,否则这些任务通常需要创建大量中间字符串。字符串缓冲区通过消除冗余的内存拷贝、对象创建、字符串驻留以及垃圾回收开销来提升性能;与 FFI 库结合时,还能实现零拷贝操作。
字符串缓冲区库还包含一个用于 Lua 对象的高性能序列化器。
## 流式序列化
在某些场景下,希望对大型数据集进行分片序列化(也称 streaming)。该序列化格式可安全拼接并支持流式:可以把多个编码简单地追加到一个缓冲区,之后再分别解码:
```lua
local buf = buffer.new()
buf:encode(obj1)
buf:encode(obj2)
local copy1 = buf:decode()
local copy2 = buf:decode()
```
下面展示如何迭代一个流:
```lua
while #buf ~= 0 do
local obj = buf:decode()
-- 对 obj 做些处理。
end
```
由于该序列化格式不会在编码前附加长度信息,网络应用可能还需要同时传输长度。
该序列化格式主要供 LuaJIT 应用内部使用。序列化数据向上兼容,并可在所有支持的 LuaJIT 平台间移植。
它是 8 位二进制格式,非人类可读,可能包含嵌入的零字节;并且会原样存储嵌入的 Lua 字符串对象(同样是 8 位透明)。编码后的数据可以安全拼接以用于流式处理,并可在之后按“顶层对象”逐个解码。
该编码相对紧凑,但优化目标是最大性能而非最小空间占用;它也能很好地被常见的按字节数据压缩算法压缩。
尽管本文档给出了说明,但该格式明确不打算作为跨语言结构化数据交换的“公共标准”(如 JSON 或 MessagePack);请不要这样使用。
规范以无上下文文法给出,以顶层对象为起点。备选项以 `|` 分隔,`*` 表示重复。分组是隐式的,或用 `{…}` 表示。终结符要么是以字节编码的十六进制数,要么带有 `.format` 后缀。
```
object → nil | false | true
```
buf: |
缓冲区对象是由垃圾回收管理的 Lua 对象。通过 `buffer.new()` 创建后,它可以(也应该)在多个操作中复用。当最后一个对缓冲区对象的引用消失后,它最终会由垃圾回收器释放,包括其分配的缓冲区空间。
缓冲区以 FIFO(先进先出)数据结构方式工作:数据可以被追加(写入)到缓冲区末尾,也可以从缓冲区头部被消费(读取);这些操作可以任意混合。
用于保存字符的缓冲区空间由系统自动管理——按需增长,并会回收已消费的空间。如需更细粒度的控制,可使用 `buffer.new(size)` 与 `buf:free()`。
单个缓冲区的最大大小与 Lua 字符串的最大大小相同,略低于 2GB。对于超大数据规模,字符串和缓冲区都不是合适的数据结构——应使用 FFI 库直接映射内存或文件,直到操作系统的虚拟内存限制。
string.buffer.data: |
要写入缓冲区的数据:字符串、数字,或带有 `__tostring` 元方法的对象 `obj`。
buf.put: |
向缓冲区追加一个字符串 `str`、数字 `num`,或任何带有 `__tostring` 元方法的对象 `obj`。多个参数会按给定顺序依次追加。
将一个缓冲区追加到另一个缓冲区是可行的,并会在内部进行短路处理,但仍然会发生一次拷贝;更好的做法是合并写入,尽量使用同一个缓冲区完成写入。
buf.putf: |
将格式化后的参数追加到缓冲区。格式字符串支持与 `string.format()` 相同的选项。
buf.putcdata: |
将 FFI cdata 对象指向的内存中 `len` 个字节追加到缓冲区。该对象需要可转换为(常量)指针。
buf.set: |
该方法允许将一个字符串或 FFI cdata 对象以“零拷贝”的方式作为缓冲区来消费。它会在缓冲区中保存对传入字符串 `str` 或 FFI cdata 对象的引用,并释放原本为缓冲区分配的任何空间。
调用该方法后,缓冲区的行为等同于调用 `buf:free():put(str)` 或 `buf:free():put(cdata, len)`,但只要缓冲区只被消费(读取),数据仅被引用而不会被复制。
如果之后对缓冲区进行写入,则会把被引用的数据拷贝到内部缓冲区,并移除对象引用(写时复制语义)。
保存的引用会作为垃圾回收器的锚点,确保原本传入的字符串或 FFI cdata 对象保持存活。
buf.reset: |
重置(清空)缓冲区。已分配的缓冲区空间不会被释放,并可被复用。
buf.free: |
释放缓冲区对象所占用的缓冲区空间。对象本身仍然存在,处于空状态并可被复用。
注意:通常不需要使用该方法。缓冲区对象被回收时,垃圾回收器会自动释放其缓冲区空间。只有在你需要立即释放关联内存时才应使用此方法。
buf.reserve: |
`reserve` 方法会在缓冲区中至少预留 `size` 字节的写入空间,并返回一个指向该空间的 `uint8_t *` FFI cdata 指针 `ptr`。
可用长度(字节)会通过 `len` 返回:它至少为 `size`,但为了便于高效扩容,可能会更大。你可以利用这部分额外空间,也可以忽略 `len`,只使用 `size` 字节。
该方法与 `buf:commit()` 配合,可对 C 风格的 read API 实现零拷贝写入:
```lua
local MIN_SIZE = 65536
repeat
local ptr, len = buf:reserve(MIN_SIZE)
local n = C.read(fd, ptr, len)
if n == 0 then break end -- EOF.
if n < 0 then error("read error") end
buf:commit(n)
until false
```
预留的写入空间不会被初始化。在调用 `commit` 之前,至少需要把实际使用的 `used` 字节写入该空间。如果没有向缓冲区添加任何内容(例如发生错误),则无需调用 `commit`。
buf.reserve.return.1: |
指向该空间的 `uint8_t *` FFI cdata 指针。
buf.reserve.return.2: |
可用长度(字节)。
buf.commit: |
将先前 `reserve` 返回的写入空间中实际使用的 `used` 字节追加到缓冲区数据中。
buf.skip: |
从缓冲区中跳过(消费)`len` 个字节,最多到当前缓冲区数据长度为止。
buf.get: |
消费缓冲区数据并返回一个或多个字符串。多个参数会按给定顺序依次消费缓冲区数据。
- 不带参数调用时,消费整个缓冲区数据。
- 以数字参数调用时,最多消费 `len` 字节。
- 参数为 `nil` 时,会消费剩余的缓冲区空间(这通常只应作为最后一个参数使用)。
注意:当长度为 0 或没有剩余缓冲区数据时,返回的是空字符串而不是 `nil`。
buf.tostring: |
从缓冲区数据创建一个字符串,但不会消费数据;缓冲区保持不变。
缓冲区对象还定义了 `__tostring` 元方法。这意味着缓冲区可以传给全局 `tostring()` 函数,以及许多可以用它代替字符串的函数。在诸如 `io.write()` 之类的函数中,一些关键的内部用法会被短路处理,以避免创建中间字符串对象。
buf.ref: |
返回一个指向缓冲区数据的 `uint8_t *` FFI cdata 指针 `ptr`,并通过 `len` 返回缓冲区数据的字节长度。
返回的指针可直接传给需要“缓冲区 + 长度”的 C 函数。你也可以对缓冲区数据进行逐字节读取(`local x = ptr[i]`)或写入(`ptr[i] = 0x40`)。
结合 `buf:skip()` 方法,可以对 C 风格的 write API 实现零拷贝写入:
```lua
repeat
local ptr, len = buf:ref()
if len == 0 then break end
local n = C.write(fd, ptr, len)
if n < 0 then error("write error") end
buf:skip(n)
until n >= len
```
与 Lua 字符串不同,缓冲区数据不会隐式以 0 结尾。把 `ptr` 传给期望以 0 结尾字符串的 C 函数并不安全。如果你不使用 `len`,那很可能是在做错事。
buf.ref.return.1: |
指向缓冲区数据的 `uint8_t *` FFI cdata 指针。
buf.ref.return.2: |
缓冲区数据的字节长度。
buf.encode: |
将 Lua 对象序列化(编码)到缓冲区。
尝试序列化不支持的对象类型、循环引用或嵌套很深的表时,该函数可能会抛出错误。
buf.decode: |
从缓冲区反序列化(解码)一个对象。
返回的对象可以是任意受支持的 Lua 类型——甚至可以是 `nil`。
当输入的数据格式错误或编码数据不完整时,该函数可能会抛出错误。
解码后会把未消费的剩余数据保留在缓冲区中。
如果尝试反序列化 FFI 类型,而 FFI 库未内建或尚未加载,则会抛出错误。
buffer.encode: |
序列化(编码)Lua 对象 `obj`。
尝试序列化不支持的对象类型、循环引用或嵌套很深的表时,该函数可能会抛出错误。
buffer.decode: |
将字符串反序列化(解码)为 Lua 对象。
返回的对象可以是任意受支持的 Lua 类型——甚至可以是 `nil`。
当输入的数据格式错误或编码数据不完整时会抛出错误。
当解码单个顶层对象后仍有剩余数据时也会抛出错误。
如果尝试反序列化 FFI 类型,而 FFI 库未内建或尚未加载,则会抛出错误。
buffer.new: |
创建一个新的缓冲区对象。
可选参数 `size` 用于确保最小的初始缓冲区大小。当你预先知道所需缓冲区大小时,这严格来说只是一次优化;无论如何,缓冲区空间都会按需增长。
可选的表参数 `options` 用于设置各种序列化选项。
string.buffer.serialization.opts: |
序列化选项
传给 `buffer.new()` 的 `options` 表可以包含以下成员(全部可选):
- `dict`:一个 Lua 表,保存一组在你要序列化的对象中经常作为表键出现的字符串字典。序列化时这些键会被紧凑地编码为索引。选择合适的字典可以节省空间并提升序列化性能。
- `metatable`:一个 Lua 表,保存一组用于你要序列化的表对象的元表字典。
`dict` 需要是字符串数组,`metatable` 需要是表数组;两者都从索引 1 开始且不能有空洞(中间不能出现 nil)。这些表会被锚定在缓冲区对象中,并在内部被改造为双向索引(不要自己这么做,只需传入普通数组)。在把它们传给 `buffer.new()` 之后,这些表不得再被修改。
编码器与解码器使用的 `dict` 与 `metatable` 必须一致。把最常见的条目放在前面,并在末尾扩展以保证向后兼容——这样旧的编码仍然能被读取。你也可以将某些索引设为 false 来显式放弃向后兼容;解码使用了这些索引的旧编码时会抛出错误。
编码时,如果某个元表不在 `metatable` 字典中,则会被忽略;解码会返回一个元表为 nil 的表。
注意:解析并准备 `options` 表的开销相对较高。建议只创建一次缓冲区对象并在多次使用中复用。避免混用编码与解码缓冲区,因为 `buf:set()` 会释放已分配的缓冲区空间:
```lua
local options = {
dict = { "commonly", "used", "string", "keys" },
}
local buf_enc = buffer.new(options)
local buf_dec = buffer.new(options)
local function encode(obj)
return buf_enc:reset():encode(obj):get()
end
local function decode(str)
return buf_dec:set(str):decode()
end
```