模块 Bytes

module Bytes: sig .. end

字节序列操作。

字节序列是一个可变数据结构,它包含一个固定长度的字节序列。每个字节都可以通过其在序列中的索引在恒定时间内进行读写。

给定一个长度为 l 的字节序列 s,我们可以通过其在序列中的索引访问 sl 个字节中的每一个。索引从 0 开始,如果索引落在范围 [0...l-1](包含)内,我们将称其在 s 中有效。位置是指两个字节之间的点,或序列的开头或结尾。如果位置落在范围 [0...l](包含)内,我们将称其在 s 中有效。请注意,索引为 n 的字节位于位置 nn+1 之间。

如果 len >= 0 并且 startstart+lens 中的有效位置,则称两个参数 startlen 指定了 s 的有效范围。

字节序列可以在内存中修改,例如通过下面描述的 setblit 函数。另请参见字符串(模块 String),它们几乎是相同的数据结构,但不能在内存中修改。

字节由 OCaml 类型 char 表示。

此模块的带标签版本可以按 StdLabels 模块中所述使用。


val length : bytes -> int

返回参数的长度(字节数)。

val get : bytes -> int -> char

get s n 返回参数 s 中索引为 n 的字节。

val set : bytes -> int -> char -> unit

set s n c 在内存中修改 s,用 c 替换索引为 n 的字节。

val create : int -> bytes

create n 返回一个长度为 n 的新字节序列。该序列未初始化,包含任意字节。

val make : int -> char -> bytes

make n c 返回一个长度为 n 的新字节序列,用字节 c 填充。

val init : int -> (int -> char) -> bytes

init n f 返回一个长度为 n 的新字节序列,字符 i 初始化为 f i 的结果(以递增的索引顺序)。

val empty : bytes

大小为 0 的字节序列。

val copy : bytes -> bytes

返回一个包含与参数相同字节的新字节序列。

val of_string : string -> bytes

返回一个包含与给定字符串相同字节的新字节序列。

val to_string : bytes -> string

返回一个包含与给定字节序列相同字节的新字符串。

val sub : bytes -> int -> int -> bytes

sub s pos len 返回一个长度为 len 的新字节序列,包含 s 的子序列,该子序列从位置 pos 开始,长度为 len

val sub_string : bytes -> int -> int -> string

Bytes.sub 相同,但返回字符串而不是字节序列。

val extend : bytes -> int -> int -> bytes

extend s left right 返回一个包含 s 的字节的新字节序列,在其前面添加了 left 个未初始化字节,在其后面添加了 right 个未初始化字节。如果 leftright 为负数,则从 s 的相应侧删除字节(而不是追加)。

val fill : bytes -> int -> int -> char -> unit

fill s pos len c 在内存中修改 s,用 c 替换 len 个字符,从 pos 开始。

val blit : bytes -> int -> bytes -> int -> int -> unit

blit src src_pos dst dst_pos lenlen 个字节从字节序列 src(从索引 src_pos 开始)复制到字节序列 dst(从索引 dst_pos 开始)。即使 srcdst 是同一个字节序列,并且源和目标间隔重叠,它也能正常工作。

val blit_string : string -> int -> bytes -> int -> int -> unit

blit_string src src_pos dst dst_pos lenlen 个字节从字符串 src(从索引 src_pos 开始)复制到字节序列 dst(从索引 dst_pos 开始)。

val concat : bytes -> bytes list -> bytes

concat sep sl 连接字节序列列表 sl,在每个序列之间插入分隔符字节序列 sep,并将结果作为新字节序列返回。

val cat : bytes -> bytes -> bytes

cat s1 s2 连接 s1s2,并将结果作为新字节序列返回。

val iter : (char -> unit) -> bytes -> unit

iter f s 依次将函数 f 应用于 s 的所有字节。它等效于 f (get s 0); f (get s 1); ...; f (get s
    (length s - 1)); ()

val iteri : (int -> char -> unit) -> bytes -> unit

Bytes.iter 相同,但函数以字节的索引作为第一个参数,以字节本身作为第二个参数应用。

val map : (char -> char) -> bytes -> bytes

map f s 依次将函数 f 应用于 s 的所有字节(以递增的索引顺序),并将结果字节存储在作为结果返回的新序列中。

val mapi : (int -> char -> char) -> bytes -> bytes

mapi f s 使用 s 的每个字符及其索引(以递增的索引顺序)调用 f,并将结果字节存储在作为结果返回的新序列中。

val fold_left : ('acc -> char -> 'acc) -> 'acc -> bytes -> 'acc

