Summary
Starting with Unity 6000.5, UnityLinker unconditionally removes custom attribute instances of
PreserveAttribute and anything that inherits from it (matched by name, walking base types).
Since MRubyObjectAttribute is declared as MRubyObjectAttribute : PreserveAttribute,
the [MRubyObject] instances disappear from stripped assemblies.
GeneratedResolver.TryInvokeRegisterFormatter uses that attribute as a runtime gate:
static bool TryInvokeRegisterFormatter(Type type)
{
if (type.GetCustomAttribute<MRubyObjectAttribute>() == null) return false; // ← always false on Unity 6000.5 IL2CPP builds
var m = type.GetMethod("__RegisterMRubyValueFormatter", ...);
...
}
On Unity 6000.5 IL2CPP builds (tested on WebGL, Managed Stripping Level = Medium), the generated
__RegisterMRubyValueFormatter method survives stripping (thanks to [Preserve]), but the
attribute check fails first, so no formatter is ever registered. Every Deserialize<T> then throws:
MRubySerializationException: <T> is not registered in resolver: MRubyCS.Serializer.StandardResolver
The Editor (Mono, no stripping) is unaffected, which makes this easy to miss until a device/player build.
Environment
- MRubyCS / MRubyCS.Serializer 0.16.0 (the same design is still present on current main:
src/ChibiRuby.Serializer/Attributes.cs and GeneratedResolver.cs)
- Unity 6000.5.8f1, IL2CPP (WebGL), Managed Stripping Level: Medium
- Unity 6000.3.21f1 and 6000.4.11f1 are not affected (attribute instances are kept)
Root cause (verified against UnityLinker itself)
I decompiled the UnityLinker shipped with 6000.3.21f1 and 6000.5.8f1 and diffed the marking logic:
- Unity matches "Preserve" attributes by
IsOrInheritFromAttributeNamed(type, "PreserveAttribute"),
i.e. by class name including inheritance — so MRubyObjectAttribute : PreserveAttribute is
classified as a Preserve attribute.
- 6000.3:
UnityMarking.ShouldMarkCustomAttribute treats Preserve-family attributes as
"link-time attributes" and keeps their instances (KeepLinkTimeAttributes is always true for
editor-driven builds).
- 6000.5: the Preserve family was moved into
StrippingControlsMarking.ShouldMarkCustomAttribute,
which now returns a hard-coded value = false — Preserve(-derived) attribute instances are always
removed from linked assemblies. The preserve semantics (keeping the annotated type/members)
still work; only the runtime-visible attribute instance is gone.
I confirmed the scoping with a canary experiment: on the same input assembly, Unity 6000.5's linker
keeps [Obsolete], a same-assembly attribute, and [MRubyIgnore], and removes only
[MRubyObject]. Unity 6000.3 keeps all of them. So this is specifically the PreserveAttribute
inheritance being caught by Unity's name-based matching, not general attribute stripping.
(Whether Unity's change is intentional is a separate question — I'm reporting it to Unity as well —
but the library is broken on 6000.5 today, and the attribute-instance removal may well be by design
since [Preserve] has no runtime meaning to Unity.)
Workaround
Calling the generated registration method explicitly at startup works, because it bypasses both the
attribute gate and reflection:
MyCommand.__RegisterMRubyValueFormatter(); // for each [MRubyObject] type
Suggested fixes
Any of these would make the library robust against this Unity behavior:
- Drop the attribute gate in
TryInvokeRegisterFormatter — the __RegisterMRubyValueFormatter
lookup alone is a sufficient (and more reliable) signal; only generated types have that method.
- Stop inheriting
PreserveAttribute in MRubyObjectAttribute, and instead have the source
generator emit preservation separately (it already puts [Preserve] on the generated members).
- Expose/document a supported eager registration API so IL2CPP users don't need to call a
double-underscore method directly.
Happy to provide the decompiled UnityLinker diff details or the minimal repro project if useful.
Summary
Starting with Unity 6000.5, UnityLinker unconditionally removes custom attribute instances of
PreserveAttributeand anything that inherits from it (matched by name, walking base types).Since
MRubyObjectAttributeis declared asMRubyObjectAttribute : PreserveAttribute,the
[MRubyObject]instances disappear from stripped assemblies.GeneratedResolver.TryInvokeRegisterFormatteruses that attribute as a runtime gate:On Unity 6000.5 IL2CPP builds (tested on WebGL, Managed Stripping Level = Medium), the generated
__RegisterMRubyValueFormattermethod survives stripping (thanks to[Preserve]), but theattribute check fails first, so no formatter is ever registered. Every
Deserialize<T>then throws:The Editor (Mono, no stripping) is unaffected, which makes this easy to miss until a device/player build.
Environment
src/ChibiRuby.Serializer/Attributes.csandGeneratedResolver.cs)Root cause (verified against UnityLinker itself)
I decompiled the UnityLinker shipped with 6000.3.21f1 and 6000.5.8f1 and diffed the marking logic:
IsOrInheritFromAttributeNamed(type, "PreserveAttribute"),i.e. by class name including inheritance — so
MRubyObjectAttribute : PreserveAttributeisclassified as a Preserve attribute.
UnityMarking.ShouldMarkCustomAttributetreats Preserve-family attributes as"link-time attributes" and keeps their instances (
KeepLinkTimeAttributesis always true foreditor-driven builds).
StrippingControlsMarking.ShouldMarkCustomAttribute,which now returns a hard-coded
value = false— Preserve(-derived) attribute instances are alwaysremoved from linked assemblies. The preserve semantics (keeping the annotated type/members)
still work; only the runtime-visible attribute instance is gone.
I confirmed the scoping with a canary experiment: on the same input assembly, Unity 6000.5's linker
keeps
[Obsolete], a same-assembly attribute, and[MRubyIgnore], and removes only[MRubyObject]. Unity 6000.3 keeps all of them. So this is specifically the PreserveAttributeinheritance being caught by Unity's name-based matching, not general attribute stripping.
(Whether Unity's change is intentional is a separate question — I'm reporting it to Unity as well —
but the library is broken on 6000.5 today, and the attribute-instance removal may well be by design
since
[Preserve]has no runtime meaning to Unity.)Workaround
Calling the generated registration method explicitly at startup works, because it bypasses both the
attribute gate and reflection:
Suggested fixes
Any of these would make the library robust against this Unity behavior:
TryInvokeRegisterFormatter— the__RegisterMRubyValueFormatterlookup alone is a sufficient (and more reliable) signal; only generated types have that method.
PreserveAttributeinMRubyObjectAttribute, and instead have the sourcegenerator emit preservation separately (it already puts
[Preserve]on the generated members).double-underscore method directly.
Happy to provide the decompiled UnityLinker diff details or the minimal repro project if useful.