首页
/ FlatBuffers Rust 使用指南:从 schema 编译、零拷贝读取到无分配构建与反射

FlatBuffers Rust 使用指南:从 schema 编译、零拷贝读取到无分配构建与反射

2026-09-10 23:30:06作者:庞眉杨Will

本指南以 docs/source/languages/rust.md 为骨架,结合 rust/flatbuffers 运行时源码与 tests/rust_usage_test 测试套件展开。文章聚焦 FlatBuffers 在 Rust 语言中的特有用法:如何用 flatc 生成 Rust 代码、如何零拷贝读取与构建缓冲区、如何处理不可信数据、如何利用 try_* API 与自定义分配器在 no_std 环境优雅处理分配失败,以及如何在延迟敏感场景下预分配内部存储避免序列化抖动。

FlatBuffers 是一种免序列化/反序列化的内存高效数据格式:数据以与内存布局一致的二进制形式存储,读取时无需解析即可直接访问字段。Rust 绑定在此基础上,把"读取只读缓冲区"与"构建缓冲区"两条路径的并发安全属性直接暴露给类型系统(Send + Sync),并提供了从"检查式安全 API"到"零检查 unsafe API"的完整梯度,供不同信任级别的数据源选用。读完本文,你将能够在自己的 Cargo 工程中完成 .fbs.rs 的代码生成、读取磁盘/网络上的 FlatBuffer 二进制、构建并写出自己的缓冲区,以及掌握分配失败处理与低延迟预分配等进阶能力。

前置条件:schema 编译与工程依赖

使用 FlatBuffers Rust 绑定的前提与其余语言一致:

  1. 编写 schema:参考 docs/source/schema.md 编写诸如 mygame.fbs 的 schema 文件(扩展名不影响)。
  2. 用 schema 编译器生成代码:参考 docs/source/flatc.md,运行 flatc --rust mygame.fbs,得到 mygame_generated.rs。Rust 代码生成器实现在 src/idl_gen_rust.cpp,其中 root_as_* 一族入口函数即由 src/idl_gen_rust.cpp#L2534-L2624 生成。
  3. 引入运行时 crate:在 Cargo.toml 中添加依赖:
[dependencies]
flatbuffers = "..."   # 对应仓库中的 rust/flatbuffers crate

运行时 crate 的入口与模块结构见 rust/flatbuffers/src/lib.rs。注意该文件顶部声明 #![cfg_attr(not(feature = "std"), no_std)],即默认启用 std,关闭 std feature 后可以面向 no_std 环境编译;此外还有 nightly(启用 error_in_coretrusted_len 等实验特性)与 serialize(为 Vector 提供 serde 序列化支持,见 rust/flatbuffers/src/vector.rs#L334-L351)等 feature。

对于尚不熟悉通用 FlatBuffers 用法的读者,docs/source/tutorial.md 提供了覆盖所有受支持语言(含 Rust)的完整入门教程,本文只讨论 Rust 特有的细节。

库代码与测试套件位置

运行测试