fold_left f x s 计算 f (... (f (f x (get s 0)) (get s 1)) ...) (get s (n-1)),其中 ns 的长度。

val fold_right : (char -> 'acc -> 'acc) -> bytes -> 'acc -> 'acc

fold_right f s x 计算 f (get s 0) (f (get s 1) ( ... (f (get s (n-1)) x) ...)),其中 ns 的长度。

val for_all : (char -> bool) -> bytes -> bool

for_all p s 检查 s 中的所有字符是否都满足谓词 p

val exists : (char -> bool) -> bytes -> bool

exists p s 检查 s 中至少一个字符是否满足谓词 p

val trim : bytes -> bytes

返回参数的副本,不包含前导和尾随空格。被视为空格的字节是 ASCII 字符 ' ''\012''\n''\r''\t'

val escaped : bytes -> bytes

返回参数的副本,特殊字符由转义序列表示,遵循 OCaml 的词法约定。所有不在 ASCII 可打印范围(32..126)内的字符,以及反斜杠和双引号,都会被转义。

val index : bytes -> char -> int

index s c 返回字节 cs 中首次出现的索引。

val index_opt : bytes -> char -> int option

index_opt s c 返回字节 cs 中首次出现的索引,或者如果 c 不出现在 s 中,则返回 None

val rindex : bytes -> char -> int

rindex s c 返回字节 cs 中最后一次出现的索引。

val rindex_opt : bytes -> char -> int option

rindex_opt s c 返回字节 cs 中最后一次出现的索引,或者如果 c 不出现在 s 中,则返回 None

val index_from : bytes -> int -> char -> int

index_from s i c 返回字节 c 在位置 i 之后在 s 中首次出现的索引。 index s c 等效于 index_from s 0 c

val index_from_opt : bytes -> int -> char -> int option

index_from_opt s i c 返回字节 c 在位置 i 之后在 s 中首次出现的索引,或者如果 c 在位置 i 之后不出现 s 中,则返回 Noneindex_opt s c 等效于 index_from_opt s 0 c

val rindex_from : bytes -> int -> char -> int

rindex_from s i c 返回字节 cs 中位置 i+1 之前的最后一次出现的索引。 rindex s c 等价于 rindex_from s (length s - 1) c

val rindex_from_opt : bytes -> int -> char -> int option

rindex_from_opt s i c 返回字节 cs 中位置 i+1 之前的最后一次出现的索引,或者如果 c 在位置 i+1 之前没有出现在 s 中,则返回 Nonerindex_opt s c 等价于 rindex_from s (length s - 1) c

val contains : bytes -> char -> bool

contains s c 测试字节 c 是否出现在 s 中。

val contains_from : bytes -> int -> char -> bool

contains_from s start c 测试字节 c 是否出现在位置 start 之后的 s 中。 contains s c 等价于 contains_from
    s 0 c

val rcontains_from : bytes -> int -> char -> bool

rcontains_from s stop c 测试字节 c 是否出现在位置 stop+1 之前的 s 中。

val uppercase_ascii : bytes -> bytes

返回参数的副本,其中所有小写字母使用 US-ASCII 字符集转换为大写字母。

val lowercase_ascii : bytes -> bytes

返回参数的副本,其中所有大写字母使用 US-ASCII 字符集转换为小写字母。

val capitalize_ascii : bytes -> bytes

返回参数的副本,其中第一个字符使用 US-ASCII 字符集设置为大写字母。

val uncapitalize_ascii : bytes -> bytes

返回参数的副本,其中第一个字符使用 US-ASCII 字符集设置为小写字母。

type t = bytes 

字节序列类型的别名。

val compare : t -> t -> int

字节序列的比较函数,具有与 compare 相同的规范。与类型 t 一样,此函数 compare 允许将模块 Bytes 作为参数传递给函子 Set.MakeMap.Make

val equal : t -> t -> bool

字节序列的相等函数。

val starts_with : prefix:bytes -> bytes -> bool

starts_with ~prefix s 当且仅当 sprefix 开头时为 true

val ends_with : suffix:bytes -> bytes -> bool

ends_with ~suffix s 当且仅当 ssuffix 结尾时为 true

不安全转换(适用于高级用户)

本节介绍 bytesstring 之间的非安全低级转换函数。它们不会复制内部数据;如果使用不当,它们会破坏 -safe-string 选项提供的字符串不可变性不变式。它们适用于专家库作者,但对于大多数目的,您应该使用始终正确的 Bytes.to_stringBytes.of_string 代替。

val unsafe_to_string : bytes -> string

将字节序列非安全地转换为字符串。

