Slang 序列化机制深度解析:AST/IR 模块的编码格式、RIFF 容器与跨版本兼容
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
本篇技术指南围绕 Slang 编译器仓库中的序列化(Serialization)基础设施展开,系统讲解 AST 模块、IR 模块与容器三种序列化形态分别处理什么数据、采用何种编码格式,以及 round-trip(序列化后再次反序列化)如何保持一致。文章适用于想要为序列化增加新字段、调试反序列化失败,或从事 IR 模块跨版本稳定性工作的开发者;读完你将掌握从slang-serialize.h的通用 API 设计,到 Fossil 后端、RIFF 容器、源码位置编码,再到稳定指令名与版本兼容约束的完整知识链路。
What is serialized:三种序列化形态
序列化设施覆盖编译管线中的两类核心中间表示,以及将它们打包在一起的容器层,分别由独立源文件实现:
- AST 模块:由 slang-serialize-ast.cpp(及其头文件 slang-serialize-ast.h)处理,负责把前端的抽象语法树(Decl 声明节点等)编码为字节流,并能在读取时"复活"(revitalize)出内存中的
Decl*对象。 - IR 模块:由 slang-serialize-ir.cpp 处理主体逻辑,配合 slang-serialize-ir-types.cpp(及 slang-serialize-ir-types.h)定义 IR 类型如何映射为可序列化表示。
- 容器:由 slang-serialize-container.cpp 负责 RIFF 式分块打包,将 AST/IR 模块、入口点、目标程序、文件依赖等组织进一个容器文件(即
.slang-module之类的产物)。
此外还有两个支撑文件: slang-serialize.cpp 与 slang-serialize.h 定义了贯穿全局的通用序列化 API,slang-serialize-types.cpp/slang-serialize-types.h 定义各模块共用的 FourCC 标识与数据结构(如SerialBinary常量表、ModuleChunk、EntryPointChunk)。
从容器实现看(slang-serialize-container.cpp),一个FrontEndCompileRequest会被编码为一个 RIFF 列表块(FourCC 为SLmc),内部再为每个编译单元(translation unit)编码一个模块;若涉及代码生成(未开启-skip-codegen),还会追加目标程序数组块(arry)与入口点列表块(epts),每个入口点记录 mangled 名、入口名与 profile。
Backends:两套编码后端
序列化框架的核心设计是"读与写共用同一套serialize()函数",避免读写逻辑分叉导致不一致。目前仓库中存在两个编码后端:
通用序列化(Generic serialize)
定义在 slang-serialize.h 中,是面向框架使用者的抽象层。它以ISerializerImpl虚接口(slang-serialize.h)规定后端必须支持的原语操作:
- 标量:
handleBool、handleInt8/16/32/64、handleUInt8/16/32/64、handleFloat32/64、handleString; - 容器:配对的
begin/endArray、begin/endOptional、begin/endDictionary、begin/endTuple、begin/endStruct、begin/endVariant,以及读取侧的hasElements()和字段标识handleFieldKey(name, index); - 指针:
handleUniquePtr(逻辑上等价 optional)与handleSharedPtr(写时对已出现过的指针只写额外引用,读时直接复用已读对象)、handleDeferredObjectContents(延迟序列化对象内容,用于打断对象图潜在的死循环递归)。
与后端交互时,客户端通过模板智能指针Serializer<Impl, Context>(slang-serialize.h)同时持有后端实现指针与上下文指针(上下文常用于提供工厂对象以支持去重/缓存式构造)。框架层面提供了大量serialize()重载(标量、List、定长数组、ShortList、std::optional、KeyValuePair、Dictionary、OrderedDictionary等),以及成对的 RAII 作用域类型与宏(如SLANG_SCOPED_SERIALIZER_STRUCT(serializer)),保证 begin/end 配对正确。
用户自定义类型只需实现一个读写共用的serialize()即可接入,例如:
template<typename S> void serialize(S const& serializer, MyThing& value) { SLANG_SCOPED_SERIALIZER_STRUCT(serializer); serialize(serializer, value.a); serialize(serializer, value.otherThings); serialize(serializer, value.object); }只要被引用的成员类型(如OtherThing、SomeObject)已经实现序列化支持,新类型即自动可序列化。
指针序列化是整个体系中最复杂的部分,代码采用分层定制点设计(slang-serialize.h):
serialize(s, T*&) → serializePtr → serializeSharedPtr / serializeUniquePtr → ISerializerImpl::handleSharedPtr / handleUniquePtr(回调) → serializeObject(读取时默认 new T(),可定制构造) → deferSerializeObjectContents(延迟调度) → serializeObjectContents(默认序列化 *value,可定制)这套分层同时解决了三件事:多引用对象与对象图环(依赖共享指针的引用去重与延迟内容序列化)、多态类型(通过额外传参的serializePtr重载拦截整个类型层级)、以及需要工厂函数创建的对象。
Fossil 后端
slang-serialize-fossil.cpp 与 slang-serialize-fossil.h 实现名为 Fossil 的具体后端,是 AST 与 IR 序列化实际使用的编码器。从实现看:
- 输出是一个 blob,顶层结构由**头部块(header chunk)与根值块(root value chunk)**组成(slang-serialize-fossil.cpp);头部手工写入魔数
Fossil::Header::kMagic、标志位、总 blob 大小(写完后回填)以及指向根值块的相对指针。 - 每个值都带有类型标签
FossilizedValKind(如Bool、Int8、Int32、Ptr等),写侧SerialWriter::handleXxx会以_writeSimpleValue(kind, v)落盘,读侧SerialReader对称还原。 - 类型映射通过
SLANG_DECLARE_FOSSILIZED_TYPE(T, Fossilized_T)宏与FossilizedTypeTraits<T>特化完成;例如 IR 侧Fossilized_IRModule记录模块名、模块版本号与FlatInstTable(扁平化指令表)字段(slang-serialize-ir.cpp),AST 侧则定义了Fossilized_$T模板来为每个Decl子类自动生成"化石化"表示,并利用Fossil::ReadContext维护旧指针到新对象的映射(slang-serialize-ast.cpp)。 - 枚举值经
serializeEnum<RawType = Int32>以中间整数编码(slang-serialize.h),AST 侧显式声明"serializeEnum()将枚举编码为FossilUInt"(slang-serialize-ast.cpp)。
需要说明的是:该通用抽象与 Fossil 后端的关系是"概念契约"而非强制继承——ISerializerImpl在头文件注释中明确写明"具体后端不需要继承自该类型,它仅用于定义需求"(slang-serialize.h),这样序列化函数才能针对特定数据格式做静态特化以获得最佳性能。
RIFF 容器格式
序列化产物以 RIFF 风格的分块容器组织。RIFF 的读写实现位于 source/core/slang-riff.h 与 source/core/slang-riff.cpp(注意:虽然序列化文档常把该实现记为slang-serialize-riff.cpp,当前仓库中的实际路径是source/core/slang-riff.cpp)。
核心概念是FourCC(Four Character Code):一个 32 位值、由四个 ASCII 字符构成,充当"可扩展枚举",不同开发者可独立新增取值而几乎不会碰撞,同时可被高效比较或用于 switch(source/core/slang-riff.h)。容器由三种块组成:Chunk(基类)、DataChunk(携带载荷)、ListChunk(包含子块列表),块之间存在对齐与大小校验(BoundsCheckedChunkPtr在遍历子块时做边界检查,source/core/slang-riff.cpp)。
序列化容器(slang-serialize-types.h)使用如下关键 FourCC:
| FourCC | 含义 |
|---|---|
SLmc | 顶层容器(container),对应SerialBinary::kContainerFourCc |
SLml | 翻译单元/模块列表 |
SLst | 字符串表 |
epts/EPnt | 入口点列表 / 单个入口点 |
arry/dict/pair/obj | 通用 JSON 式结构:数组、字典、键值对、对象 |
i32/u32/i64/u64/f32/f64/str/true/false/null | 通用标量 |
ast | 模块中的 AST 块(PropertyKeys<Module>::ASTModule) |
ir | 模块中的 IR 块(PropertyKeys<IRModule>::IRModule) |
SHA1 | 模块内容的 SHA1 摘要 |
fdep | 文件依赖列表 |
Sdeb/Sdst/Sdln/Sdal/Sdso | 调试与源码位置信息块(见下文) |
模块编码上下文中(slang-serialize-container.cpp)还持有跨整个容器的字符串池(StringSlicePool)与可选的SerialSourceLocWriter,最终通过RIFF::Builder落盘。
Source-location 序列化
源码位置信息由 slang-serialize-source-loc.cpp 与 slang-serialize-source-loc.h 处理。其核心数据结构SerialSourceLocData定义了多种 FourCC 块:Sdeb(调试信息根块)、Sdst(调试字符串)、Sdln(行信息)、Sdal(调整后的行信息)、Sdso(源码信息)。
SourceRange以相对偏移(m_start/m_end)记录一段源码区间,读取时通过SourceView把相对位置映射回内存中的SourceLoc(slang-serialize-source-loc.h)。写入与否是可配置的:SerialContainerUtil::WriteOptions中的sourceManagerToUseWhenSerializingSourceLocs若为空,则产物不包含源码位置信息;若非空,则必须是生成输入模块中所有SourceLoc的那个SourceManager(slang-serialize-container.h)。读侧对应的ReadOptions则携带SourceManager、NamePool、SharedASTBuilder、Linkage等用于重建环境的对象。
Versioning and backwards compatibility
序列化 IR 模块的跨版本兼容是平台级能力,权威设计文档为 docs/design/backwards-compat-for-ir-modules.md,此处给出要点。
双版本追踪
- 模块版本(
IRModule::m_version):语义化版本,表示 IR 指令集的版本,范围在k_minSupportedModuleVersion与k_maxSupportedModuleVersion之间,随每个模块写入产物。 - 序列化版本(
IRModuleInfo::serializationVersion):表示序列化格式本身的版本,当前为 0,用于未来允许序列化结构自身演进。
另外每个模块记录创建它的编译器精确版本(SLANG_TAG_VERSION),为将来按版本做兼容处理预留依据。
稳定指令名机制
其核心是稳定指令名表source/slang/slang-ir-insts-stable-names.lua:这是一个由机器生成的lua表,把指令名映射为永久整数 ID,例如["nop"] = 0、["Unrecognized"] = 1、["Type.BasicType.Void"] = 2……ID 一旦分配永久有效、绝不复用,新增指令只会拿到新 ID。运行时通过getOpcodeStableName(IROp)把运行时 opcode 映射到稳定 ID,getStableNameOpcode(UInt)反向映射,未知 ID 一律映射为kIROp_Unrecognized,保证老模块遇到新编译器不认识的新指令时能安全降级。
对slang-ir-insts.lua插入的硬约束
指令定义源文件是 source/slang/slang-ir-insts.lua,新增指令时只能在其末尾追加,绝不能在中间插入导致既有指令的相对顺序变化——因为稳定 ID 由check-ir-stable-names.lua依据指令顺序生成,只有尾插才能保证既有指令 ID 不漂移,老模块才能继续反序列化。
校验与查询工具
仓库提供了一整套配套工具:
- extras/check-ir-stable-names.lua 加载
slang-ir-insts.lua并与稳定名表对比,校验"所有指令都有稳定名、无重复 ID、稳定名表与当前指令构成双射";GitHub Actions 工作流会以 check 模式拦截不合规提交,或以 update 模式自动为新指令分配稳定 ID。 - 命令行工具:
slangc -get-module-info <module-file>在不加载模块的情况下打印其名称、模块版本与创建它的编译器版本;slangc -get-supported-module-versions打印当前编译器支持的模块版本区间。 - API:
ISession::loadModuleInfoFromIRBlob允许程序化读取模块元数据而不做完整反序列化。
Round-trip 与 repro 机制
Round-trip(写后读)对称性是整个系统的重要设计目标:Serializer读/写共用同一serialize()函数,IR 侧递归遍历在写入与读取两个方向都使用同一套代码路径(traverseInstsInSerializationOrder,slang-serialize-ir.h),并且与 IR 特化的深度预算保持一致——kMaxIRSerializationDepth = 512对两个方向同时生效,保证往返对称。序列化遍历还开启一个轻微优化kReorderInstructionsForSerialization = true:模块指令下先递归处理普通指令,把 bool/int/float/ptr/void 字面量(以及 string/blob 字面量)挪到末尾,语义不变但让读取时的控制流更易预测(slang-serialize-ir.h)。
此外 slang-serialize-container.h 提供verifyIRSerialize(),可对一个IRModule做序列化-反序列化验证,直接用于确认 round-trip 正确性。
关于 repro(复现)机制,请以 CLAUDE.md 的说明为准(CLAUDE.md):slangc的-dump-repro与-dump-ast、-dump-intermediates、-serial-ir等并列被列为"不推荐使用"的选项;而-load-repro与-extract-repro则被明确保留为专门的 repro 处理工具,其输入在使用前会经过校验。因此相关序列化产物若出现在受关注路径中,读者应知道存在这一机制,但新工作不应依赖-dump-repro。
为类型增加序列化支持的实操路径
综合上述机制,在 Slang 仓库中为一种新类型接入序列化,遵循的路径是:
- 为类型实现
template<typename S> void serialize(S const&, T&),内部用SLANG_SCOPED_SERIALIZER_STRUCT/ARRAY/DICTIONARY/...宏包裹字段序列化; - 指针成员走默认的
serializePtr → serializeSharedPtr链路即可获得引用去重与环支持;需要定制构造时,特化serializeObject(读取侧分配)并把其余成员放入deferSerializeObjectContents; - 若要写入容器产物,确认
SerialContainerUtil::WriteOptions(是否携带源码位置信息)与ReadOptions(重建所需的Session、SourceManager、NamePool等)配置正确; - 若涉及 IR 指令,在 source/slang/slang-ir-insts.lua 末尾追加定义,并运行 extras/check-ir-stable-names.lua 的 update 模式生成/更新稳定 ID;
- 用
verifyIRSerialize()或slangc -get-module-info验证往返结果与版本信息正确。
这套"读写共形 + 分层指针处理 + 稳定 ID + RIFF 打包"的设计,使 Slang 的模块产物既能高效编码复杂对象图,又能跨编译器版本稳定加载,是理解其缓存、增量编译与模块互操作能力的关键一环。
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考