From 5e3cb92479e525ce3e702a97003c3b8558581696 Mon Sep 17 00:00:00 2001 From: hadashi Date: Wed, 2 Sep 2026 12:33:03 +0900 Subject: [PATCH] Fix #199: Drop [MRubyObject] runtime gate in GeneratedResolver Unity 6000.5's linker unconditionally strips attribute instances of PreserveAttribute-derived attributes from player builds, so type.GetCustomAttribute() 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 --- .../Resolvers/GeneratedResolver.cs | 5 +++-- tests/ChibiRuby.Serializer.Tests/Classes.cs | 21 +++++++++++++++++++ .../GeneratedFormatterTest.cs | 12 +++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/ChibiRuby.Serializer/Resolvers/GeneratedResolver.cs b/src/ChibiRuby.Serializer/Resolvers/GeneratedResolver.cs index a23bb8d2..5da7d254 100644 --- a/src/ChibiRuby.Serializer/Resolvers/GeneratedResolver.cs +++ b/src/ChibiRuby.Serializer/Resolvers/GeneratedResolver.cs @@ -26,8 +26,9 @@ static Cache() static bool TryInvokeRegisterFormatter(Type type) { - if (type.GetCustomAttribute() == null) return false; - + // Do not gate on [MRubyObject] here: Unity 6000.5+'s linker strips instances of + // PreserveAttribute-derived attributes from player builds, so the attribute can be + // absent at runtime even for generated types. The generated method is the reliable signal. var m = type.GetMethod("__RegisterMRubyValueFormatter", BindingFlags.Public | BindingFlags.NonPublic | diff --git a/tests/ChibiRuby.Serializer.Tests/Classes.cs b/tests/ChibiRuby.Serializer.Tests/Classes.cs index 91ec4e64..f29b9524 100644 --- a/tests/ChibiRuby.Serializer.Tests/Classes.cs +++ b/tests/ChibiRuby.Serializer.Tests/Classes.cs @@ -25,3 +25,24 @@ partial struct Struct1 { public long Id { get; set; } } + +// Simulates a source-generated [MRubyObject] type after Unity 6000.5's linker has stripped the +// attribute instance (#199): the generated registration method exists, but the attribute does not. +class AttributeStrippedObject +{ + public int Value { get; set; } + + public static void __RegisterMRubyValueFormatter() + { + GeneratedResolver.Register(new AttributeStrippedObjectFormatter()); + } + + class AttributeStrippedObjectFormatter : IMRubyValueFormatter + { + public MRubyValue Serialize(AttributeStrippedObject? value, MRubyState mrb, MRubyValueSerializerOptions options) + => value is null ? default : new MRubyValue(value.Value); + + public AttributeStrippedObject? Deserialize(MRubyValue value, MRubyState mrb, MRubyValueSerializerOptions options) + => new() { Value = checked((int)value.IntegerValue) }; + } +} diff --git a/tests/ChibiRuby.Serializer.Tests/GeneratedFormatterTest.cs b/tests/ChibiRuby.Serializer.Tests/GeneratedFormatterTest.cs index 56376185..237e317d 100644 --- a/tests/ChibiRuby.Serializer.Tests/GeneratedFormatterTest.cs +++ b/tests/ChibiRuby.Serializer.Tests/GeneratedFormatterTest.cs @@ -95,4 +95,16 @@ public void DeserializeWithCtor() Assert.That(result.Y, Is.EqualTo(456)); Assert.That(result.Hoge, Is.EqualTo("hello hello")); } + + [Test] + public void RegisterFormatterWithoutAttributeInstance() + { + // The [MRubyObject] attribute instance may be stripped by Unity 6000.5+'s linker (#199). + // Registration must work based on the generated method alone. + var result = MRubyValueSerializer.Deserialize(new MRubyValue(42), state)!; + Assert.That(result.Value, Is.EqualTo(42)); + + var serialized = MRubyValueSerializer.Serialize(new AttributeStrippedObject { Value = 43 }, state); + Assert.That(serialized, Is.EqualTo(new MRubyValue(43))); + } }