Skip to content

NativeAOT / trimming / IL2CPP support for ChibiRuby.Serializer (fixes #199) - #201

Merged
hadashiA merged 3 commits into
mainfrom
claude/nativeaot-support
Sep 5, 2026
Merged

NativeAOT / trimming / IL2CPP support for ChibiRuby.Serializer (fixes #199)#201
hadashiA merged 3 commits into
mainfrom
claude/nativeaot-support

Conversation

@hadashiA

@hadashiA hadashiA commented Sep 2, 2026

Copy link
Copy Markdown
Owner

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). A ModuleInitializerAttribute polyfill 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

__RegisterMRubyValueFormatter now 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 to BuiltinResolver — recursively (e.g. Dictionary<string, List<MyStruct[]>>). A re-entrancy guard makes self-/mutually-referencing types cycle-safe. BuiltinResolver's dynamic path now catches NotSupportedException/MissingMethodException and returns null so resolution falls through to these registrations (JIT and IL2CPP full-generic-sharing behavior is unchanged). EnumAsStringFormatter uses Enum.GetValues<T>() (constraint tightened to struct, Enum) to clear IL3050.

Phase 3 — call-site-only root declaration

New [assembly: MRubyFormattable(typeof(List<double>))] declares serializable types that appear only at Serialize<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

  • IsAotCompatible on net8.0+ targets; remaining intentional dynamic paths carry UnconditionalSuppressMessage with justifications; publish is IL-warning-free.
  • sandbox/NativeAotSanity: a PublishAot console app covering the member graph (enum/nullable/collections), call-site int[], and MRubyFormattable roots. Runs in a new test-nativeaot CI job and treats IL2xxx/IL3xxx warnings as errors.
  • README gains a "NativeAOT / trimming / IL2CPP" section.

The reflection lookup in GeneratedResolver is kept (attribute gate removed, per #199) purely as a fallback for assemblies compiled with older generator versions.

Verification

  • All 5 test projects pass (268 tests), including new tests for eager member registrations, MRubyFormattable roots, and the reflection fallback; the incremental-generator caching test still passes with the new aggregate output.
  • dotnet publish -r osx-arm64 of NativeAotSanity: zero IL warnings, all checks pass at runtime (before this change, every check failed with MRubySerializationException / NotSupportedException / MissingMethodException).
  • netstandard2.1 + LangVersion 9 consumer (Unity-equivalent) compiles cleanly with the emitted polyfill.

Notes / limitations

  • Generic [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>))].
  • The emitted ModuleInitializerAttribute polyfill 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

hadashiA and others added 3 commits September 2, 2026 18:09
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
hadashiA marked this pull request as draft September 2, 2026 23:32
@hadashiA
hadashiA marked this pull request as ready for review September 4, 2026 15:18
@hadashiA
hadashiA merged commit 22659d5 into main Sep 5, 2026
2 checks passed
@hadashiA
hadashiA deleted the claude/nativeaot-support branch September 5, 2026 01:47
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.

[MRubyObject] instances are stripped by Unity 6000.5's linker (PreserveAttribute inheritance), breaking GeneratedResolver registration in IL2CPP builds

1 participant