Skip to content

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

Description

@adarapata

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:

  1. Drop the attribute gate in TryInvokeRegisterFormatter — the __RegisterMRubyValueFormatter
    lookup alone is a sufficient (and more reliable) signal; only generated types have that method.
  2. Stop inheriting PreserveAttribute in MRubyObjectAttribute, and instead have the source
    generator emit preservation separately (it already puts [Preserve] on the generated members).
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions