ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpacetimeDB C 绑定全解析:从 BSATN 序列化、Roslyn 源码生成器到 WebAssembly 模块运行时

SpacetimeDB C 绑定全解析:从 BSATN 序列化、Roslyn 源码生成器到 WebAssembly 模块运行时 SpacetimeDB C# 绑定全解析从 BSATN 序列化、Roslyn 源码生成器到 WebAssembly 模块运行时【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB导读本篇文章以 SpacetimeDB 仓库中 crates/bindings-csharp/README.md 为骨架系统讲解 SpacetimeDB 的 C# 技术栈BSATN.Codegen与BSATN.Runtime如何让任意 C# 类型自动获得 BSATN 自描述与序列化能力同时服务于 C# 模块与 C# 客户端Codegen与Runtime又如何支撑用 C# 编写 SpacetimeDB 服务端模块并编译为符合 FFI ABI 的 WebAssembly 产物。读完本文你将理解[SpacetimeDB.Type]、[SpacetimeDB.Table]、[SpacetimeDB.Reducer]三个核心特性背后的生成机制掌握零尺寸BSATN序列化器结构的设计动机并了解模块运行时如何通过 WASI 与原生绑定打通 Mono 与 WebAssembly FFI。稳定性提示README 明确指出该项目接口不稳定可能在没有通知的情况下变更。面向最终用户的稳定文档应参考官方 C# 模块库参考与 C# 客户端 SDK 参考本文所涉内容均以当前仓库代码为准。仓库概览四个 C# 工程两条使用路径bindings-csharp目录下共有四个核心工程README 将它们清晰地划分为两条使用路径工程职责使用方BSATN.CodegenRoslyn 增量源码生成器为标注[SpacetimeDB.Type]的类型生成自描述与序列化代码C# 模块、C# 客户端BSATN.RuntimeBSATN 序列化的接口定义、宽整数类型、特殊类型与基础序列化器实现C# 模块、C# 客户端CodegenRoslyn 增量源码生成器为[SpacetimeDB.Table]、[SpacetimeDB.Reducer]生成注册与 FFI 包装代码仅 C# 模块RuntimeSpacetimeDB WebAssembly 模块的运行时绑定FFI、事务上下文、表访问器、HTTP 处理器等仅 C# 模块四个工程共用一个解决方案文件 SpacetimeSharpSATS.sln并共享 Directory.Build.props 中的统一编译配置。简言之前两个工程解决数据如何序列化、如何描述的问题后两个工程解决模块如何注册、如何与宿主交互的问题。[SpacetimeDB.Type]让任意 C# 类型自描述并可序列化BSATN.Codegen中的Type.cs实现了核心增量源码生成器Type : IIncrementalGenerator见 crates/bindings-csharp/BSATN.Codegen/Type.cs。它的入口通过ForAttributeWithMetadataName(SpacetimeDB.TypeAttribute, ...)监听所有标注了[SpacetimeDB.Type]的语法节点并分两条流水线处理枚举EnumDeclarationSyntax只做诊断检查不生成代码其余类型解析字段、生成序列化与自描述扩展。被标注的类型将自动获得以下能力正确实现的Equals、GetHashCode、ToStringBSATN 序列化/反序列化代码AlgebraicType自描述GetAlgebraicType(ITypeRegistrar registrar)返回该类型的代数类型描述用于模块初始化时向 SpacetimeDB 注册表结构。必须声明为partial任何[SpacetimeDB.Type]都必须标记为partial因为生成器需要在另一个 partial 声明中追加成员。README 中的示例[SpacetimeDB.Type] partial struct Banana { public int Freshness; public int LengthMeters; }从源码看生成器对类型种类还有细分基类为SpacetimeDB.TaggedEnumVariants的类型被判定为TypeKind.Sum和类型/标记联合其余为TypeKind.Product积类型/结构体对应sats中的代数类型模型。字段发现规则只序列化特性所在 partial 声明中的字段BaseTypeDeclaration构造函数crates/bindings-csharp/BSATN.Codegen/Type.cs中有一段值得注意的实现细节字段发现并非简单调用type.GetMembers()而是通过SpacetimeDbFieldDiscovery.GetFieldsDeclaredInAnnotatedPartial遍历标注了特性所在的那份 partial 声明语法树只收集其中声明的非静态字段。这是刻意设计用户可以在额外的 partial 声明中追加自己的ExtraField生成器会忽略这些非 BSATN 字段避免干扰表结构定义。零尺寸BSATN结构体与IReadWriteT每个[SpacetimeDB.Type]会生成一个名为BSATN的成员结构体它实现SpacetimeDB.BSATN.IReadWriteT接口void Example(System.IO.BinaryReader reader, System.IO.BinaryWriter writer) { Banana.BSATN serializer new(); Banana banana1 serializer.Read(reader); // 从 reader 读取 BSATN 编码的 Banana Banana banana2 serializer.Read(reader); Console.Log($bananas: {banana1} {banana2}); Console.Log($equal?: {banana1.Equals(banana2)}); serializer.Write(writer, banana2); // 将 BSATN 编码的 Banana 写入 writer serializer.Write(writer, banana1); }由于Banana.BSATN不含任何实例字段分配一个它是零成本的。README 解释了这一模式的原因当前目标 C# 版本不支持静态接口方法因此用零尺寸结构体来携带序列化实现。IReadWriteT接口见 crates/bindings-csharp/BSATN.Runtime/BSATN/Runtime.cs定义三个方法T Read(BinaryReader reader)读取一个 BSATN 编码的值并将 reader 推进到该值末尾void Write(BinaryWriter writer, T value)写出一个 BSATN 编码的值AlgebraicType GetAlgebraicType(ITypeRegistrar registrar)返回类型元数据用于模块初始化。IStructuralReadWrite免序列化器的直接读写对于不继承TaggedEnum的[SpacetimeDB.Type]生成器额外实现IStructuralReadWrite接口允许在不需要BSATN结构体的情况下直接读写字段void Example(System.IO.BinaryReader reader, System.IO.BinaryWriter writer) { Banana banana new(); // 具有默认字段值 banana.ReadFields(reader); // 现在它已被初始化 banana.WriteFields(writer); // 也可以直接写出 }TaggedEnum不能实现该接口因为反序列化之前其具体变体类型未知。从IStructuralReadWrite的源码注释可以了解到另一层性能考量直接调用生成的ReadFields避免了对泛型方法的反射调用——IStructuralReadWrite.ReadT使用Activator.CreateInstance创建实例而手工单态化的生成代码用new()初始化在 Mono/IL2CPP 下更快。C# 中的标记联合TaggedEnumVariantsREADME 展示了如何在 C# 中模拟 Rust 风格的枚举[SpacetimeDB.Type] partial record OptionT : SpacetimeDB.TaggedEnum(T Some, Unit None);生成器将命名元组(T Some, Unit None)的每个元素展开为继承的自定义 recordOption.Some(T Some_)与Option.None(Unit None_)从而可以利用 C# 模式匹配Optionint opt new Option.Some(42); var value opt switch { Option.Some(var v) v, Option.None(_) -1, };从 crates/bindings-csharp/BSATN.Codegen/Type.cs 的TypeKind.Sum分支可以看到完整的生成策略每个变体生成public sealed record {Variant}({VariantType} {Variant}_)Read时先读取一个byte作为 tag再switch分派到对应变体的序列化器Write时先写 tag 字节再写变体载荷。README 还强调为规避 C# 将 record 字段与变体名放入同一命名空间导致的命名冲突生成的字段名会追加下划线如Some_。生成代码长什么样快照测试一窥究竟Codegen.Tests的快照目录保存了所有生成结果是理解生成机制的最佳教材。例如 crates/bindings-csharp/Codegen.Tests/fixtures/server/snapshots/Module#PublicTable.verified.cs 展示了PublicTable的完整生成代码每个字段对应一个internal static readonly的IReadWrite序列化器如SpacetimeDB.BSATN.I32 IdRW、SpacetimeDB.BSATN.U128Stdb U128FieldRWReadFields/WriteFields逐字段调用ToString()则借助SpacetimeDB.BSATN.StringUtil.GenericToString实现深度友好打印——该工具类crates/bindings-csharp/BSATN.Runtime/BSATN/Runtime.cs对 null 打印为null、对字符串加引号、对列表打印[ ... ]且超过 16 个元素时只打印首尾各 8 个并省略中间防止在 Unity 中误打印巨大数组导致崩溃。在客户端快照目录 crates/bindings-csharp/Codegen.Tests/fixtures/client/snapshots 中Type#CustomStruct、Type#CustomTaggedEnum、Type#PublicTable等快照展示了客户端场景的生成结果。枚举的特殊规则与诊断Type生成器对 C# 枚举非TaggedEnum做两项严格校验见 crates/bindings-csharp/BSATN.Codegen/Type.cs禁止显式赋值SATS 枚举不支持显式 tag变体必须连续因此生成器对enum成员带EqualsValue的情况报告ErrorDescriptor.EnumWithExplicitValues变体数不得超过 256SATS 用byte表示 tag超过 256 个变体报告ErrorDescriptor.EnumTooManyVariants。运行时侧SpacetimeDB.BSATN.EnumTcrates/bindings-csharp/BSATN.Runtime/BSATN/Runtime.cs用Enum.GetValues构建tag 到值的连续数组Read时读取一个字节查表Write时只做上界检查tag TagToValue.Length而非昂贵的Enum.IsDefined反射调用——源码注释记录了这是刻意的性能优化。BSATN.Runtime接口、宽整数与特殊类型BSATN.Runtime是BSATN.Codegen的运行时伴侣见 crates/bindings-csharp/BSATN.Runtime/README.md包含宽整数类型U128、I128、U256、I256位于 crates/bindings-csharp/BSATN.Runtime/BSATN/用于兼容 SpacetimeDB 的 128/256 位整数特殊 SpacetimeDB 类型Identity、ConnectionId、Timestamp、TimeDuration、ScheduleAt全部实现在 crates/bindings-csharp/BSATN.Runtime/Builtins.csAlgebraicType类型内部使用描述类型的代数结构两个核心接口IStructuralReadWrite与IReadWriteT各类原始类型的 BSATN 序列化器Bool、U8/U16/U32/U64、I8/I16/I32/I64、F32/F64、String、Array、List、ByteArray、RefOption/ValueOption、EnumT等均位于 crates/bindings-csharp/BSATN.Runtime/BSATN/Runtime.cs主要供BSATN.Codegen生成的代码引用。特殊类型的Ref-less自描述Builtins.cs的开头注释揭示了特殊类型Unit、ConnectionId、Identity等的实现动机它们具有特殊的无需 Ref 间接引用的AlgebraicType表示。因此这些类型不使用[SpacetimeDB.Type]而是手工实现原本由生成器产出的序列化代码以便覆盖GetAlgebraicType返回 Ref-less 形式。以Unit为例public readonly partial struct Unit { public readonly struct BSATN : IReadWriteUnit { public Unit Read(BinaryReader reader) default; public void Write(BinaryWriter writer, Unit value) { } public AlgebraicType GetAlgebraicType(ITypeRegistrar registrar) // 直接返回 Product而非 Ref因为这是特殊类型 new AlgebraicType.Product([]); } }ConnectionId则提供From(ReadOnlySpanbyte)小端、FromBigEndian、FromHexString32 位十六进制字符串与Random()等工厂方法并实现了IEquatableConnectionId、IComparable。序列化的工程细节IStructuralReadWrite提供便捷的字节序列化帮助方法// 通过 IReadWrite 序列化 public static byte[] ToBytesRW, T(RW rw, T value) where RW : IReadWriteT; // 通过 IStructuralReadWrite 直接序列化 public static byte[] ToBytesT(T value) where T : IStructuralReadWrite;String序列化器对 null 抛出ArgumentNullException并提示若要序列化 null 字符串必须使用可空字符串string?以编码为 BSATN option——这是 BSATN 格式语义的体现。ByteArray是字节数组的特化实现避免逐元素装箱Array/List的Read都会先读Int32长度再预分配输出Enumerable则是惰性yield return版本。对于不支持的类型的诊断占位UnsupportedT的所有方法直接抛出NotSupportedException用于在编译报错时减少噪声错误。Codegen为表与 Reducer 生成 FFI 注册代码Codegen见 crates/bindings-csharp/Codegen/README.md只服务于 C# 模块包含另两个核心特性[SpacetimeDB.Table]生成代码在启动时将该表注册进FFI使其可被__describe_module__FFI API 枚举。它隐含[SpacetimeDB.Type]因此同一结构体上不能同时标注两个特性。TableAttributecrates/bindings-csharp/Runtime/Attrs.cs还支持Accessor表访问器名称默认取类型的nameofName宿主侧表的规范名Public/Event公开表、事件表Scheduled/ScheduledAt声明调度表scheduled table且要求表具有ulong类型主键、ScheduledAt列类型为SpacetimeDB.ScheduleAt否则生成器报告InvalidScheduledDeclaration诊断。[SpacetimeDB.Reducer]生成代码在启动时注册一个静态函数为 SpacetimeDB reducer并为__call_reducer__FFI API 创建包装器解析 SATS 二进制 blob 为各个参数、调用底层函数。ReducerAttribute支持KindReducerKind.UserDefined等与Name。列级特性与校验ColumnDeclarationcrates/bindings-csharp/Codegen/Module.cs解析字段上的列特性并执行严格的组合校验[AutoInc]要求整型列IsInteger覆盖byte到UInt64、Int128/UInt128、I128/U128/I256/U256否则报告AutoIncNotInteger[Unique]要求列可等值比较IsEquatable覆盖整数、无载荷枚举、string/bool、ConnectionId/Identity/Timestamp/Uuid且不允许可空标注否则报告UniqueNotEquatable[Default]与AutoInc/PrimaryKey/Unique组合时报告IncompatibleDefaultAttributesCombination。从源码结构看Codegen还支持 B-tree 索引TableIndex当前TableIndexType仅BTree一种、视图IView、存储过程ProcedureAttribute与命名空间/类嵌套等场景快照目录中均有对应示例如Module#BTreeMultiColumn.verified.cs、Module#BTreeViews.verified.cs、Module#MultiTableRow.verified.cs。用快照测试校验生成结果Codegen.Tests 是基于快照的测试工程包含client、server、diag、explicitnames四套 fixture。快照文件名即测试意图的注释例如Module#Reducers.TestDuplicateReducerName.verified.cs、Module#Reducers.TestDuplicateReducerKind1.verified.cs重复/冲突 reducer 的诊断Module#TestIndexIssues.verified.cs、Module#TestUniqueNotEquatable.verified.cs非法索引与 unique 的诊断Module#FFI.verified.csFFI 生成类的完整示例Type#ViewPrimaryKeyPartialRow.verified.cs视图主键的生成示例。此外在任意使用BSATN.Codegen的项目中可以在.csproj的PropertyGroup中设置EmitCompilerGeneratedFilestrue/EmitCompilerGeneratedFiles即可在obj/Debug/*/generated/SpacetimeDB.BSATN.Codegen目录下查看自己项目实际生成的代码crates/bindings-csharp/BSATN.Codegen/Type.cs 顶部注释也说明了这一点。RuntimeWebAssembly 模块的运行时绑定Runtime见 crates/bindings-csharp/Runtime/README.md包含 SpacetimeDB WebAssembly 模块的运行时绑定。模块被编译为暴露特定接口的 Wasm 模块目前通过Wasi.Sdk包.NET 对 WASI 标准的实现构建且 README 注明这一方案未来可能改变。从 C# 到 Wasm FFIbindings.c 的完整链路虽然未被正式文档化README 详细记录了构建带自定义绑定的原始 Wasm 模块的流程这是理解模块运行时原理的关键声明 Wasm importsbindings.ccrates/bindings-csharp/Runtime/bindings.c声明指向 SpacetimeDB FFIimports的原始 C 绑定并用__attribute__((import_module(spacetime), import_name(_insert)))之类的属性标记它们为 WebAssembly imports注释注明函数名重复目前不可避免Mono 兼容包装器bindings.c实现一批 Mono 兼容的包装函数负责在 Mono 类型与 SpacetimeDB FFI 期望的原始类型之间转换并调用对应的原始绑定C# 侧声明Runtime.cscrates/bindings-csharp/Runtime/Runtime.cs声明签名兼容的函数供包装器挂接全部标记[MethodImpl(MethodImplOptions.InternalCall)]挂接bindings.c在mono_stdb_attach_bindings函数中把所有 Mono 兼容包装器挂接到对应 C# 声明上导出 FFI 兼容的 exportsbindings.c添加 FFI 兼容的exports如__attribute__((export_name(__call_reducer__)))这些导出在 Mono 运行时中按程序集名、命名空间、类名与方法名查找方法并调用WASI no-op 垫片最后bindings.c为所有 WASI API 实现 no-op 垫片使其内部链接而不从运行时导入。最终产物是与 SpacetimeDB FFI 兼容且没有任何 WASI imports 的 Wasm 模块。Runtime目录下还有 driver.h 等头文件支撑该链路。__describe_module__与 RawModuleDef 的再生成Runtime还包含Internal/Autogen目录——一系列自动生成的类型定义RawModuleDefV8RawModuleDefV10、RawTableDefV10、RawReducerDefV10等见 crates/bindings-csharp/Runtime/Internal/Autogen用于序列化__describe_module__返回的RawModuleDef。这些类型对应不同版本化的模块描述结构。要重新生成该目录在仓库根目录运行cargo run -p spacetimedb-codegen --example regen-csharp-moduledef模块侧的核心抽象Runtime还提供模块编程所需的各类抽象见 crates/bindings-csharp/Runtime/ITable/IView/IIndex表、视图、索引访问接口IReducerreducer 基接口HandlerContext/ProcedureContext/AuthCtxreducer 与存储过程的上下文含JwtClaimsTxContext/TransactionalContextState事务上下文状态Http/IHttpHandlerHTTP 处理器HttpMethod、RawHttpHandlerDefV10等Log/LogStopwatch日志与耗时统计Exceptions.cs模块异常体系。Runtime.Testscrates/bindings-csharp/Runtime.Tests包含JwtClaimsTest与RouterTests等测试可验证相关模块的行为。测试体系快照测试 随机模糊测试README 明确指出测试分布在两处Codegen.Tests快照式测试逐字节校验生成代码是否符合预期让代码审查更直观。除了上面提到的 fixture还有diagfixture诊断信息快照与explicitnamesfixture显式命名场景如DemoTable、DemoReducer、DemoTypeBSATN.Runtime.Tests随机化运行时测试Tests.cs 对多种类型随机模糊测试生成的序列化器验证往返round-trip一致性。快速上手与实用建议要在自己的 C# 模块或客户端项目中启用这套绑定引用工程模块引用CodegenRuntimeBSATN.Codegen/BSATN.Runtime作为传递依赖纯客户端应用只需BSATN.CodegenBSATN.Runtime定义类型用[SpacetimeDB.Type] partial ...声明表行类型与 reducer 参数类型需要标记联合时继承SpacetimeDB.TaggedEnum(VariantA A, VariantB B)声明表与 reducer在模块中用[SpacetimeDB.Table]、[SpacetimeDB.Reducer]标注列特性使用[AutoInc]/[PrimaryKey]/[Unique]/[Default]与索引特性调试生成代码在.csproj中开启EmitCompilerGeneratedFilestrue/EmitCompilerGeneratedFiles查看obj/Debug/*/generated/下的实际产物保持字段声明规整由于生成器只收集特性所在 partial 声明内的非静态字段额外字段请放在未标注特性的 partial 声明中避免意外进入表结构。总结crates/bindings-csharp是 SpacetimeDB C# 生态的完整拼图BSATN.CodegenBSATN.Runtime用 Roslyn 增量生成器把 BSATN 序列化、自描述与 C# 类型系统优雅地缝合在一起零尺寸BSATN结构体是应对无静态接口方法限制的巧妙设计CodegenRuntime则负责将表、reducer 与整个模块接入 SpacetimeDB 的 WebAssembly FFI。四者共同支撑起用 C# 编写 SpacetimeDB 模块、用 C# 客户端与模块交互的完整链路。需要注意的是这一绑定层目前仍是内部项目、接口不稳定正式开发应以官方稳定文档为准。【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表