尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Slang 序列化架构深度解析:Fossil 内存映射格式、RIFF 容器与通用序列化框架

Slang 序列化架构深度解析:Fossil 内存映射格式、RIFF 容器与通用序列化框架 Slang 序列化架构深度解析Fossil 内存映射格式、RIFF 容器与通用序列化框架【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang导读本文基于 docs/design/serialization.md系统讲解 Slang 着色器编译器slang的序列化基础设施。Slang 的序列化目前处于多套子系统并存的过渡状态既有面向内存映射读取的Fossil 二进制格式也有基于RIFF 块容器的模块库、IR、SourceLoc 与 AST 序列化还有一套面向用户扩展的通用层次化序列化框架目前以 Fossil 为唯一后端。读完本文你将掌握 Fossil 格式的逐字节编码规则、RIFF 在编译器各环节的实际用途、如何用serialize()函数为自己的类型接入序列化以及 IR/AST/SourceLoc 各自的序列化策略与历史遗留问题。一、序列化子系统全景为什么会有多套方案并存原文档开篇即指出Slang 的序列化基础设施currently in flux正在演进中因此仓库里并存着多种使用不同技术手段的子系统。从代码目录 source/slang 与 source/core 可以归纳出以下几条主线子系统关键文件用途Fossil 格式source/slang/slang-fossil.h、source/slang/slang-fossil.cpp内存映射友好的通用二进制序列化格式通用序列化框架source/slang/slang-serialize.h、source/slang/slang-serialize.cpp面向用户的一个serialize()函数抽象Fossil 序列化后端source/slang/slang-serialize-fossil.h、source/slang/slang-serialize-fossil.cpp上述框架的 Fossil 实现RIFF 读写支持source/core/slang-riff.h、source/core/slang-riff.cppWAV 风格的块式容器RIFF 虚拟文件系统source/core/slang-riff-file-system.h、source/core/slang-riff-file-system.cpp基于 RIFF 的层次化虚拟文件系统AST 序列化source/slang/slang-serialize-ast.h、source/slang/slang-serialize-ast.cpp通用框架在 AST 上的应用IR / SourceLoc 序列化source/slang/slang-serialize-ir.h、source/slang/slang-serialize-ir.cpp 等更简单、更特化的 IR 与源码位置序列化原文档特别说明本文档目前仍然minimal精简主要目的是替代一份已过时的旧草案。以下内容以该文档为主体骨架并结合仓库源码对每个子系统做纵深展开。二、Fossil 格式可内存映射的通用序列化格式2.1 设计目标Fossil化石格式是一种内存可映射memory-mappable的通用序列化二进制格式。之所以命名为fossil正如 source/slang/slang-fossil.h 头注释所述它把原本活着的对象转换成一种不能再执行原功能、但仍可被检查与挖掘的替代形态。原文档列出的核心目标数据可从内存中直接读取as-is基本类型存储在自然对齐的偏移上例如 4 字节整数按 4 字节对齐指针编码为相对偏移数据加载后无需任何 relocation重定位步骤即可遍历对象图。支持通用数据包括复杂对象图。数据可携带内嵌布局信息允许代码在静态未知结构的情况下遍历数据内嵌布局信息需要支持版本化——新代码应能通过察觉哪些字段被编码、哪些没有来加载旧数据。布局信息是可选的对于已知/假定布局的代码可以用最小开销遍历数据。2.2 顶层结构Header 与根值一个 Fossil 格式的序列化 blob 以Header开头对应Slang::Fossil::HeaderHeader 指向root value根值。blob 中所有其他数据都应当能从根值出发到达应用可以自由决定根值的类型数组、结构体等。结合源码 source/slang/slang-fossil.hHeader的实布局为字段类型说明magicchar[16]标识 Fossil 格式 blob 的魔数kMagic定义其期望值totalSizeIncludingHeaderUInt64含 Header 在内的整个 blob 总字节数flagsUInt32标志位预留给未来使用rootValueFossilizedPtrFossilizedVariantObj指向对象图根值的相对指针static_assert(sizeof(Header) 32)保证头恰好 32 字节。一个值得注意的实现细节Fossil blob 只允许一个根值且根值必须是 variant变体这样它才能引用描述自身的布局信息根对象的内容可以是任意的应用可通过数组、结构体等在根上挂多个值。源码还提供了两个getRootValue重载source/slang/slang-fossil.h分别从ISlangBlob*和裸指针大小读取根对象并对 blob 做基本的尺寸/损坏校验。2.3 编码规则详解字节序Endian所有数据都按宿主机的字节序读写。当前格式没有内建字节序标记机制文档明确写到若未来需要支持大端平台应当加入 byte-order mark。这是原文档直接声明的限制在使用该格式做跨平台二进制交换时需要特别注意。基本类型Fixed-Size Types基本类型定宽整数、浮点数按原样编码N 字节的值直接存储为 N 字节数据并保持 N 字节对齐。布尔值编码为 8 位无符号整数取值仅为 0 或 1。源码中FossilizedSimpleValT, Kind是标量值的统一封装source/slang/slang-fossil.h通过SLANG_DECLARE_FOSSILIZED_SIMPLE_TYPE宏为int8_t到double全部注册并有一系列static_assert验证sizeof(Fossilizedint8_t) 1、sizeof(Fossilizedfloat) 4等尺寸约束。布尔类型不直接以自身布局序列化跨目标平台布局不保证一致而是退化为底层uint8_t并在读取时转换Fossilized_bool见 source/slang/slang-fossil.h。指针Pointers指针编码为4 字节有符号整数表示相对偏移偏移值为 0 → 空指针偏移值非 0 → 将该值与指针自身所在偏移相加得到目标偏移。源码中FossilizedPtrT派生自RelativePtr32Tsource/slang/slang-fossil.hstatic_assert(sizeof(FossilizedPtrvoid) sizeof(uint32_t))确认其恰好 32 位。这正是紧凑 无需重定位的关键32 位相对指针让格式在 64 位平台上也能保持紧凑。Optionals可选值std::optionalT编码为指向T的指针指针为空 → 无值否则值存储在指针指向的偏移处。需要注意当编码指向 optional 的指针std::optionalT*或optional 指针std::optionalT*时会产生两层间接。源码中FossilizedOptional封装了一个FossilizedPtrTFossilizedOptionalObjBase也注明absent optional 编码为空指针present 值编码为指向该值的指针因此持有值就位于this地址上source/slang/slang-fossil.h。记录Records概念上类似struct/tuple 的类型编码为记录即一串字段fields记录的对齐 其所有字段对齐的最大值字段顺序排布每个字段取前一个字段之后的下一个合适对齐的偏移**不做填补空洞**的优化。源码中的FossilizedRecordLayoutsource/slang/slang-fossil.h包含kind、fieldCount以及跟随其后的FossilizedRecordElementLayout数组每个元素含字段类型的相对指针 字段在记录内的偏移与文档描述一一对应。一个文档明确指出的已知缺陷目前记录的大小不会向上取整到对齐的倍数因此可能出现某个字段落在前一个字段的 tail padding 里文档认为这个行为应当被修正使 fossil 布局更接近 C/C 编译器通常的排布方式。变长类型Variable-Size Types不同类型的实例占用字节数可能不同编码方式分为两种当变长类型V被指针或 optional 引用V*、std::optionalV时内联编码在该指针/optional 的目标地址处在其他所有上下文包括作为记录的字段中间接编码概念上等同于字段实际是V*。间接编码时空指针解释为该类型V的一个空实例。数组ArraysT的数组编码为一系列T值元素之间以strideT的大小向上取整到T对齐分隔数组的偏移即首元素的偏移。元素个数编码为 4 字节无符号整数紧挨在数组偏移之前。源码中FossilizedContainerObjBase的实现source/slang/slang-fossil.h正是从this地址向前读一个FossilUInt得到元素个数。字符串Strings字符串按 8 位字节数组的同样方式编码首元素之前存计数唯一额外要求是序列化数据必须在最后一个元素之后额外包含一个 nul 字节。数据假定为 UTF-8 编码但格式本身不校验、不强制这一点。源码中FossilizedStringObj存储字节大小 nul 结尾字节序列source/slang/slang-fossil.h并提供getSize()/get()返回UnownedTerminatedStringSlice。字典Dictionaries键类型K、值类型V的字典编码方式与P的数组相同其中P是K与V的二元组。当前格式没有为字典提供高效查找的机制遍历即可。源码中FossilizedDictionaryObjBase与FossilizedContainerObjBase同构FossilizedKeyValuePairK, V就是键值二元组source/slang/slang-fossil.h。变体Variantsvariant是能够描述自身布局的化石值持有类型T的 variant 内容编码方式与单字段T记录完全一致从 variant 自身偏移开始variant前面 4 字节存储一个相对指针指向T的 fossil 布局。源码FossilizedVariantObjsource/slang/slang-fossil.h明确在this地址之前有一个指向内容布局的相对指针内容从this地址开始。这使 variant 成为self-describing自描述值——仅凭数据和布局即可动态检查自身结构。2.4 布局信息Layouts每个布局以 4 字节无符号整数开头存放类型标签取值见Slang::FossilizedValKindsource/slang/slang-fossil.h。枚举包含Bool、Int8…UInt64、Float32/Float64、StringObj、ArrayObj、OptionalObj、DictionaryObj、Tuple、Struct、Ptr、VariantObj。标签决定标签之后跟随哪些信息。在任何期待布局相对指针的位置空指针都表示布局信息未知或已从化石数据中省略。三种布局的详细编码布局类型标签之后的内容指针类T*、OptionalT指向T布局的相对指针FossilizedPtrLikeLayout容器类数组、字典指向元素类型布局的相对指针 4 字节无符号整数elementStrideFossilizedContainerLayout记录类FossilizedRecordLayout4 字节无符号整数字段数NN个 8 字节字段描述每个含字段类型的相对指针 4 字节无符号字段偏移Fossil::ValRefT/Fossil::ValPtrTsource/slang/slang-fossil.h是值引用/指针 布局信息的动态遍历 APIAnyValRef ValRefvoid表示未知类型的化石值 其布局配合ValRefFossilizedContainerObjBase、ValRefFossilizedRecordVal等特化即可在不静态知道类型的情况下遍历数组、记录、optional 等结构。2.5 让 C 类型化石化类型特质与宏仓库为如何把一个 C 类型映射为化石表示提供了完整机制FossilizedTypeTraits见 source/slang/slang-fossil.h默认情况下未特化的类型以不透明FossilizedOpaqueVal形式化石化SLANG_DECLARE_FOSSILIZED_AS(TYPE, FOSSILIZED_AS)声明某类型复用另一个类型的化石表示SLANG_DECLARE_FOSSILIZED_AS_MEMBER(TYPE, MEMBER)聚合类型化石化为其某个成员SLANG_DECLARE_FOSSILIZED_SIMPLE_TYPE标量映射SLANG_DECLARE_FOSSILIZED_ENUM(TYPE)枚举默认按 32 位有符号整数化石化内置映射覆盖String、ListT、ShortListT,N、定长数组T[N]、DictionaryK,V、OrderedDictionaryK,V、std::optionalT、std::pairK,V、KeyValuePairK,V、裸指针T*、RefPtrT等常见类型。三、RIFF 支持代码source/core/slang-riff.h 与 source/core/slang-riff.cpp 实现了读写RIFF 结构文件的抽象。RIFF 是一种简单的块chunk式文件格式WAV 文件即为其典型使用者并启发了媒体/游戏领域的大量类似容器格式。原文档对当前 RIFF 实现给出了坦诚的评估当前实现试图正确地遵循 RIFF 在其他地方如.wav的用法但不确定这个选择是帮助还是损害如果继续使用很可能会定制该格式至少会提高 chunk 的最小对齐。RIFF 结构目前在 Slang 中服务于以下场景模块库module libraries序列化文件的顶层结构让编译器能导航相关结构、只提取所需部分例如只取模块的 digest而不必加载 AST 或 IR——这是利用 RIFF块可定位特性的典型场景Repro复现文件使用顶层 RIFF 容器但只是封装单个原始数据 blob内部是基于偏移的指针IR 与SourceLoc序列化格式用 RIFF chunk 作为顶层结构但并未真正利用其在内存中导航或随机访问的能力序列化 AST 格式目前是深层嵌套的 RIFF chunk 层次结构RIFF 虚拟文件系统source/core/slang-riff-file-system.h用于序列化的 core module文档推测之所以用它只是因为包含 LZ4 支持——实际被序列化的文件系统里似乎只有一个文件。四、通用层次化数据序列化框架4.1 设计理念一个serialize()函数搞定读写source/slang/slang-serialize.h 实现了意在轻量易用、又能扩展到 AST 序列化这类复杂场景的序列化框架。其最核心的设计决策是一个类型的读取与写入共用同一个serialize()函数从而保证读写两侧天然一致。原文档给出的最小示例——声明一个类型struct MyThing { float f; ListOtherThing others; SomeObject* obj; };只需编写void serialize(Serializer const serializer, MyThing value) { SLANG_SCOPED_SERIALIZER_STRUCT(serializer); serialize(serializer, value.f); serialize(serializer, value.others); serialize(serializer, value.obj); }只要OtherThing和SomeObject已经具备各自的序列化支持这就够了。当然深入细节与困难场景后还有很多内容文档建议以 source/slang/slang-serialize.h 为最佳学习入口。4.2 序列化模式与后端接口框架用SerializationMode枚举区分读写Read/Write见 source/slang/slang-serialize.h并提供isReading()/isWriting()便捷判断。ISerializerImplsource/slang/slang-serialize.h是后端必须支持的操作集合标量/字符串handleBool、handleInt8/16/32/64、handleUInt8/16/32/64、handleFloat32/64、handleString容器beginArray/endArray、beginOptional/endOptional、beginDictionary/endDictionary、beginTuple/endTuple、beginStruct/endStruct、beginVariant/endVariant配套hasElements()与handleFieldKey(name, index)指针handleUniquePtr逻辑上类似 optional、handleSharedPtr可被多次引用写入时去重、读取时复用、handleDeferredObjectContents延迟序列化避免循环引用导致无限递归。关键设计具体后端并不强制继承ISerializerImpl——它只是用来描述需求的占位接口。为了性能序列化函数会被静态特化到具体格式。后端通过Scope对象void* storage[8]保存 begin/end 之间的层次状态避免自行维护堆分配栈。4.3 客户端视角Serializer智能指针大多数客户端代码并不直接操作后端而是通过SerializerImpl, Context这一双指针智能指针source/slang/slang-serialize.hImpl指向ISerializerImpl派生的后端Context指向携带额外上下文的对象例如反序列化时需要的工厂对象。命名空间级辅助函数getMode、isReading、isWriting、hasElements、各标量serialize重载将用户代码与后端细节隔离开。serializeEnumRawType还支持枚举经中间整数类型转换序列化默认按Int32。4.4 RAII 作用域宏框架为每个 begin/end 操作定义了 RAII 类型与宏source/slang/slang-serialize.h宏对应 RAII 类型用途SLANG_SCOPED_SERIALIZER_ARRAYScopedSerializerArray数组SLANG_SCOPED_SERIALIZER_DICTIONARYScopedSerializerDictionary字典SLANG_SCOPED_SERIALIZER_OPTIONALScopedSerializerOptional可选值SLANG_SCOPED_SERIALIZER_STRUCTScopedSerializerStruct结构体SLANG_SCOPED_SERIALIZER_TUPLEScopedSerializerTuple元组SLANG_SCOPED_SERIALIZER_VARIANTScopedSerializerVariant变体这些 RAII 类型负责正确分配/持有Scope并把同一Scope传入 begin 与 end保证配对正确即使中途异常退出析构也会调用 end。容器List、定长数组、ShortList、std::optional、KeyValuePair、std::pair、Dictionary、OrderedDictionary的serialize重载都在 source/slang/slang-serialize.h 中给出读写分支通过isWriting区分。4.5 指针序列化的分层处理指针是整个系统最复杂的部分需要应对多引用/循环对象图、多态类型Derived*经Base*序列化、需要工厂函数创建对象的类型。框架将其拆分为多层source/slang/slang-serialize.hserialize(s, T*)拦截指针类型分发到serializePtr可对整个类型层级做重载拦截serializePtr通常调用serializeUniquePtr或serializeSharedPtr后者调用后端handleUniquePtr/handleSharedPtrshared 情形会处理空指针与已见指针去重回调最终调用serializeObject(s, v, (T*)nullptr)——这是又一个定制点默认实现读取时new T()需要复杂创建逻辑的类型应在此定制serializeObject应只序列化分配对象所需的最少成员然后调用deferSerializeObjectContents()调度剩余内容配合handleDeferredObjectContents从而保证对象图中的循环不会造成问题serializeObjectContents是最后的定制点默认直接serialize(serializer, *value)。4.6 Fossil 后端与历史注记source/slang/slang-serialize-fossil.h 与 source/slang/slang-serialize-fossil.cpp 是该通用框架的 Fossil 实现读写前文所述的 fossil 格式。框架的一个关键目标是序列化格式可以被替换而不影响各类型的serialize()函数。目前 Fossil 是唯一实现。原文档还给出了一条历史注记曾存在第二个后端slang-serialize-riff.{h,cpp}把值直接编码为 RIFF chunk实践上类似以 RIFF 编码的 JSON它是为与 Fossil 序列化对比而写的在对比中落败后被作为死代码删除如未来需要可从 git 历史恢复。另外Fossil 后端还提供了编译期可选的校验SLANG_ENABLE_VALIDATION_FOSSIL宏默认 0见 source/slang/slang-fossil.h控制是否编译反序列化校验——该校验可防护畸形输入但会拖慢热路径如从slang.dll加载受信任的 core module。五、AST 序列化通用框架的应用AST 序列化是通用框架的一个应用实例。文档指出存在一个ASTSerializer类型它在Serializer之上扩展了处理 AST 相关类型SourceLoc、Name、NodeBase层级所需的额外上下文。从源码看AST 序列化上下文由ASTSerialWriteContextsource/slang/slang-serialize-ast.cpp承载它持有SerialSourceLocWriter*以便把SourceLoc信息交给统一的 SourceLoc 写入器source/slang/slang-serialize-ast.h 中的入口函数同样接收SerialSourceLocWriter*参数。序列化的 AST 在磁盘上体现为深层嵌套的 RIFF chunk 层次结构见前文 RIFF 部分。六、IR 序列化更简单、更特化的机制IR 序列化比通用序列化简单得多因为 IR 类型在设计上非常同质。原文档指出一条指令一般由以下部分组成它的类型type一个SourceLoc0 或多个操作数operands0 或多个子指令children由于 IR 指令内是指向IRInst派生类型的指针而直接序列化指针通常不是好主意因此采取以下策略指针转为 32 位索引利用一条指令最多只能属于一个父指令的性质序列化时对子指令做特殊处理某个父的所有子指令的索引被排成连续区间且顺序与父中持有的子顺序一致。这样不必直接保存哪些索引属于哪个父只需保存索引区间即可序列化机制与通用机制类似——被引用的对象按索引顺序保存不同之处在于编码把Inst的大小固定为IRSerialData它最多容纳两个操作数若指令操作数超过两个则其中一个UInt32是操作数个数、另一个是操作数列表的偏移。文档建议未来可以改为直接流式写入指令负载IR 序列化支持一种简单压缩机制因为 IR 序列化数据大部分是UInt32可以使用可变字节编码variable byte encoding。七、SourceLoc 序列化跨机制的共享方案SourceLoc序列化有两个棘手之处两个不同的序列化机制都要用到它——IR 序列化和通用AST序列化都会引用SourceLoc因此它不能直接内嵌进两者之一需要保证在 AST/IR 反序列化开始前SourceLoc 信息已经就位。当前方案原文档描述IR 与通用序列化的写入端都把 SourceLoc 信息累积进SerialSourceLocWriter随后把这份信息保存为一个RIFF section该 section 可在通用或 IR 反序列化之前加载读取时必须先定位并反序列化 SourceLoc 信息再开始任何 AST/IR 反序列化SourceLoc数据随后转为SerialSourceLocReader要么设置在SerialReaders的SerialExtraObjects上要么传给IRSerialReader。源码中source/slang/slang-serialize-container.cpp 的序列化容器上下文持有RefPtrSerialSourceLocWriter并负责创建它印证了写入端统一收集 SourceLoc的实现方式。八、旧序列化系统及其遗留原文档指出旧的序列化系统大部分已被移除但仍有一些痕迹可见。旧系统的特征依赖一套广泛的 RTTI 系统类型必须向它注册配合一套从 AST 节点 C 声明生成的样板宏其理念是要序列化用户 C 类型Foo需要手工编写对应的 C 类型SerialFooData再编写Foo↔SerialFooData的转换代码以及SerialFooData与真实序列化数据格式之间的读写代码。IR 与SourceLoc的序列化方式目前仍深受旧系统影响且仍残留着为支持旧系统引入的 RTTI 基础设施文档的希望是随着更多子系统迁移到更新的序列化方法这些代码终将被彻底清除。文档末尾的 IR / SourceLoc 两节即属于尚未重新审视的旧格式描述。九、总结与展望综合原文档与仓库源码Slang 的序列化版图可以概括为一条清晰的演进路径Fossilsource/slang/slang-fossil.h提供了面向内存映射、相对指针、可选内嵌布局的底层二进制格式是当前推荐的数据编码基础通用序列化框架source/slang/slang-serialize.h以一个serialize()函数统一读写通过ISerializerImpl定义后端契约、RAII 作用域宏保证配对、分层指针处理解决对象图难题目前以Fossil为唯一后端RIFFsource/core/slang-riff.h充当模块库、Repro、IR/SourceLoc、序列化 AST 与 core module 虚拟文件系统的容器层AST 序列化source/slang/slang-serialize-ast.cpp是通用框架的落地应用IR 与 SourceLoc 序列化仍是旧体系影响下的特化方案正在等待被新方法接管。对开发者而言最实用的接入路径是为新类型实现serialize()函数并配合SLANG_SCOPED_SERIALIZER_*宏即可获得跨格式当前为 Fossil的序列化能力而深入理解 Fossil 的编码规则相对指针、对齐、变长类型的间接编码、变体的自描述布局则是调试序列化数据或在其他语言/平台上实现兼容读写的关键。【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表