为了推断 unsafe_to_string 的用法,考虑“所有权”规则比较方便。操作某些数据的代码片段“拥有”它;有几种不相交的所有权模式,包括

  • 唯一所有权:可以访问和修改数据
  • 共享所有权:数据有多个所有者,它们只能访问它,不能修改它。

唯一所有权是线性的:将数据传递给另一段代码意味着放弃所有权(我们无法再写入数据)。唯一所有者可以选择共享数据(放弃对它的修改权),但共享数据不能再次成为唯一拥有的。

unsafe_to_string s 只能在调用者拥有字节序列 s 时使用,无论是唯一所有权还是共享的不可变数据。调用者放弃对 s 的所有权,并获得对返回字符串的所有权。

有两种有效的用例符合此所有权规则

1. 通过初始化和修改在初始化后不再更改的字节序列来创建字符串。

let string_init len f : string =
  let s = Bytes.create len in
  for i = 0 to len - 1 do Bytes.set s i (f i) done;
  Bytes.unsafe_to_string s
   

此函数是安全的,因为字节序列 s 在调用 unsafe_to_string 之后将不再被访问或修改。 string_init 代码放弃对 s 的所有权,并将结果字符串的所有权返回给它的调用者。

请注意,如果将 s 作为附加参数传递给函数 f,则它将不安全,因为它可能以这种方式转义并在将来被修改 - string_init 将放弃对 s 的所有权以将其传递给 f,并且不能安全地调用 unsafe_to_string

我们提供了 String.initString.mapString.mapi 函数来涵盖大多数构建新字符串的情况。您应该在适用时优先使用这些函数而不是 to_stringunsafe_to_string

2. 将字节序列的所有权临时授予期望唯一拥有的字符串并返回所有权的函数,以便我们可以在调用结束之后再次修改序列。

let bytes_length (s : bytes) =
  String.length (Bytes.unsafe_to_string s)
   

在此用例中,我们不承诺 s 在调用 bytes_length s 之后将永远不会被修改。 String.length 函数暂时借用字节序列的唯一所有权(并将其视为 string),但将此所有权返回给调用者,调用者可以假设 s 在调用之后仍然是有效的字节序列。请注意,这仅在已知 String.length 不会捕获其参数时才正确 - 它可以通过诸如记忆组合器之类的侧信道转义。

调用者在借用字符串时可能不会修改 s(它已经暂时放弃了所有权)。这会影响并发程序,还会影响高阶函数:如果 String.length 返回稍后要调用的闭包,则 s 应该在该闭包完全应用并返回所有权之前不进行修改。

val unsafe_of_string : string -> bytes

将共享字符串非安全地转换为不应该被修改的字节序列。

使 unsafe_to_string 正确的相同所有权规则适用于 unsafe_of_string:如果您是 string 值的所有者,您可以使用它,并且您将以相同的模式拥有返回的 bytes

实际上,字符串值的唯一所有权非常难以正确推理。您应该始终假设字符串是共享的,而不是唯一拥有的。

例如,字符串字面量被编译器隐式共享,因此您永远不会唯一拥有它们。

let incorrect = Bytes.unsafe_of_string "hello"
let s = Bytes.of_string "hello"
    

第一个声明是错误的,因为字符串字面量 "hello" 可能被编译器与程序的其他部分共享,而修改 incorrect 是一个错误。您必须始终使用第二个版本,它执行复制,因此是正确的。

假设不是字符串字面量,而是部分由字符串字面量构建的字符串的唯一所有权也是错误的。例如,修改 unsafe_of_string ("foo" ^ s) 可能修改共享字符串 "foo" - 假设字符串的绳索式表示。更一般地说,操作字符串的函数将假设共享所有权,它们不会保留唯一所有权。因此,假设 unsafe_of_string 结果的唯一所有权是错误的。

我们唯一有合理信心是安全的案例是,如果生成的 bytes 是共享的 - 用作不可变字节序列。这可能对增量迁移处理不可变字节序列的低级程序(例如 Marshal.from_bytes)并且以前为此目的使用 string 类型很有用。

val split_on_char : char -> bytes -> bytes list

split_on_char sep s 返回 s 的所有(可能为空)子序列的列表,这些子序列由 sep 字符分隔。如果 s 为空,则结果是单例列表 [empty]

函数的输出由以下不变式指定

  • 列表不为空。
  • 使用 sep 作为分隔符连接其元素会返回一个字节序列,该序列等于输入 (Bytes.concat (Bytes.make 1 sep)
          (Bytes.split_on_char sep s) = s
    )。
  • 结果中的任何字节序列都不包含 sep 字符。

迭代器

val to_seq : t -> char Seq.t

