From 4f4703ad3daa044107533f7ba337a746a25185fa Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 3 May 2026 21:55:18 +0200 Subject: [PATCH 1/2] Fix trimmable Java object activation Run default managed constructors for generated no-arg constructor callbacks, and mark generated constructor activation peers as replaceable so managed wrappers can replace temporary peers created during Java-side virtual dispatch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyEmitter.cs | 35 +++++++++++++++++++ .../Java.Interop/JavaPeerProxy.cs | 26 ++++++++++++++ .../Java.Interop/JnienvTest.cs | 4 --- .../Java.Lang/ObjectTest.cs | 1 - .../NUnitInstrumentation.cs | 6 ---- 5 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index ccf1ac857a4..8f2334d63a3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -99,6 +99,8 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _jniObjectReferenceCtorRef; MemberReferenceHandle _jniEnvDeleteRefRef; MemberReferenceHandle _shouldSkipActivationRef; + MemberReferenceHandle _activateDefaultConstructorRef; + MemberReferenceHandle _markActivationPeerReplaceableRef; MemberReferenceHandle _waitForBridgeProcessingRef; MemberReferenceHandle _androidEnvironmentUnhandledExceptionRef; MemberReferenceHandle _ucoAttrCtorRef; @@ -325,6 +327,19 @@ void EmitMemberReferences () rt => rt.Type ().Boolean (), p => { p.AddParameter ().Type ().IntPtr (); })); + _activateDefaultConstructorRef = _pe.AddMemberRef (_javaPeerProxyNonGenericRef, "ActivateDefaultConstructor", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); + + _markActivationPeerReplaceableRef = _pe.AddMemberRef (_javaPeerProxyNonGenericRef, "MarkActivationPeerReplaceable", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().IntPtr ())); + _waitForBridgeProcessingRef = _pe.AddMemberRef (_androidRuntimeInternalRef, "WaitForBridgeProcessing", sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { })); @@ -988,6 +1003,22 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy return noopHandle; } + if (jniParams.Count == 0) { + var defaultCtorHandle = _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { + enc.LoadArgument (1); // self + enc.OpCode (ILOpCode.Ldtoken); + enc.Token (targetTypeRef); + enc.Call (_getTypeFromHandleRef); + enc.Call (_activateDefaultConstructorRef); + }), + EncodeUcoConstructorLocals_Standard); + AddUnmanagedCallersOnlyAttribute (defaultCtorHandle); + return defaultCtorHandle; + } + MethodDefinitionHandle handle; if (activationCtor.Style == ActivationCtorStyle.JavaInterop) { var ctorRef = AddJavaInteropActivationCtorRef ( @@ -1027,6 +1058,8 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy enc.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy enc.Call (ctorRef); } + enc.LoadArgument (1); // self + enc.Call (_markActivationPeerReplaceableRef); }), EncodeUcoConstructorLocals_JavaInterop); } else { @@ -1059,6 +1092,8 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy enc.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer enc.Call (ctorRef); } + enc.LoadArgument (1); // self + enc.Call (_markActivationPeerReplaceableRef); }), EncodeUcoConstructorLocals_Standard); } diff --git a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs index 88f7fa32244..ff1120943be 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Reflection; using Android.Runtime; namespace Java.Interop @@ -103,6 +104,31 @@ public static bool ShouldSkipActivation (IntPtr jniSelf) return (state & JniManagedPeerStates.Activatable) != JniManagedPeerStates.Activatable && (state & JniManagedPeerStates.Replaceable) != JniManagedPeerStates.Replaceable; } + + public static void ActivateDefaultConstructor ( + IntPtr jniSelf, + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type targetType) + { + var cinfo = targetType.GetConstructor (BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, binder: null, Type.EmptyTypes, modifiers: null); + if (cinfo == null) { + throw new NotSupportedException ($"Unable to find a default constructor for type `{targetType.FullName}`."); + } + + JniEnvironment.Runtime.ValueManager.ActivatePeer (null, new JniObjectReference (jniSelf), cinfo, null); + MarkActivationPeerReplaceable (jniSelf); + } + + public static void MarkActivationPeerReplaceable (IntPtr jniSelf) + { + var reference = new JniObjectReference (jniSelf, JniObjectReferenceType.Invalid); + var peer = JniEnvironment.Runtime.ValueManager.PeekPeer (reference); + if (peer == null) { + return; + } + + peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); + } } /// diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs index 148c7dc9383..ae49c425e93 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs @@ -302,14 +302,11 @@ public void ActivatedDirectObjectSubclassesShouldBeRegistered () } } - // TODO: https://github.com/dotnet/android/issues/11170 — throwable subclass not registered under trimmable typemap [Test] public void ActivatedDirectThrowableSubclassesShouldBeRegistered () { if (Build.VERSION.SdkInt <= BuildVersionCodes.GingerbreadMr1) Assert.Ignore ("Skipping test due to Bug #34141"); - - Console.Error.WriteLine ($"# jonp: BEGIN ActivatedDirectThrowableSubclassesShouldBeRegistered!!!"); using (var ThrowableActivatedFromJava_class = Java.Lang.Class.FromType (typeof (ThrowableActivatedFromJava))) { var ThrowableActivatedFromJava_init = JNIEnv.GetMethodID (ThrowableActivatedFromJava_class.Handle, "", "()V"); @@ -325,7 +322,6 @@ public void ActivatedDirectThrowableSubclassesShouldBeRegistered () Assert.IsTrue (v.Constructed); v.Dispose (); } - Console.Error.WriteLine ($"# jonp: END ActivatedDirectThrowableSubclassesShouldBeRegistered!!!"); } [Test] diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Lang/ObjectTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Lang/ObjectTest.cs index b4d921acd17..764fc416379 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Lang/ObjectTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Lang/ObjectTest.cs @@ -19,7 +19,6 @@ namespace Java.LangTests [TestFixture] public class ObjectTest { - // TODO: https://github.com/dotnet/android/issues/11170 — trimmable typemap doesn't resolve most-derived managed type [Test] public void GetObject_ReturnsMostDerivedType () { 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 d3783cdee0b..573940a6250 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 @@ -43,12 +43,6 @@ protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) // Open generic type handling differs from non-trimmable "Java.InteropTests.JnienvTest.NewOpenGenericTypeThrows", - // Throwable subclass registration - "Java.InteropTests.JnienvTest.ActivatedDirectThrowableSubclassesShouldBeRegistered", - - // Instance identity after JNI round-trip - "Java.LangTests.ObjectTest.JnienvCreateInstance_RegistersMultipleInstances", - // Global ref leak when inflating custom views "Xamarin.Android.RuntimeTests.CustomWidgetTests.InflateCustomView_ShouldNotLeakGlobalRefs", }; From 82ae8f0a1e986721ecf2f014d7009024bb9e0dc1 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 3 May 2026 22:31:31 +0200 Subject: [PATCH 2/2] Generate direct default constructor activation Avoid reflection for generated no-arg Java constructor callbacks by emitting IL that attaches the JNI peer and invokes the managed default constructor directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyEmitter.cs | 73 ++++++++++++++++--- .../Java.Interop/JavaPeerProxy.cs | 15 ---- 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 8f2334d63a3..7f9585bb197 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -97,9 +97,9 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _getUninitializedObjectRef; MemberReferenceHandle _notSupportedExceptionCtorRef; MemberReferenceHandle _jniObjectReferenceCtorRef; + MemberReferenceHandle _setPeerReferenceRef; MemberReferenceHandle _jniEnvDeleteRefRef; MemberReferenceHandle _shouldSkipActivationRef; - MemberReferenceHandle _activateDefaultConstructorRef; MemberReferenceHandle _markActivationPeerReplaceableRef; MemberReferenceHandle _waitForBridgeProcessingRef; MemberReferenceHandle _androidEnvironmentUnhandledExceptionRef; @@ -310,6 +310,11 @@ void EmitMemberReferences () p.AddParameter ().Type ().Type (_jniObjectReferenceTypeRef, true); })); + _setPeerReferenceRef = _pe.AddMemberRef (_iJavaPeerableRef, "SetPeerReference", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().Type (_jniObjectReferenceRef, true))); + // JNIEnv.DeleteRef(IntPtr, JniHandleOwnership) — static, internal // Used by JI-style activation to clean up the original handle after constructing the peer. // Matches the legacy TypeManager.CreateProxy behavior. @@ -327,14 +332,6 @@ void EmitMemberReferences () rt => rt.Type ().Boolean (), p => { p.AddParameter ().Type ().IntPtr (); })); - _activateDefaultConstructorRef = _pe.AddMemberRef (_javaPeerProxyNonGenericRef, "ActivateDefaultConstructor", - sig => sig.MethodSignature ().Parameters (2, - rt => rt.Void (), - p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().Type (_systemTypeRef, false); - })); - _markActivationPeerReplaceableRef = _pe.AddMemberRef (_javaPeerProxyNonGenericRef, "MarkActivationPeerReplaceable", sig => sig.MethodSignature ().Parameters (1, rt => rt.Void (), @@ -883,6 +880,14 @@ MemberReferenceHandle AddActivationCtorRef (EntityHandle declaringTypeRef) })); } + MemberReferenceHandle AddDefaultCtorRef (EntityHandle declaringTypeRef) + { + return _pe.AddMemberRef (declaringTypeRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, + rt => rt.Void (), + p => { })); + } + MethodDefinitionHandle EmitUcoMethod (UcoMethodData uco, JavaPeerProxyData proxy) { var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature); @@ -1004,17 +1009,36 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy } if (jniParams.Count == 0) { + var defaultCtorRef = AddDefaultCtorRef (targetTypeRef); var defaultCtorHandle = _pe.EmitBody (uco.WrapperName, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, encodeSig, (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { - enc.LoadArgument (1); // self enc.OpCode (ILOpCode.Ldtoken); enc.Token (targetTypeRef); enc.Call (_getTypeFromHandleRef); - enc.Call (_activateDefaultConstructorRef); + enc.Call (_getUninitializedObjectRef); + enc.OpCode (ILOpCode.Castclass); + enc.Token (targetTypeRef); + enc.StoreLocal (4); + + enc.LoadLocalAddress (3); // jniRef + enc.LoadArgument (1); // self + enc.LoadConstantI4 (0); // JniObjectReferenceType.Invalid + enc.Call (_jniObjectReferenceCtorRef); + + enc.LoadLocal (4); + enc.LoadLocal (3); + enc.OpCode (ILOpCode.Callvirt); + enc.Token (_setPeerReferenceRef); + + enc.LoadLocal (4); + enc.Call (defaultCtorRef); + + enc.LoadArgument (1); // self + enc.Call (_markActivationPeerReplaceableRef); }), - EncodeUcoConstructorLocals_Standard); + blob => EncodeUcoConstructorLocals_DefaultConstructor (blob, targetTypeRef)); AddUnmanagedCallersOnlyAttribute (defaultCtorHandle); return defaultCtorHandle; } @@ -1215,6 +1239,31 @@ void EncodeUcoConstructorLocals_JavaInterop (BlobBuilder blob) blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniObjectReferenceRef)); } + /// + /// LOCAL_SIG for UCO constructors that invoke a no-arg managed constructor. + /// Locals: 0=JniTransition, 1=JniRuntime, 2=Exception, 3=JniObjectReference, 4=target type. + /// + void EncodeUcoConstructorLocals_DefaultConstructor (BlobBuilder blob, EntityHandle targetTypeRef) + { + blob.WriteByte (0x07); // LOCAL_SIG + blob.WriteCompressedInteger (5); + // local 0: JniTransition (valuetype) + blob.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniTransitionRef)); + // local 1: JniRuntime (class) + blob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniRuntimeRef)); + // local 2: Exception (class) + blob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_exceptionRef)); + // local 3: JniObjectReference (valuetype) + blob.WriteByte (0x11); // ELEMENT_TYPE_VALUETYPE + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniObjectReferenceRef)); + // local 4: target type (class) + blob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (targetTypeRef)); + } + void EmitRegisterNatives (JavaPeerProxyData proxy, Dictionary wrapperHandles) { diff --git a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs index ff1120943be..190f1c597f7 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs @@ -2,7 +2,6 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Reflection; using Android.Runtime; namespace Java.Interop @@ -105,20 +104,6 @@ public static bool ShouldSkipActivation (IntPtr jniSelf) && (state & JniManagedPeerStates.Replaceable) != JniManagedPeerStates.Replaceable; } - public static void ActivateDefaultConstructor ( - IntPtr jniSelf, - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - Type targetType) - { - var cinfo = targetType.GetConstructor (BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, binder: null, Type.EmptyTypes, modifiers: null); - if (cinfo == null) { - throw new NotSupportedException ($"Unable to find a default constructor for type `{targetType.FullName}`."); - } - - JniEnvironment.Runtime.ValueManager.ActivatePeer (null, new JniObjectReference (jniSelf), cinfo, null); - MarkActivationPeerReplaceable (jniSelf); - } - public static void MarkActivationPeerReplaceable (IntPtr jniSelf) { var reference = new JniObjectReference (jniSelf, JniObjectReferenceType.Invalid);