From 18bfad08e62642b6ce4ad12603651eca60acbe02 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 27 Apr 2026 11:01:26 +0200 Subject: [PATCH] Trimmable typemap: emit alias-base entry as unconditional The alias-group base entry (e.g. java/lang/String -> alias holder) was emitted as a 3-arg conditional TypeMap attribute regardless of the ForceUnconditionalEntries workaround for dotnet/runtime#127004. When that workaround is in effect, the indexed alias entries are 2-arg (unconditional) and the alias holder is reachable only via TypeMapAssociation. Per #127004 the trimmer strips the association when a TypeMap entry references the same type, removing the holder and making the dictionary key 'java/lang/String' (and 'java/lang/Object', 'java/lang/Throwable', ...) disappear at runtime. The hierarchy walker then advances past the most-derived JNI class and CreatePeer falls back to the targetType-based lookup, returning a base-class peer. Concrete symptom (verified on device): GetObject_ReturnsMostDerivedType hierarchy: jni='java/lang/String' targetType='Java.Lang.Object' proxyCount=0 hierarchy: jni='java/lang/Object' targetType='Java.Lang.Object' proxyCount=0 -> Expected Java.Lang.String but was Java.Lang.Object Route the alias-base entry through the same unconditional decision used by BuildEntry: emit it as 2-arg whenever ForceUnconditionalEntries is on, the JNI name is in EssentialRuntimeTypes, or any peer in the alias group already needs to be unconditional. After the fix the same lookup reports proxyCount=2 and selects Java.Lang.String, and the test (and the full Mono.Android.NET-Tests trimmable suite) passes with 0 failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 10 +++++++++- .../NUnitInstrumentation.cs | 3 --- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 996ce142a13..79570a14bda 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -177,10 +177,18 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, } // Base JNI name entry → alias holder (self-referencing trim target, kept alive by associations) + // When ForceUnconditionalEntries is true we MUST emit this as 2-arg (unconditional) just + // like BuildEntry does: dotnet/runtime#127004 strips the TypeMapAssociation that keeps the + // holder alive when a TypeMap entry references the same type, leaving the dictionary key + // missing at runtime and breaking hierarchy lookups for essential types like + // java/lang/String and java/lang/Object. + bool aliasBaseUnconditional = ForceUnconditionalEntries + || EssentialRuntimeTypes.Contains (jniName) + || peersForName.Any (IsUnconditionalEntry); model.Entries.Add (new TypeMapAttributeData { JniName = jniName, ProxyTypeReference = holderRef, - TargetTypeReference = holderRef, + TargetTypeReference = aliasBaseUnconditional ? null : holderRef, }); model.AliasHolders.Add (new AliasHolderData { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs index 63c219c712a..d24d53ad324 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs @@ -85,9 +85,6 @@ protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) // Throwable subclass registration "Java.InteropTests.JnienvTest.ActivatedDirectThrowableSubclassesShouldBeRegistered", - // Typemap doesn't resolve most-derived type - "Java.LangTests.ObjectTest.GetObject_ReturnsMostDerivedType", - // Instance identity after JNI round-trip "Java.LangTests.ObjectTest.JnienvCreateInstance_RegistersMultipleInstances",