在字符串上进行迭代,按照递增的索引顺序。在迭代期间对字符串的修改将反映在序列中。

val to_seqi : t -> (int * char) Seq.t

在字符串上进行迭代,按照递增顺序,在字符之间生成索引

val of_seq : char Seq.t -> t

从生成器创建字符串

UTF 编码和验证

UTF-8

val get_utf_8_uchar : t -> int -> Uchar.utf_decode

get_utf_8_uchar b i 解码 b 中索引 i 处的 UTF-8 字符。

val set_utf_8_uchar : t -> int -> Uchar.t -> int

set_utf_8_uchar b i uu 的 UTF-8 编码到 b 中的索引 i 处,并返回从 i 开始写入的字节数 n。如果 n0,则没有足够的空间在 i 处编码 u,并且 b 保持不变。否则,可以在 i + n 处编码新字符。

val is_valid_utf_8 : t -> bool

is_valid_utf_8 b 当且仅当 b 包含有效的 UTF-8 数据时为 true

UTF-16BE

val get_utf_16be_uchar : t -> int -> Uchar.utf_decode

get_utf_16be_uchar b i 解码 b 中索引 i 处的 UTF-16BE 字符。

val set_utf_16be_uchar : t -> int -> Uchar.t -> int

set_utf_16be_uchar b i uu 的 UTF-16BE 编码到 b 中的索引 i 处,并返回从 i 开始写入的字节数 n。如果 n0,则没有足够的空间在 i 处编码 u,并且 b 保持不变。否则,可以在 i + n 处编码新字符。

val is_valid_utf_16be : t -> bool

is_valid_utf_16be b 当且仅当 b 包含有效的 UTF-16BE 数据时为 true

UTF-16LE

val get_utf_16le_uchar : t -> int -> Uchar.utf_decode

get_utf_16le_uchar b i 解码 b 中索引 i 处的 UTF-16LE 字符。

val set_utf_16le_uchar : t -> int -> Uchar.t -> int

set_utf_16le_uchar b i uu 的 UTF-16LE 编码到 b 中的索引 i 处,并返回从 i 开始写入的字节数 n。如果 n0,则没有足够的空间在 i 处编码 u,并且 b 保持不变。否则,可以在 i + n 处编码新字符。

val is_valid_utf_16le : t -> bool

is_valid_utf_16le b 当且仅当 b 包含有效的 UTF-16LE 数据时为 true

整数的二进制编码/解码

本节中的函数将整数二进制编码和解码到字节序列。

如果索引 i 处解码或编码整数所需的存储空间不可用,则以下所有函数都将引发 Invalid_argument 异常。

小端序(resp. 大端序)编码意味着最低(resp. 最高)有效字节先存储。大端序也被称为网络字节序。原生端序编码可以是小端序或大端序,具体取决于 Sys.big_endian

32 位和 64 位整数分别由 int32int64 类型表示,可以解释为有符号或无符号数。

8 位和 16 位整数由 int 类型表示,该类型比二进制编码具有更多位。这些额外的位按如下方式处理

val get_uint8 : bytes -> int -> int

get_uint8 b ib 从字节索引 i 开始的无符号 8 位整数。

val get_int8 : bytes -> int -> int

get_int8 b ib 从字节索引 i 开始的有符号 8 位整数。

val get_uint16_ne : bytes -> int -> int

get_uint16_ne b ib 从字节索引 i 开始的原生端序无符号 16 位整数。

val get_uint16_be : bytes -> int -> int

get_uint16_be b ib 从字节索引 i 开始的大端序无符号 16 位整数。

val get_uint16_le : bytes -> int -> int

get_uint16_le b ib 从字节索引 i 开始的小端序无符号 16 位整数。

val get_int16_ne : bytes -> int -> int

get_int16_ne b ib 从字节索引 i 开始的原生端序有符号 16 位整数。

val get_int16_be : bytes -> int -> int

get_int16_be b ib 从字节索引 i 开始的大端序有符号 16 位整数。

val get_int16_le : bytes -> int -> int

get_int16_le b ib 从字节索引 i 开始的小端序有符号 16 位整数。

val get_int32_ne : bytes -> int -> int32

get_int32_ne b ib 从字节索引 i 开始的原生端序 32 位整数。

val get_int32_be : bytes -> int -> int32

get_int32_be b ib 从字节索引 i 开始的大端序 32 位整数。

val get_int32_le : bytes -> int -> int32

get_int32_le b ib 从字节索引 i 开始的小端序 32 位整数。

val get_int64_ne : bytes -> int -> int64

get_int64_ne b ib 从字节索引 i 开始的原生端序 64 位整数。

