FlatBuffers C++ 如何直接访问 struct 内存并对接 OpenGL 顶点数组?
【免费下载链接】flatbuffersFlatBuffers: Memory Efficient Serialization Library项目地址: https://gitcode.com/GitHub_Trending/fl/flatbuffers
如果你的 C++ 程序把顶点数据(比如Vec3这类定长结构)序列化进 FlatBuffers,并想在渲染时跳过解包步骤、直接把缓冲区内的 struct 数组指针交给 OpenGL 的glVertexAttribPointer,那么需要满足 FlatBuffers 官方文档给出的几个前提:struct 的内存布局在所有平台上确定且一致,且你运行在小端机器上。本文基于仓库内 C++ 语言指南 的 "Direct memory access" 一节、schema 文档 的 struct 说明,以及 monster.fbs 这个随仓库自带的示例 schema,给出从建 schema 到取指针传给 OpenGL 的完整路径。
前提:schema 中用 struct 描述顶点,而不是 table
FlatBuffers 中 table 的字段按偏移访问、布局对用户不透明,不能直接取成员内存;而 struct 可以。根据 schema 文档:
- struct 只能包含标量或其他 struct,字段全部必填、没有默认值;
- struct 永远内联存储(inline)在父对象中,不使用虚表,比 table 占内存少、访问更快;
- struct 一旦定义就不能加字段或弃用字段——所以只适合你确信"永远不会改"的数据,顶点坐标正好符合这一条。
仓库示例 monster.fbs 里的Vec3就是典型的顶点 struct,并且Monster表同时演示了两种持有方式:单个内联字段pos:Vec3和 struct 向量path:[Vec3]:
// 来自 samples/monster.fbs struct Vec3 { x:float; y:float; z:float; } table Monster { pos:Vec3; ... path:[Vec3]; } root_type Monster;另外,struct 还支持定长数组的简写语法(当前只有 struct 支持数组),struct Vec3 { v:[float:3]; }与上面三个 float 字段的写法二进制等价。顶点数据如果带法线、UV 等多组分量,可以用这种写法合并。
用 flatc 生成 C++ 头文件,例如:
flatc -c monster.fbs会生成monster_generated.h(仓库里已包含 monster_generated.h 可直接对照)。生成的Vec3长这样:
// 摘自 samples/monster_generated.h FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(4) Vec3 FLATBUFFERS_FINAL_CLASS { private: float x_; float y_; float z_; public: Vec3() : x_(0), y_(0), z_(0) {} Vec3(float _x, float _y, float _z) : x_(::flatbuffers::EndianScalar(_x)), y_(::flatbuffers::EndianScalar(_y)), z_(::flatbuffers::EndianScalar(_z)) {} float x() const { return ::flatbuffers::EndianScalar(x_); } // y(), z() 同理 }; FLATBUFFERS_STRUCT_END(Vec3, 12);FLATBUFFERS_MANUALLY_ALIGNED_STRUCT会关掉编译器自行添加的填充、按 FlatBuffers 规则强制对齐,保证该 struct 布局与编译器和平台无关;访问器里的EndianScalar负责标量字节序转换。struct 的整体大小固定为 12 字节(3 个 float)。
直接访问内存的官方规则与硬性限制
C++ 语言指南 "Direct memory access" 一节的原意是:由于所有东西在 wire 格式上都是小端、且普通 table 布局对用户不透明,所以默认必须走生成的访问器;但 struct 是例外——
- struct 的布局是确定的(deterministic),并且在各平台保证一致:标量按自身大小对齐,struct 整体按最大成员对齐;
- 官方明确允许对 struct 指针(包括 struct 数组)直接
sizeof()和memcpy; - 要计算 struct 内部子元素的偏移时,文档建议让子元素本身也是 struct,这样可以直接用取地址的指针相减得到偏移,不需要硬编码数值。这正是"用 arrays of structs 对接
glVertexAttribPointer之类的 OpenGL 调用"的用法,文档原文点名了这个场景; - 限制:struct 内存永远是按小端存储的。只有在你能确定不会在大端机器上运行时才可用这种技巧,文档建议加一条断言:
assert(FLATBUFFERS_LITTLEENDIAN)。FLATBUFFERS_LITTLEENDIAN由 include/flatbuffers/base.h 在编译期按编译器宏推导(GCC/Clang 下看__BYTE_ORDER__,MSVC 下_M_PPC定义为 0,否则为 1)。
注意另一条隐含前提:读到的根对象指针(GetMonster返回的指针)指向的是缓冲区内部,不是独立拷贝。把 struct 数组指针交给 OpenGL 之前,必须保证源缓冲区(你new char[length]出来的那块内存或文件映射)仍然存活,文档中对 root 指针与buffer_pointer的区分就是在这个意义上强调的。
构造包含 struct 数组的 FlatBuffer
用 monster.fbs 作为 schema 时,C++ 侧构造path字段(struct 向量)的代码见 tutorial:
#include "flatbuffers/flatbuffers.h" #include "monster_generated.h" using namespace MyGame::Sample; FlatBufferBuilder builder(1024); // 构造一个 Vec3 数组 Vec3 points[] = { Vec3(1.0f, 2.0f, 3.0f), Vec3(4.0f, 5.0f, 6.0f) }; // 序列化为 struct 向量 flatbuffers::Offset<flatbuffers::Vector<Vec3>> path = builder.CreateVectorOfStructs(points, 2); // 继续创建 Monster(pos 直接传 Vec3 指针内联存储),Finish 得到根偏移 auto monster = CreateMonster(builder, &points[0], // pos:Vec3 150, 100, // mana, hp builder.CreateString("Orc"), flatbuffers::Offset<flatbuffers::Vector<uint8_t>>(), 0, flatbuffers::Offset<flatbuffers::Vector<Weapon>>(), Equipment_NONE, path); builder.Finish(monster);如果数据来自已有的二进制文件(例如由flatc -b monster.fbs monsterdata.json生成,或仓库自带的 monsterdata_test.mon),读取方式同样来自 C++ 语言指南:
std::ifstream infile("monsterdata_test.mon", std::ios::binary | std::ios::in); infile.seekg(0, std::ios::end); int length = infile.tellg(); infile.seekg(0, std::ios::beg); char *data = new char[length]; // 注意:此内存要活到 OpenGL 使用完 struct 数组为止 infile.read(data, length); infile.close(); Monster *monster = GetMonster(data);pos是 struct 字段时monster->pos()返回const Vec3 *,本身就指向缓冲区内的 12 字节内联数据;path是向量字段时monster->path()返回const Vector<Vec3> *,其元素连续排布,就是一个标准的 arrays of structs。
取 struct 数组指针并对接 glVertexAttribPointer
从缓冲区拿到 struct 数组后,直接访问不再经过访问器逐字段读取:
const flatbuffers::Vector<Vec3> *path = monster->path(); if (path) { // path->data() 即第一个 Vec3 的内存,(float*)path->data() 就是顶点坐标数组 const float *verts = reinterpret_cast<const float *>(path->data()); uint32_t count = path->size(); // 需要按成员取偏移(如分离式属性、法线数组)时, // 用指针相减代替硬编码偏移量: uint8_t *base = const_cast<uint8_t *>(path->data()); size_t off_y = (uintptr_t)(&const_cast<Vec3 *>(path->data())->y()) - (uintptr_t)base; glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vec3), verts); }各值的来源与替换方式:
verts、count、off_y都来自上面读到的缓冲区,无外部输入;sizeof(Vec3)为 12,由生成的FLATBUFFERS_STRUCT_END(Vec3, 12)保证,等价于文档所说对 struct 指针使用sizeof();off_y展示了文档建议的"用指针计算子元素偏移"的做法,如果你的 struct 成员更多(法线、UV 都是独立字段或独立 struct),对每个字段照此取址即可,不要手写 0/4/8 这类魔数;glVertexAttribPointer调用本身属于 OpenGL 侧接口,FlatBuffers 文档只点明了这种对接方式,具体location、GL_FLOAT等参数按你的渲染需求填写;- 使用前加
assert(FLATBUFFERS_LITTLEENDIAN),确认运行在小端平台。
一个更完整的端到端参照实现是仓库的 sample_binary.cpp,它演示了加载.mon文件、GetMonster之后逐字段读取的完整流程,你只需把读取pos的部分换成上面对path数组的处理。
验证与边界
- 验证读取是否成功:调用生成的访问器本身,如
monster->path()非空且monster->path()->size()与写入的 struct 数量一致(上面例子为 2);对不受信任来源的缓冲区,可先跑Verifier verifier(buf, len); VerifyMonsterBuffer(verifier);,ok为真才继续访问,这是 C++ 语言指南给出的标准验证入口。 - 边界一:这条"直接取指针"的路径只对 struct 成立。
inventory、name、weapons这类 table/string/vector 字段仍必须通过访问器读取,不能把monster指针当普通 C 结构体用。 - 边界二:struct 字段不可增删。将来顶点格式要改(加切线、改精度),只能换 schema 重新生成数据;需要演进的数据结构应留在 table 层。
- 边界三:大端平台不可用(
FLATBUFFERS_LITTLEENDIAN == 0时assert会触发),这与 base.h 中该宏的推导一致。
延伸阅读
- C++ 语言指南 中 "Direct memory access" 与 "Access of untrusted buffers" 两节:前者是本文依据,后者说明 verifier 的用法与开销。
- tutorial 的 "Vectors" 小节:
CreateVectorOfStructs的完整多语言对照。 - internals:展示生成头文件中
Vec3与Monster的完整形态,理解FLATBUFFERS_MANUALLY_ALIGNED_STRUCT如何关闭编译器填充、手动加对齐。
【免费下载链接】flatbuffersFlatBuffers: Memory Efficient Serialization Library项目地址: https://gitcode.com/GitHub_Trending/fl/flatbuffers
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考