NativeAOT / trimming / IL2CPP support for ChibiRuby.Serializer (fixes #199) - #201
Merged
Conversation
Unity 6000.5's linker unconditionally strips attribute instances of PreserveAttribute-derived attributes from player builds, so type.GetCustomAttribute<MRubyObjectAttribute>() returns null for generated types on IL2CPP builds and no formatter was ever registered. The __RegisterMRubyValueFormatter method lookup alone is a sufficient and stripping-proof signal, since only generated types have that method. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Formatter registration previously relied on runtime reflection (GetMethod + Invoke gated by an attribute lookup) and on MakeGenericType/Activator.CreateInstance for collection, enum and nullable member types. Both paths break under NativeAOT (and the attribute gate already broke under Unity 6000.5's linker, #199). The source generator now makes every registration statically rooted: - Emit a per-assembly [ModuleInitializer] that calls each generated type's __RegisterMRubyValueFormatter on assembly load. A polyfill of ModuleInitializerAttribute is emitted for target frameworks that lack it (netstandard2.1 / Unity). - __RegisterMRubyValueFormatter now also registers closed generic formatter instantiations for every member type reachable from the [MRubyObject] graph (arrays, enums, Nullable<T>, known collections), so AOT builds never need MakeGenericType. A re-entrancy guard makes self/mutually-referencing types cycle-safe. - New [assembly: MRubyFormattable(typeof(...))] declares serializable root types that appear only at call sites (e.g. Deserialize<List<T>>), including closed generics of [MRubyObject] types. Runtime changes: - BuiltinResolver catches NotSupportedException/MissingMethodException from the dynamic path and returns null so resolution falls through to the source-generated registrations. - EnumAsStringFormatter uses Enum.GetValues<T>()/GetNames<T>() (with a struct, Enum constraint) on modern TFMs to avoid IL3050. - GeneratedResolver's reflection lookup remains as a fallback for assemblies compiled with older generators, with trim-analysis suppressions documenting why it is safe. - IsAotCompatible enabled for net8.0+ targets; publish is warning-free. Verification: sandbox/NativeAotSanity is a PublishAot console app exercising member-graph types, call-site-only roots and enum/nullable round-trips; it runs in a new CI job (fails on any IL2/IL3 warning). Fixes #199 as well: the module initializer sidesteps both the stripped attribute instances and reflection entirely on Unity 6000.5+. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ention Replace BuiltinFormatterWalker's hard-coded KnownGenericFormatters list with discovery over the referenced ChibiRuby.Serializer assembly: a public generic formatter class implementing IMRubyValueFormatter<Target> whose Target is constructed exactly from the formatter's own type parameters (ListFormatter<T> : IMRubyValueFormatter<List<T>?>) maps Target's definition to that formatter. This mirrors what BuiltinResolver instantiates via MakeGenericType, so runtime and generator stay in sync by construction; array and enum formatters keep their dedicated handling, matching BuiltinResolver's own special cases. The map is cached per assembly symbol. Also make the collection/tuple formatter classes public. Most were internal, so the previously emitted registrations would not even have compiled from a user assembly for members like Stack<T>, HashSet<T>, KeyValuePair<,> or tuples; tests and the NativeAOT sanity app now cover those types. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hadashiA
marked this pull request as draft
September 2, 2026 23:32
hadashiA
marked this pull request as ready for review
September 4, 2026 15:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes ChibiRuby.Serializer fully AOT-safe, following the approach MessagePack-CSharp v3/v4 took (statically-rooted, source-generated registration; no runtime reflection or
MakeGenericType). Fixes #199 (supersedes #200) and makes the NativeAOT sanity suite pass, where previously every check failed.Phase 1 — statically rooted registration (module initializer)
The source generator now emits one
[ModuleInitializer]per assembly that calls every generated type's__RegisterMRubyValueFormatter()on assembly load. Registration no longer depends on reflection or on[MRubyObject]attribute instances at runtime, so it survives .NET trimming, NativeAOT, Unity managed stripping, and Unity 6000.5's Preserve-attribute-instance removal (#199). AModuleInitializerAttributepolyfill is emitted only when the target framework lacks it (netstandard2.1 / Unity, verified against a C# 9 netstandard2.1 consumer).Phase 2 — compile-time closed-generic instantiation
__RegisterMRubyValueFormatternow also registers fully-constructed formatter instantiations for every type reachable from the[MRubyObject]member graph — arrays (1–4 dims), enums,Nullable<T>, and all collections known toBuiltinResolver— recursively (e.g.Dictionary<string, List<MyStruct[]>>). A re-entrancy guard makes self-/mutually-referencing types cycle-safe.BuiltinResolver's dynamic path now catchesNotSupportedException/MissingMethodExceptionand returns null so resolution falls through to these registrations (JIT and IL2CPP full-generic-sharing behavior is unchanged).EnumAsStringFormatterusesEnum.GetValues<T>()(constraint tightened tostruct, Enum) to clear IL3050.Phase 3 — call-site-only root declaration
New
[assembly: MRubyFormattable(typeof(List<double>))]declares serializable types that appear only atSerialize<T>/Deserialize<T>call sites, which the generator cannot see. Closed generics of[MRubyObject]types are supported; open generics get a new MRBCS010 warning. This is the ChibiRuby analogue of MessagePack v4's[MessagePackSerializable<T>]root declaration.Phase 4 — annotations & CI verification
IsAotCompatibleon net8.0+ targets; remaining intentional dynamic paths carryUnconditionalSuppressMessagewith justifications; publish is IL-warning-free.sandbox/NativeAotSanity: aPublishAotconsole app covering the member graph (enum/nullable/collections), call-siteint[], andMRubyFormattableroots. Runs in a newtest-nativeaotCI job and treats IL2xxx/IL3xxx warnings as errors.The reflection lookup in
GeneratedResolveris kept (attribute gate removed, per #199) purely as a fallback for assemblies compiled with older generator versions.Verification
MRubyFormattableroots, and the reflection fallback; the incremental-generator caching test still passes with the new aggregate output.dotnet publish -r osx-arm64of NativeAotSanity: zero IL warnings, all checks pass at runtime (before this change, every check failed withMRubySerializationException/NotSupportedException/MissingMethodException).Notes / limitations
[MRubyObject]types are not called from the module initializer (an open generic cannot be); closed instantiations can be rooted via[assembly: MRubyFormattable(typeof(X<int>))].ModuleInitializerAttributepolyfill could collide with another generator emitting the same polyfill (e.g. PolySharp) in a netstandard2.1 assembly — modern TFMs never emit it.🤖 Generated with Claude Code