val get_int64_be : bytes -> int -> int64

get_int64_be b ib 从字节索引 i 开始的大端序 64 位整数。

val get_int64_le : bytes -> int -> int64

get_int64_le b ib 从字节索引 i 开始的小端序 64 位整数。

val set_uint8 : bytes -> int -> int -> unit

set_uint8 b i vb 从字节索引 i 开始的无符号 8 位整数设置为 v

val set_int8 : bytes -> int -> int -> unit

set_int8 b i vb 从字节索引 i 开始的有符号 8 位整数设置为 v

val set_uint16_ne : bytes -> int -> int -> unit

set_uint16_ne b i vb 从字节索引 i 开始的原生端序无符号 16 位整数设置为 v

val set_uint16_be : bytes -> int -> int -> unit

set_uint16_be b i vb 从字节索引 i 开始的大端序无符号 16 位整数设置为 v

val set_uint16_le : bytes -> int -> int -> unit

set_uint16_le b i vb 从字节索引 i 开始的小端序无符号 16 位整数设置为 v

val set_int16_ne : bytes -> int -> int -> unit

set_int16_ne b i vb 从字节索引 i 开始的原生端序有符号 16 位整数设置为 v

val set_int16_be : bytes -> int -> int -> unit

set_int16_be b i vb 从字节索引 i 开始的大端序有符号 16 位整数设置为 v

val set_int16_le : bytes -> int -> int -> unit

set_int16_le b i vb 从字节索引 i 开始的小端序有符号 16 位整数设置为 v

val set_int32_ne : bytes -> int -> int32 -> unit

set_int32_ne b i vb 从字节索引 i 开始的原生端序 32 位整数设置为 v

val set_int32_be : bytes -> int -> int32 -> unit

set_int32_be b i vb 从字节索引 i 开始的大端序 32 位整数设置为 v

val set_int32_le : bytes -> int -> int32 -> unit

set_int32_le b i vb 从字节索引 i 开始的小端序 32 位整数设置为 v

val set_int64_ne : bytes -> int -> int64 -> unit

set_int64_ne b i vb 从字节索引 i 开始的原生端序 64 位整数设置为 v

val set_int64_be : bytes -> int -> int64 -> unit

set_int64_be b i vb 从字节索引 i 开始的大端序 64 位整数设置为 v

val set_int64_le : bytes -> int -> int64 -> unit

set_int64_le b i vb 从字节索引 i 开始的小端序 64 位整数设置为 v

字节序列和并发安全性

在从多个域并发访问字节序列时,必须注意:访问字节序列永远不会使程序崩溃,但不同步的访问可能会导致意外(非顺序一致)的结果。

原子性

访问多个字节的每个字节序列操作都不是原子的。这包括迭代和扫描。

例如,考虑以下程序

let size = 100_000_000
let b = Bytes.make size  ' '
let update b f ()  =
  Bytes.iteri (fun i x -> Bytes.set b i (Char.chr (f (Char.code x)))) b
let d1 = Domain.spawn (update b (fun x -> x + 1))
let d2 = Domain.spawn (update b (fun x -> 2 * x + 1))
let () = Domain.join d1; Domain.join d2

字节序列 b 可能包含 '!''A''B''C' 值的非确定性混合。

执行此代码后,序列 b 的每个字节都将是 '!''A''B''C'。如果需要原子性,则用户必须实现自己的同步(例如,使用 Mutex.t)。

数据竞争

如果两个域仅访问字节序列的不同部分,则观察到的行为等同于来自两个域的操作的一些顺序交织。

当两个域在没有同步的情况下访问同一个字节,并且至少有一个访问是写入时,就会发生数据竞争。在没有数据竞争的情况下,观察到的行为等同于来自不同域的操作的一些顺序交织。

只要可能,应通过使用同步来协调对序列元素的访问来避免数据竞争。

实际上,在存在数据竞争的情况下,程序不会崩溃,但观察到的行为可能不等于来自不同域的操作的任何顺序交织。然而,即使在存在数据竞争的情况下,读取操作也会返回先前对该位置的某个写入的值。

混合大小访问

另一个微妙之处是,如果数据竞争涉及对同一位置的混合大小写入和读取,则这些写入和读取被域观察到的顺序是不确定的。例如,以下代码按顺序将一个 32 位整数和一个 char 写入同一个索引

let b = Bytes.make 10 '\000'
let d1 = Domain.spawn (fun () -> Bytes.set_int32_ne b 0 100; b.[0] <- 'd' )

在这种情况下,观察到将 'd' 写入 b.0 的域不能保证也观察到对索引 123 的写入。