测试脚本 tests/RustTest.sh 需要本机安装 Rust 工具链,且部分用例(生成文件放入 OUT_DIR 的测试)要求仓库根目录存在编译好的 flatc 可执行文件——脚本中通过 if <a href="https://link.gitcode.com/i/157376115d152e4b81d04673106a4f5a" target="_blank">[ -f ../../flatc ]] 判断。构建 flatc 的方法见 [docs/source/building.md。在 Linux 上运行:

cd tests && ./RustTest.sh

脚本依次执行:serde 序列化测试(rust_serialize_test)、no_std 编译测试(rust_no_std_compilation_test,需要 nightly 工具链与 thumbv7m-none-eabi 目标)、主测试套件(cargo test,同时跑默认 feature 与 --no-default-features 两种配置)、堆分配检查(flatbuffers_alloc_checkflexbuffers_alloc_check)、clippy 检查与 cargo bench 基准测试;当环境变量 RUST_NIGHTLY=1 时还会用 miri 做未定义行为检测。

读取 FlatBuffer:第一个完整示例

Rust 绑定同时支持读取与写入。读取路径的核心思想是:把整个二进制文件读入一个 u8 向量,以字节切片形式交给生成的 root_as_monster() 之类的入口函数,返回的 Monster 直接指向缓冲区内部(根对象指针并非缓冲区起始指针,二者不同)。以下完整示例来自测试套件(仓库快照中对应 tests/rust_usage_test/tests/integration_test.rs 等测试;文档原始出处为测试套件中的 monster_example 二进制示例):

extern crate flatbuffers;

#[allow(dead_code, unused_imports)]
#[path = "../../monster_test_generated.rs"]
mod monster_test_generated;
pub use monster_test_generated::my_game;

use std::io::Read;

fn main() {
    let mut f = std::fs::File::open("../monsterdata_test.mon").unwrap();
    let mut buf = Vec::new();
    f.read_to_end(&mut buf).expect("file reading failed");

    let monster = my_game::example::root_as_monster(&buf[..]);

拿到 monster(类型为 Monster)后,生成代码为每个字段提供了便捷访问器,例如 hp()mana() 等:

    println!("{}", monster.hp());     // `80`
    println!("{}", monster.mana());   // default value of `150`
    println!("{:?}", monster.name()); // Some("MyMonster")
}

注意我们从未在缓冲区中写入 mana,因此读取到的是 schema 中定义的默认值——这正是 FlatBuffers "字段未存储时返回默认值" 的压缩策略:为节省空间,等于默认值的字段根本不会被写入缓冲区(对应构建器中的 force_defaults(false) 默认行为,见 rust/flatbuffers/src/builder.rs#L710-L720)。

从源码结构看,root_as_monster 这类入口函数由 flatc 生成,底层调用运行时 crate 的 root_with_opts / root_unchecked 等函数(见下文"不可信缓冲区"一节),而这些函数实现在 rust/flatbuffers/src/get_root.rs

Fallible API 与自定义分配器

FlatBufferBuilder 中每一个可能发生分配的方法都有对应的 try_* 版本(如 try_create_stringtry_pushtry_push_slottry_push_slot_alwaystry_end_tabletry_finishtry_create_vectortry_create_shared_string 等),它们返回 Result<T, A::Error> 而非直接 panic。这在分配失败必须被优雅处理的场景(例如 no_std 环境或固定容量缓冲区)下非常有用。全部 try_* 方法列表可查阅 FlatBufferBuilder 的 rustdoc。传统的会 panic 的方法保持不变,在默认分配器下仍然是最简单的选择。

自定义 Allocator

实现 Allocator trait 并通过 FlatBufferBuilder::new_in() 传入:

use flatbuffers::{Allocator, FlatBufferBuilder};

struct MyAllocator { /* ... */ }

unsafe impl Allocator for MyAllocator {
    type Error = MyError;
    fn grow_downwards(&mut self) -> Result<(), Self::Error> { /* ... */ }
    fn len(&self) -> usize { /* ... */ }
}

let alloc = MyAllocator::new(/* ... */);
let mut builder = FlatBufferBuilder::new_in(alloc);

Allocator trait 的定义位于 rust/flatbuffers/src/builder.rs#L48-L59:它要求 DerefMut<Target = [u8]>,关联类型 Error: Display + Debug 描述分配失败,grow_downwards 负责向下增长缓冲区(旧内容移到末尾),len 返回内部缓冲区字节数。文档注释特别提醒:如果实现不真正增长内部缓冲区,会陷入无限循环。

内置的 DefaultAllocatorVec<u8> 为后端(rust/flatbuffers/src/builder.rs#L62-L121),其 Error = Infallible(不可失败),因此默认构建器上的 try_* 方法永远不会失败——它们与对应 panic 版本行为一致,只是返回 ResultDefaultAllocatorgrow_downwards 将容量翻倍(max(1, old_len * 2)),把旧数据搬移到新缓冲末尾并把中间区域清零。

带错误传播的构建示例

fn build<A: flatbuffers::Allocator>(
    builder: &mut FlatBufferBuilder<A>,
) -> Result<(), A::Error> {
    let name = builder.try_create_string("Orc")?;
    let inventory = builder.try_create_vector(&[0u8, 1, 2, 3, 4])?;

    let table_start = builder.start_table();
    builder.try_push_slot_always(Monster::VT_NAME, name)?;
    builder.try_push_slot_always(Monster::VT_INVENTORY, inventory)?;
    builder.try_push_slot(Monster::VT_HP, 80i16, 100)?;
    let root = builder.try_end_table(table_start)?;

    builder.try_finish(root, None)?;
    Ok(())
}

其中 try_push_slot(slot, x, default) 会在 x == default 时跳过写入(配合 force_defaults(false) 实现默认值压缩);try_push_slot_always 则无条件写入并登记到正在构建的 vtable 中。对应的不可失败版本 push_slot / push_slot_always 内部就是对 try_* 版本调用 .expect("Flatbuffer allocation failure")(见 rust/flatbuffers/src/builder.rs#L360-L407)。

直接内存访问:Struct 与向量切片

如前面的示例所示,缓冲区中所有元素都通过生成的访问器访问。原因有二:其一,所有平台上数据都以**小端(little endian)**存储,访问器在大端机器上会执行字节交换(见 rust/flatbuffers/src/endian_scalar.rsEndianScalar trait 的 to_little_endian/from_little_endian 实现——整数用 to_le/from_le,浮点先 to_bits 再交换字节序);其二,布局通常对用户不可知。

但对 struct 而言,布局是确定且跨平台一致的:标量按自身大小对齐,struct 自身按其最大成员对齐。因此允许通过 safe_slice 直接访问 struct 引用(乃至 struct 数组)对应的内存。要计算 struct 子元素的偏移,应确保这些子元素本身是 struct,这样可以用指针相减得出偏移而无需硬编码——这对向 OpenGL glVertexAttribPointer 之类的 API 传入 struct 数组非常有用。

需要强调的是:struct 在所有机器上仍是小端存储,所以这类"零转换直读"的能力只在小端机器上开放。如果你同时要支持大端机器,请用 #[cfg(target_endian = "little")] 属性包裹相关代码,否则无法编译通过。

从当前仓库源码看,向量/struct 底层直接转换的原语是 rust/flatbuffers/src/vector.rs#L155-L166follow_cast_ref:它断言 T 的对齐为 1 后,把字节切片按指针转换为 &T 引用返回——这就是"跳过逐字段解析、直接取引用"的机制。文档中描述为始终可用的 safe_slice(对 struct、bool、u8、i8 的向量,其余标量类型在小端系统上条件编译)以及构建侧的 create_vector_direct(对可用 memcpy 端安全写入的类型开放,是 safe_slice 的写入端对应物),在编写本文时未在 rust/flatbuffers/src 中找到同名实现,读者如需使用请以当前 crate 文档/生成代码实际提供的 API 为准。

访问不可信缓冲区:校验器与 unchecked 梯度

Rust 绑定把 FlatBuffer 的信任模型直接编码为 API 形态:

  • 安全版本rootsize_prefixed_rootroot_with_optssize_prefixed_root_with_opts 会**先运行校验器(verifier)**再返回访问器。这有一定性能开销,但设计目标是对来自不可信来源的数据安全。实现见 rust/flatbuffers/src/get_root.rsroot_with_opts 构造 Verifier 并对根执行 run_verifier,通过后才调用内部的无检查 root_unchecked
  • unsafe 版本:名称以 _unchecked 结尾(root_uncheckedsize_prefixed_root_unchecked),跳过全部校验,可能访问任意内存,因此要求调用者保证数据确实是合法的 FlatBuffer(例如由你自己的软件构建的)。

生成的访问器通过偏移访问字段,速度极快;当前实现利用偏移直接读写内存,不再做额外的边界检查。所有安全 API 都保证在访问前已对缓冲区运行过校验器。

校验器本身位于 rust/flatbuffers/src/verifier.rs,其错误类型 InvalidFlatbufferrust/flatbuffers/src/verifier.rs#L40-L78)枚举了各类非法情形:MissingRequiredField(缺失必填字段)、InconsistentUnion(union 判别式与值不一致)、Utf8ErrorMissingNullTerminator(字符串缺少结尾空字符)、Unaligned(未对齐)、RangeOutOfBounds(越界)、SignedOffsetOutOfBounds(有符号偏移越界),以及用于防 DoS 的 TooManyTablesApparentSizeTooLargeDepthLimitReached(后三者不产生详细错误轨迹,因为轨迹本身可能很大)。ErrorTraceDetail 则记录错误发生的位置(向量元素下标、表字段名、union 变体等),方便定位。

使用建议:处理大量来自可信来源的数据(如自己生成的磁盘文件)时,_unchecked 版本可以接受;而读取可能被攻击者篡改的网络数据时,应使用带校验的安全版本。

线程安全:由类型系统强制保证

  • 读取:读取 FlatBuffer 不会触碰缓冲区之外的任何内存,完全只读(全部不可变),因此即使没有同步原语也可以从多个线程安全访问。
  • 构建:创建 FlatBuffer 不是线程安全的。所有构建状态都封装在 FlatBufferBuilder 实例内,不触碰其外部内存。要做到线程安全,要么不跨线程共享 FlatBufferBuilder(推荐),要么手动用同步原语包裹。项目有意不提供自动方案——设计者认为多线程构建单个缓冲区是罕见场景,而同步开销代价高昂。

与其他语言不同,Rust 中这些属性被类型系统直接暴露并强制:

  • flatbuffers::Table 及生成的表类型实现了 Send + Sync,意味着它们可以自由跨线程共享,任何拿到共享(&)引用的线程都能访问数据;并且不存在需要可变(独占)引用的函数,所以所有可用函数都能以共享引用调用。
  • flatbuffers::FlatBufferBuilder 同样是 Send + Sync,但其所有修改性函数都要求可变(独占)引用——这种引用只有在不存在其他引用时才能创建,既不能在同一线程内复制,更谈不上跨线程传递。

反射(Reflection)与原地调整大小

FlatBuffers 对反射提供实验性支持:即使不知道缓冲区的精确格式,也能读写其中的数据,甚至可以原地改变字符串的大小

其实现思路相当优雅:存在一个"描述 schema 的 schema"——元 schema 位于 reflection/reflection.fbs。编译器 flatc 可以把任何它刚解析过的 schema 按这个元 schema 输出为二进制 FlatBuffer(.bfbs 文件)。运行时加载这样的二进制 schema,就能遍历任何与之对应的 FlatBuffer 数据而无需预先知道精确格式:你可以查询存在哪些字段,然后读写它们。

方便起见,可以使用 flatbuffers-reflection crate(仓库中对应 rust/reflection),它既包含元 schema 的生成代码,也包含大量辅助函数。crate 根 rust/reflection/src/lib.rs 提供了两类 API:

  • Unsafe getters/settersget_any_rootget_field_integerget_field_floatget_field_stringget_field_structget_field_vectorget_field_table,以及 set_fieldset_any_field_integerset_any_field_floatset_any_field_stringset_string 等。适用于处理可信数据(来自已知来源或已通过校验)。set_stringrust/reflection/src/lib.rs#L428-L527)会在字符串变长/变短时插入/删除字节,并递归遍历缓冲区更新所有受影响的相对偏移(update_offset 函数),保持数据对齐(增量向上取整到 c_long 大小的倍数)。
  • SafeBuffer 安全读取器:位于 rust/reflection/src/safe_buffer.rsSafeBuffer::new(buf, schema) 在构造时即对整个缓冲区按 schema 运行校验(verify_with_options),之后通过 SafeTable::get_field_integerget_field_float 等按字段名取值的方法访问数据,适用于任何数据来源。字段查找利用生成代码中按 key 排序的字段向量做二分查找。

使用示例可参考 tests/rust_reflection_test/src/lib.rs

延迟敏感场景:内部向量预分配

在延迟敏感的应用中,动态内存分配会引入不可预测的延迟尖峰。FlatBufferBuilder 内部使用了多个 Vec,序列化过程中可能发生扩容重分配:

  • 承载 FlatBuffer 数据的后备缓冲区
  • field_locs:记录表内字段位置;
  • written_vtable_revpos:用于 vtable 去重;
  • strings_pool:共享字符串驻留池(std 模式下为 HashMap,O(1) 摊销查找;no_std 模式下退化为有序 Vec + 二分查找,见 rust/flatbuffers/src/builder.rs#L507-L591)。

要避免序列化过程中的分配,可以用 with_internal_capacity 一次性预分配全部内部向量:

// Preallocate: 1KB buffer, 8 field locations, 16 vtables, 32 shared strings
let mut builder = FlatBufferBuilder::with_internal_capacity(1024, 8, 16, 32);

// All subsequent operations will not allocate (if capacities are sufficient)
let name = builder.create_shared_string("MyMonster");
// ... build your FlatBuffer ...

该系列共有三个变体(实现见 rust/flatbuffers/src/builder.rs#L180-L304):

构造器 说明
with_internal_capacity(size, field_locs, vtables, strings) 新建构建器,四个参数分别为后备缓冲区初始字节数、字段位置容量、vtable 反向位置容量、共享字符串池容量
from_vec_with_internal_capacity(buffer, field_locs, vtables, strings) 复用已有的 Vec<u8> 作为后备缓冲区(会断言其长度不超过 2 GiB 的格式上限 FLATBUFFERS_MAX_BUFFER_SIZE
new_in_with_internal_capacity(allocator, field_locs, vtables, strings) 结合自定义 Allocator 使用,同时预分配内部向量

reset() 配合时,可以在一次初始化后跨多次序列化复用同一个构建器,期间零分配:

let mut builder = FlatBufferBuilder::with_internal_capacity(1024, 8, 16, 32);

loop {
    // Build a FlatBuffer (allocation-free if capacities are sufficient)
    let data = build_message(&mut builder);
    send(data);

    // Reset for reuse - clears state but retains allocated capacity
    builder.reset();
}

reset()rust/flatbuffers/src/builder.rs#L324-L338)只清零可能被污染的缓冲区区域,重置 head、清空 written_vtable_revposfield_locsstrings_pool,并把 nested/finished 标志与 min_align 复位——但保留已分配的全部容量,这正是实现分配-free 复用的关键。对应地,builder.rs 中还有专门测试 with_internal_capacity_preallocates_vecsrust/flatbuffers/src/builder.rs#L1268)验证各内部向量确实被预分配;测试套件中的 flatbuffers_alloc_check 等二进制目标则用于验证构建过程堆分配次数。

生态工具

  • flatc-rust:把 flatc 编译器封装成 API,通过 Cargo build script 透明地在构建期完成 .fbs.rs 代码生成,省去手工调用 flatc 的步骤(注意:当前仓库只读,该工具为社区项目,请以对应仓库的文档为准)。

小结

Rust 绑定为 FlatBuffers 提供了一条从安全到极致性能的完整光谱:默认的 root_as_* + 访问器适合绝大多数场景;面对网络等不可信输入,安全 root_with_optsVerifier 提供防线;对延迟敏感的热路径,with_internal_capacity + reset() 复用构建器可以做到零分配;Allocator trait 与 try_* 方法则把分配失败控制权交还给 no_std 或固定容量场景的调用者;而 Send + Sync 的类型系统约束让跨线程只读共享变得天然安全。仓库内 rust/flatbuffers/src 的源码与 tests/rust_usage_test 的集成测试、基准测试是继续深入的最佳参照。

热门项目推荐
相关项目推荐

项目优选

收起
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.15 K
2.77 K
kernelkernel
deepin linux kernel
C
34
18
docsdocs
暂无描述
Markdown
900
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
929
1.85 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.36 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.94 K
1.03 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.47 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
534
603
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
398
leetcodeleetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
Markdown
77
23