Skip to content

Add Native AOT / trimming support - #197

Draft
hadashiA wants to merge 1 commit into
masterfrom
ku/native-aot
Draft

hadashiA wants to merge 1 commit into
masterfrom
ku/native-aot

Conversation

@hadashiA

@hadashiA hadashiA commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

Makes VYaml work on .NET Native AOT and trimmed (PublishTrimmed) apps, with zero trim/AOT analysis warnings. Previously a Native AOT publish crashed on the very first Serialize call: the generated __RegisterVYamlFormatter methods were only reachable via reflection (so ILC trimmed them — [Preserve] is Unity-IL2CPP-only), and BuiltinResolver composed enum/collection formatters with MakeGenericType, whose value-type instantiations don't exist in an AOT binary.

The design follows the approach MessagePack-CSharp v4 is taking (module-initializer push registration + compile-time harvesting of closed generic shapes), translated to VYaml's resolver architecture.

What changed

Source generator

  • Emits one VYamlGeneratedRegistration.g.cs per assembly: a [ModuleInitializer] that
    • calls __RegisterVYamlFormatter() for every closed [YamlObject] type (interfaces register their nested generated formatter directly),
    • registers statically constructed formatters for the closed generic shapes harvested from the member graph: enums (EnumAsStringFormatter<TEnum>), collections (List<T>, Dictionary<K,V>, arrays 1–4D, sets, queues, tuples, interfaces, ...), Nullable<T>, and closed instantiations of generic [YamlObject] types (recursing into their substituted members).
  • Being ordinary statically reachable code, the registrations cannot be trimmed and force the value-type generic instantiations into the binary.
  • New [assembly: YamlSerializable(typeof(...))] (in VYaml.Annotations) declares root-only shapes — the ones that only appear as top-level Serialize/Deserialize arguments; the whole reachable closure of the declared type is harvested. Open generics are rejected with the new VYAML016 diagnostic.
  • A ModuleInitializerAttribute polyfill is emitted when the target framework lacks it (netstandard2.0/2.1, Unity), gated on the compilation not already having one. Registration generation requires C# 9+; older LangVersions keep the previous behavior.
  • Both the Roslyn4 incremental generator and the Roslyn3 (Unity) generator produce identical output.

Runtime

  • BuiltinResolver: the MakeGenericType/Activator.CreateInstance path no longer throws out of FormatterCache<T>'s static constructor on AOT — failures (NotSupportedException, TypeLoadException, MissingMethodException) return null so StandardResolver falls through to GeneratedResolver, where the statically registered formatter lives. JIT and IL2CPP behavior is unchanged (the dynamic path still works there and wins first).
  • GeneratedResolver: the reflection-based lazy registration is kept as a JIT fallback — the CLR runs module initializers lazily, and it also covers assemblies compiled with an older generator.
  • EnumAsStringFormatter<T>: [DynamicallyAccessedMembers(PublicFields)] annotations so enum field metadata survives trimming.
  • IsAotCompatible=true on net8.0+ targets (enables the trim/AOT analyzers, published to NuGet metadata); all remaining analyzer warnings resolved or suppressed with justifications (TypeFormatter's Type.GetType is inherently dynamic and documented as such). PolySharp extended to netstandard2.1 for the annotation attributes.

Tests / CI / docs

  • VYaml.Tests.NativeAot: a PublishAot smoke test covering roundtrips with enum / alias enum / collection / array / nested / closed-generic members, interface and abstract unions, [assembly: YamlSerializable] roots, the untyped object path, and multi-document deserialization. Wired into CI: the publish fails on any warning IL*, then the native binary runs its checks.
  • README section on Native AOT / trimming.

Verification

  • Existing test suite: 410 passed (JIT behavior unchanged).
  • dotnet publish -c Release of the AOT test on osx-arm64: 0 IL warnings, binary runs all 20 checks green.

Compatibility

  • No public API breaks. Consumer assemblies compiled with an older VYaml keep working on JIT/IL2CPP via the reflection fallback; Native AOT requires recompiling with this generator.
  • Formatter construction is now eager (at module initialization) instead of lazy-on-first-use for generated types.

Known limitations

  • Root-only generic shapes must be declared with [assembly: YamlSerializable] (documented).
  • Deserialize<Dictionary<K,V>> etc. of shapes neither harvested nor declared fail on AOT with the existing "not registered" error rather than a crash.

🤖 Generated with Claude Code

The source generator now emits a per-assembly module initializer that
statically registers every [YamlObject] formatter plus statically
constructed formatters for the closed generic shapes harvested from the
member graph (enums, collections, Nullable, closed user generics), so
nothing relies on reflection-only registration and every needed generic
instantiation is compiled into Native AOT binaries.

- [assembly: YamlSerializable(typeof(...))] declares root-only shapes;
  the whole reachable closure of the declared type is registered
- BuiltinResolver's MakeGenericType path no longer poisons the formatter
  cache on AOT: failures fall through to GeneratedResolver
- The reflection-based lazy registration remains as a JIT fallback (lazy
  module cctors, assemblies built with an older generator)
- Trim/AOT analyzers enabled (IsAotCompatible) with zero IL warnings;
  DynamicallyAccessedMembers annotations on the enum formatter
- VYaml.Tests.NativeAot: PublishAot smoke test wired into CI

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hadashiA
hadashiA marked this pull request as draft September 2, 2026 23:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant