From d7ad68f5a32d59e5fe9a9acf2aee57c10c59697b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Apr 2026 09:39:49 +0200 Subject: [PATCH 1/5] [TrimmableTypeMap] Fix UCO constructor activation parity with legacy Refactor UCO constructor wrappers to match legacy ManagedPeer.Construct activation behavior: - Add WithinNewObjectScope guard: skip activation when the object is being created from managed code (JNIEnv.StartCreateInstance/NewObject), preventing double peer creation and GC crashes. - For non-leaf types (activation ctor on a base type), call ActivatePeerFromJavaConstructor at runtime instead of inline IL. This finds the matching managed ctor for the JNI signature (e.g., parameterless for "()V") and invokes it via ActivatePeer, preserving user constructor side effects like field initialization. - For leaf types, inline the activation ctor call directly in the UCO with the WithinNewObjectScope guard. - Emit no-op UCO for open generic type definitions (type params unknown). - Add ControlFlowBuilder support to PEAssemblyBuilder.EmitBody for branch instructions (useBranches parameter). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/PEAssemblyBuilder.cs | 8 +- .../Generator/TypeMapAssemblyEmitter.cs | 149 ++++++++++++++---- .../TrimmableTypeMap.cs | 72 +++++++++ 3 files changed, 195 insertions(+), 34 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 56ae75638b2..e08caa319a1 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -272,6 +272,11 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, Action encodeSig, Action emitIL, Action? encodeLocals) + => EmitBody (name, attrs, encodeSig, emitIL, encodeLocals, useBranches: false); + + public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, + Action encodeSig, Action emitIL, + Action? encodeLocals, bool useBranches) { _sigBlob.Clear (); encodeSig (new BlobEncoder (_sigBlob)); @@ -287,7 +292,8 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, } _codeBlob.Clear (); - var encoder = new InstructionEncoder (_codeBlob); + ControlFlowBuilder? cfb = useBranches ? new ControlFlowBuilder () : null; + var encoder = new InstructionEncoder (_codeBlob, cfb); emitIL (encoder); while (ILBuilder.Count % 4 != 0) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 34bfd15a9b6..bc647058c5a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -77,7 +77,6 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _systemTypeRef; TypeReferenceHandle _runtimeTypeHandleRef; TypeReferenceHandle _jniTypeRef; - TypeReferenceHandle _trimmableNativeRegistrationRef; TypeReferenceHandle _notSupportedExceptionRef; TypeReferenceHandle _runtimeHelpersRef; @@ -87,7 +86,8 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _notSupportedExceptionCtorRef; MemberReferenceHandle _jniObjectReferenceCtorRef; MemberReferenceHandle _jniEnvDeleteRefRef; - MemberReferenceHandle _activateInstanceRef; + MemberReferenceHandle _withinNewObjectScopeRef; + MemberReferenceHandle _activatePeerFromJavaConstructorRef; MemberReferenceHandle _ucoAttrCtorRef; BlobHandle _ucoAttrBlobHandle; MemberReferenceHandle _typeMapAttrCtorRef2Arg; @@ -184,8 +184,6 @@ void EmitTypeReferences () metadata.GetOrAddString ("System"), metadata.GetOrAddString ("RuntimeTypeHandle")); _jniTypeRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniType")); - _trimmableNativeRegistrationRef = metadata.AddTypeReference (_pe.MonoAndroidRef, - metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("TrimmableNativeRegistration")); _notSupportedExceptionRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException")); _runtimeHelpersRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, @@ -240,12 +238,22 @@ void EmitMemberReferences () p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); })); - _activateInstanceRef = _pe.AddMemberRef (_trimmableNativeRegistrationRef, "ActivateInstance", - sig => sig.MethodSignature ().Parameters (2, + // JniEnvironment.get_WithinNewObjectScope() -> bool (static property) + _withinNewObjectScopeRef = _pe.AddMemberRef (_jniEnvironmentRef, "get_WithinNewObjectScope", + sig => sig.MethodSignature ().Parameters (0, + rt => rt.Type ().Boolean (), + p => { })); + + // TrimmableTypeMap.ActivatePeerFromJavaConstructor(Type, IntPtr, string) + var trimmableTypeMapRef = _pe.Metadata.AddTypeReference (_pe.MonoAndroidRef, + _pe.Metadata.GetOrAddString ("Microsoft.Android.Runtime"), _pe.Metadata.GetOrAddString ("TrimmableTypeMap")); + _activatePeerFromJavaConstructorRef = _pe.AddMemberRef (trimmableTypeMapRef, "ActivatePeerFromJavaConstructor", + sig => sig.MethodSignature ().Parameters (3, rt => rt.Void (), p => { - p.AddParameter ().Type ().IntPtr (); p.AddParameter ().Type ().Type (_systemTypeRef, false); + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().String (); })); // JniNativeMethod..ctor(byte*, byte*, IntPtr) @@ -390,7 +398,7 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary), - // so the wrapper signature must include all parameters to match the ABI. - // Only jnienv (arg 0) and self (arg 1) are used — the constructor parameters - // are not forwarded because ActivateInstance creates the managed peer using the - // activation ctor (IntPtr, JniHandleOwnership), not the user-visible constructor. var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature); int paramCount = 2 + jniParams.Count; - var handle = _pe.EmitBody (uco.WrapperName, - MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - sig => sig.MethodSignature ().Parameters (paramCount, - rt => rt.Void (), - p => { - p.AddParameter ().Type ().IntPtr (); // jnienv - p.AddParameter ().Type ().IntPtr (); // self - for (int j = 0; j < jniParams.Count; j++) - JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); - }), - encoder => { - encoder.LoadArgument (1); // self - encoder.OpCode (ILOpCode.Ldtoken); - encoder.Token (userTypeRef); - encoder.Call (_getTypeFromHandleRef); - encoder.Call (_activateInstanceRef); - encoder.OpCode (ILOpCode.Ret); + Action encodeSig = sig => sig.MethodSignature ().Parameters (paramCount, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().IntPtr (); // jnienv + p.AddParameter ().Type ().IntPtr (); // self + for (int j = 0; j < jniParams.Count; j++) + JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); }); + // Open generic types can't be activated — emit a no-op UCO. + if (proxy.IsGenericDefinition) { + var noopHandle = _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + encoder => { + encoder.OpCode (ILOpCode.Ret); + }); + AddUnmanagedCallersOnlyAttribute (noopHandle); + return noopHandle; + } + + MethodDefinitionHandle handle; + + // For types where the activation ctor is NOT on the leaf type, use the runtime + // ActivatePeerFromJavaConstructor helper which matches legacy ManagedPeer.Construct + // behavior: finds the managed ctor matching the JNI signature, creates an + // uninitialized instance of the correct target type, sets peer reference, and + // invokes the matching ctor (preserving user ctor side effects). + if (!activationCtor.IsOnLeafType) { + handle = _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + encoder => { + var skipLabel = encoder.DefineLabel (); + encoder.Call (_withinNewObjectScopeRef); + encoder.Branch (ILOpCode.Brtrue, skipLabel); + + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (userTypeRef); + encoder.Call (_getTypeFromHandleRef); + encoder.LoadArgument (1); // jniSelf + encoder.LoadString (_pe.Metadata.GetOrAddUserString (uco.JniSignature)); + encoder.Call (_activatePeerFromJavaConstructorRef); + + encoder.MarkLabel (skipLabel); + encoder.OpCode (ILOpCode.Ret); + }, + encodeLocals: null, + useBranches: true); + } else if (activationCtor.Style == ActivationCtorStyle.JavaInterop) { + var ctorRef = AddJavaInteropActivationCtorRef (userTypeRef); + + handle = _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + encoder => { + var skipLabel = encoder.DefineLabel (); + encoder.Call (_withinNewObjectScopeRef); + encoder.Branch (ILOpCode.Brtrue, skipLabel); + + encoder.LoadLocalAddress (0); + encoder.LoadArgument (1); // self + encoder.Call (_jniObjectReferenceCtorRef); + + encoder.LoadLocalAddress (0); + encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (ctorRef); + encoder.OpCode (ILOpCode.Pop); + + encoder.MarkLabel (skipLabel); + encoder.OpCode (ILOpCode.Ret); + }, + EncodeJniObjectReferenceLocal, + useBranches: true); + } else { + var ctorRef = AddActivationCtorRef (userTypeRef); + + handle = _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + encoder => { + var skipLabel = encoder.DefineLabel (); + encoder.Call (_withinNewObjectScopeRef); + encoder.Branch (ILOpCode.Brtrue, skipLabel); + + encoder.LoadArgument (1); // self + encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (ctorRef); + encoder.OpCode (ILOpCode.Pop); + + encoder.MarkLabel (skipLabel); + encoder.OpCode (ILOpCode.Ret); + }, + encodeLocals: null, + useBranches: true); + } + AddUnmanagedCallersOnlyAttribute (handle); return handle; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index f2c535dd8c7..0bb0feef90c 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -208,4 +208,76 @@ static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHa Environment.FailFast ($"TrimmableTypeMap: Failed to register natives for class '{className}'.", ex); } } + + /// + /// Activates a managed peer from a Java constructor call. Called by UCO constructor + /// wrappers to match the legacy ManagedPeer.Construct behavior: for parameterless + /// constructors, invokes the user's managed ctor (preserving side effects); for + /// parameterized constructors, falls back to the activation ctor from the hierarchy. + /// + internal static void ActivatePeerFromJavaConstructor ( + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type targetType, + IntPtr jniSelf, + string jniCtorSignature) + { + var reference = new JniObjectReference (jniSelf); + + var existingPeer = JniEnvironment.Runtime.ValueManager.PeekPeer (reference); + if (existingPeer is not null) { + var state = existingPeer.JniManagedPeerState; + if ((state & JniManagedPeerStates.Activatable) != JniManagedPeerStates.Activatable && + (state & JniManagedPeerStates.Replaceable) != JniManagedPeerStates.Replaceable) { + return; + } + } + + // For parameterless constructors, prefer the user's parameterless ctor + // to preserve side effects (e.g., field initialization). + if (jniCtorSignature == "()V") { + var paramlessCtor = targetType.GetConstructor ( + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + null, + Type.EmptyTypes, + null); + if (paramlessCtor is not null) { + JniEnvironment.Runtime.ValueManager.ActivatePeer (existingPeer, reference, paramlessCtor, null); + return; + } + } + + // For parameterized constructors, fall back to activation ctor from the hierarchy. + // Create the target type (not the base type) and invoke the base activation ctor. + var activationCtor = FindActivationConstructor (targetType); + if (activationCtor is not null) { + var self = (IJavaPeerable) System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject (targetType); + self.SetPeerReference (reference); + activationCtor.Invoke (self, [jniSelf, JniHandleOwnership.DoNotTransfer]); + return; + } + + throw new NotSupportedException ( + $"Unable to find constructor matching JNI signature '{jniCtorSignature}' or activation constructor for type '{targetType.FullName}'."); + } + + static readonly Type[] ActivationCtorTypes = [typeof (IntPtr), typeof (JniHandleOwnership)]; + + [UnconditionalSuppressMessage ("Trimming", "IL2072", Justification = "Java.Lang.Object subclass constructors are preserved by the MarkJavaObjects trimmer step.")] + [UnconditionalSuppressMessage ("Trimming", "IL2075", Justification = "Java.Lang.Object subclass constructors are preserved by the MarkJavaObjects trimmer step.")] + static ConstructorInfo? FindActivationConstructor ( + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type type) + { + for (var t = type; t is not null; t = t.BaseType) { + var ctor = t.GetConstructor ( + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly, + null, + ActivationCtorTypes, + null); + if (ctor is not null) { + return ctor; + } + } + return null; + } } From cfbf866136c611a68b24dc8879df9d90ac33ef45 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Apr 2026 17:28:14 +0200 Subject: [PATCH 2/5] [TrimmableTypeMap] Remove UCO reflection fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyEmitter.cs | 23 +++--- .../TrimmableTypeMap.cs | 71 ------------------- .../TypeMapAssemblyGeneratorTests.cs | 18 +++++ 3 files changed, 27 insertions(+), 85 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index bc647058c5a..19c32e80522 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -87,7 +87,7 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _jniObjectReferenceCtorRef; MemberReferenceHandle _jniEnvDeleteRefRef; MemberReferenceHandle _withinNewObjectScopeRef; - MemberReferenceHandle _activatePeerFromJavaConstructorRef; + MemberReferenceHandle _activateInstanceRef; MemberReferenceHandle _ucoAttrCtorRef; BlobHandle _ucoAttrBlobHandle; MemberReferenceHandle _typeMapAttrCtorRef2Arg; @@ -244,16 +244,15 @@ void EmitMemberReferences () rt => rt.Type ().Boolean (), p => { })); - // TrimmableTypeMap.ActivatePeerFromJavaConstructor(Type, IntPtr, string) + // TrimmableTypeMap.ActivateInstance(IntPtr, Type) var trimmableTypeMapRef = _pe.Metadata.AddTypeReference (_pe.MonoAndroidRef, _pe.Metadata.GetOrAddString ("Microsoft.Android.Runtime"), _pe.Metadata.GetOrAddString ("TrimmableTypeMap")); - _activatePeerFromJavaConstructorRef = _pe.AddMemberRef (trimmableTypeMapRef, "ActivatePeerFromJavaConstructor", - sig => sig.MethodSignature ().Parameters (3, + _activateInstanceRef = _pe.AddMemberRef (trimmableTypeMapRef, "ActivateInstance", + sig => sig.MethodSignature ().Parameters (2, rt => rt.Void (), p => { - p.AddParameter ().Type ().Type (_systemTypeRef, false); p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().String (); + p.AddParameter ().Type ().Type (_systemTypeRef, false); })); // JniNativeMethod..ctor(byte*, byte*, IntPtr) @@ -735,11 +734,8 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy MethodDefinitionHandle handle; - // For types where the activation ctor is NOT on the leaf type, use the runtime - // ActivatePeerFromJavaConstructor helper which matches legacy ManagedPeer.Construct - // behavior: finds the managed ctor matching the JNI signature, creates an - // uninitialized instance of the correct target type, sets peer reference, and - // invokes the matching ctor (preserving user ctor side effects). + // For non-leaf activation, keep the WithinNewObjectScope guard but route back + // through the generated proxy activation path instead of a runtime reflection helper. if (!activationCtor.IsOnLeafType) { handle = _pe.EmitBody (uco.WrapperName, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, @@ -749,12 +745,11 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy encoder.Call (_withinNewObjectScopeRef); encoder.Branch (ILOpCode.Brtrue, skipLabel); + encoder.LoadArgument (1); // jniSelf encoder.OpCode (ILOpCode.Ldtoken); encoder.Token (userTypeRef); encoder.Call (_getTypeFromHandleRef); - encoder.LoadArgument (1); // jniSelf - encoder.LoadString (_pe.Metadata.GetOrAddUserString (uco.JniSignature)); - encoder.Call (_activatePeerFromJavaConstructorRef); + encoder.Call (_activateInstanceRef); encoder.MarkLabel (skipLabel); encoder.OpCode (ILOpCode.Ret); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 0bb0feef90c..b227292746d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -209,75 +209,4 @@ static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHa } } - /// - /// Activates a managed peer from a Java constructor call. Called by UCO constructor - /// wrappers to match the legacy ManagedPeer.Construct behavior: for parameterless - /// constructors, invokes the user's managed ctor (preserving side effects); for - /// parameterized constructors, falls back to the activation ctor from the hierarchy. - /// - internal static void ActivatePeerFromJavaConstructor ( - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - Type targetType, - IntPtr jniSelf, - string jniCtorSignature) - { - var reference = new JniObjectReference (jniSelf); - - var existingPeer = JniEnvironment.Runtime.ValueManager.PeekPeer (reference); - if (existingPeer is not null) { - var state = existingPeer.JniManagedPeerState; - if ((state & JniManagedPeerStates.Activatable) != JniManagedPeerStates.Activatable && - (state & JniManagedPeerStates.Replaceable) != JniManagedPeerStates.Replaceable) { - return; - } - } - - // For parameterless constructors, prefer the user's parameterless ctor - // to preserve side effects (e.g., field initialization). - if (jniCtorSignature == "()V") { - var paramlessCtor = targetType.GetConstructor ( - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, - null, - Type.EmptyTypes, - null); - if (paramlessCtor is not null) { - JniEnvironment.Runtime.ValueManager.ActivatePeer (existingPeer, reference, paramlessCtor, null); - return; - } - } - - // For parameterized constructors, fall back to activation ctor from the hierarchy. - // Create the target type (not the base type) and invoke the base activation ctor. - var activationCtor = FindActivationConstructor (targetType); - if (activationCtor is not null) { - var self = (IJavaPeerable) System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject (targetType); - self.SetPeerReference (reference); - activationCtor.Invoke (self, [jniSelf, JniHandleOwnership.DoNotTransfer]); - return; - } - - throw new NotSupportedException ( - $"Unable to find constructor matching JNI signature '{jniCtorSignature}' or activation constructor for type '{targetType.FullName}'."); - } - - static readonly Type[] ActivationCtorTypes = [typeof (IntPtr), typeof (JniHandleOwnership)]; - - [UnconditionalSuppressMessage ("Trimming", "IL2072", Justification = "Java.Lang.Object subclass constructors are preserved by the MarkJavaObjects trimmer step.")] - [UnconditionalSuppressMessage ("Trimming", "IL2075", Justification = "Java.Lang.Object subclass constructors are preserved by the MarkJavaObjects trimmer step.")] - static ConstructorInfo? FindActivationConstructor ( - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - Type type) - { - for (var t = type; t is not null; t = t.BaseType) { - var ctor = t.GetConstructor ( - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly, - null, - ActivationCtorTypes, - null); - if (ctor is not null) { - return ctor; - } - } - return null; - } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index d892b0161da..10cedc025ed 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -188,6 +188,24 @@ public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () Assert.True (ctorRefs.Count >= 2, "Should have ctor refs for proxy base + target type"); } + [Fact] + public void Generate_InheritedCtor_UcoUsesGuardWithoutReflectionFallback () + { + var peers = ScanFixtures (); + var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); + Assert.NotNull (simpleActivity.ActivationCtor); + Assert.NotEqual (simpleActivity.ManagedTypeName, simpleActivity.ActivationCtor.DeclaringTypeName); + + using var stream = GenerateAssembly (new [] { simpleActivity }, "InheritedCtorUcoTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var memberNames = GetMemberRefNames (reader); + + Assert.Contains ("get_WithinNewObjectScope", memberNames); + Assert.Contains ("ActivateInstance", memberNames); + Assert.DoesNotContain ("ActivatePeerFromJavaConstructor", memberNames); + } + [Fact] public void Generate_GenericType_ThrowsNotSupportedException () { From 8a427addab6fbb1d6b4a251ccc218d5cefab6294 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 8 Apr 2026 21:04:20 +0200 Subject: [PATCH 3/5] Inline activation ctor in UCO wrappers - UCO constructors directly call activation ctor (no ActivateInstance indirection) - WithinNewObjectScope guard prevents double peer creation - No-op UCO for open generic type definitions - ControlFlowBuilder support in PEAssemblyBuilder - Remove TrimmableNativeRegistration wrapper and ActivateInstance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/Model/TypeMapAssemblyData.cs | 4 +- .../Generator/PEAssemblyBuilder.cs | 9 +- .../Generator/TypeMapAssemblyEmitter.cs | 116 ++++++++++-------- .../TrimmableTypeMap.cs | 33 ----- .../TypeMapAssemblyGeneratorTests.cs | 5 +- 5 files changed, 77 insertions(+), 90 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 56484f7a99a..e2868a3b2c5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -182,7 +182,7 @@ sealed record UcoMethodData /// An [UnmanagedCallersOnly] static wrapper for a constructor callback. /// Signature must match the full JNI native method signature (jnienv + self + ctor params) /// so the ABI is correct when JNI dispatches the call. -/// Body: TrimmableNativeRegistration.ActivateInstance(self, typeof(TargetType)). +/// Body: directly activates the target type using its generated activation ctor. /// sealed record UcoConstructorData { @@ -192,7 +192,7 @@ sealed record UcoConstructorData public required string WrapperName { get; init; } /// - /// Target type to pass to ActivateInstance. + /// Target type to activate in the generated wrapper. /// public required TypeRefData TargetType { get; init; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index e08caa319a1..ddec657a085 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -239,7 +239,7 @@ TypeDefinitionHandle GetOrCreateSizedType (int size) int typeMethodStart = Metadata.GetRowCount (TableIndex.MethodDef) + 1; var handle = Metadata.AddTypeDefinition ( - TypeAttributes.NestedPrivate | TypeAttributes.ExplicitLayout | TypeAttributes.Sealed | TypeAttributes.AnsiClass, + TypeAttributes.NestedAssembly | TypeAttributes.ExplicitLayout | TypeAttributes.Sealed | TypeAttributes.AnsiClass, default, Metadata.GetOrAddString ($"__utf8_{size}"), Metadata.AddTypeReference (SystemRuntimeRef, @@ -259,7 +259,7 @@ TypeDefinitionHandle GetOrCreateSizedType (int size) /// public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, Action encodeSig, Action emitIL) - => EmitBody (name, attrs, encodeSig, emitIL, encodeLocals: null); + => EmitBody (name, attrs, encodeSig, emitIL, encodeLocals: null, useBranches: false); /// /// Emits a method body and definition with optional local variable declarations. @@ -269,6 +269,11 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, /// and must write the full LOCAL_SIG blob (header 0x07, /// compressed count, then each variable type). /// + /// + /// If true, creates a so the emitted IL can use + /// , , + /// and . + /// public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, Action encodeSig, Action emitIL, Action? encodeLocals) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 19c32e80522..63ccba17203 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -43,7 +43,9 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// /// [UnmanagedCallersOnly] /// public static void nctor_0_uco(IntPtr jnienv, IntPtr self) -/// => TrimmableNativeRegistration.ActivateInstance(self, typeof(Activity)); +/// => new Activity(self, JniHandleOwnership.DoNotTransfer); +/// // or: var obj = (Activity)RuntimeHelpers.GetUninitializedObject(typeof(Activity)); +/// // obj.BaseCtor(self, JniHandleOwnership.DoNotTransfer); /// /// // Registers JNI native methods (ACWs only): /// public void RegisterNatives(JniType jniType) @@ -87,7 +89,6 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _jniObjectReferenceCtorRef; MemberReferenceHandle _jniEnvDeleteRefRef; MemberReferenceHandle _withinNewObjectScopeRef; - MemberReferenceHandle _activateInstanceRef; MemberReferenceHandle _ucoAttrCtorRef; BlobHandle _ucoAttrBlobHandle; MemberReferenceHandle _typeMapAttrCtorRef2Arg; @@ -244,17 +245,6 @@ void EmitMemberReferences () rt => rt.Type ().Boolean (), p => { })); - // TrimmableTypeMap.ActivateInstance(IntPtr, Type) - var trimmableTypeMapRef = _pe.Metadata.AddTypeReference (_pe.MonoAndroidRef, - _pe.Metadata.GetOrAddString ("Microsoft.Android.Runtime"), _pe.Metadata.GetOrAddString ("TrimmableTypeMap")); - _activateInstanceRef = _pe.AddMemberRef (trimmableTypeMapRef, "ActivateInstance", - sig => sig.MethodSignature ().Parameters (2, - rt => rt.Void (), - p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().Type (_systemTypeRef, false); - })); - // JniNativeMethod..ctor(byte*, byte*, IntPtr) _jniNativeMethodCtorRef = _pe.AddMemberRef (_jniNativeMethodRef, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (3, @@ -354,6 +344,16 @@ void EmitTypeMapAssociationAttributeCtorRef () void EmitProxyType (JavaPeerProxyData proxy, Dictionary wrapperHandles) { + if (proxy.IsAcw) { + // RegisterNatives uses RVA-backed UTF-8 fields under . + // Materialize those helper types before adding the proxy TypeDef, otherwise the + // later RegisterNatives method can be attached to the helper type instead. + foreach (var reg in proxy.NativeRegistrations) { + _pe.GetOrAddUtf8Field (reg.JniMethodName); + _pe.GetOrAddUtf8Field (reg.JniSignature); + } + } + var metadata = _pe.Metadata; var typeDefHandle = metadata.AddTypeDefinition ( TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, @@ -368,7 +368,7 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }), encoder => { @@ -376,6 +376,7 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary { + // Skip activation if the object is being created from managed code + // (e.g., JNIEnv.StartCreateInstance / JNIEnv.NewObject). var skipLabel = encoder.DefineLabel (); encoder.Call (_withinNewObjectScopeRef); encoder.Branch (ILOpCode.Brtrue, skipLabel); - encoder.LoadArgument (1); // jniSelf - encoder.OpCode (ILOpCode.Ldtoken); - encoder.Token (userTypeRef); - encoder.Call (_getTypeFromHandleRef); - encoder.Call (_activateInstanceRef); - - encoder.MarkLabel (skipLabel); - encoder.OpCode (ILOpCode.Ret); - }, - encodeLocals: null, - useBranches: true); - } else if (activationCtor.Style == ActivationCtorStyle.JavaInterop) { - var ctorRef = AddJavaInteropActivationCtorRef (userTypeRef); - - handle = _pe.EmitBody (uco.WrapperName, - MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - encodeSig, - encoder => { - var skipLabel = encoder.DefineLabel (); - encoder.Call (_withinNewObjectScopeRef); - encoder.Branch (ILOpCode.Brtrue, skipLabel); + if (!activationCtor.IsOnLeafType) { + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (targetTypeRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_getUninitializedObjectRef); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (targetTypeRef); + } encoder.LoadLocalAddress (0); encoder.LoadArgument (1); // self encoder.Call (_jniObjectReferenceCtorRef); - encoder.LoadLocalAddress (0); - encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (ctorRef); - encoder.OpCode (ILOpCode.Pop); + if (activationCtor.IsOnLeafType) { + encoder.LoadLocalAddress (0); + encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (ctorRef); + encoder.OpCode (ILOpCode.Pop); + } else { + encoder.LoadLocalAddress (0); + encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy + encoder.Call (ctorRef); + } encoder.MarkLabel (skipLabel); encoder.OpCode (ILOpCode.Ret); @@ -783,21 +783,36 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy EncodeJniObjectReferenceLocal, useBranches: true); } else { - var ctorRef = AddActivationCtorRef (userTypeRef); + var ctorRef = AddActivationCtorRef ( + activationCtor.IsOnLeafType ? targetTypeRef : _pe.ResolveTypeRef (activationCtor.DeclaringType)); handle = _pe.EmitBody (uco.WrapperName, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, encodeSig, encoder => { + // Skip activation if the object is being created from managed code var skipLabel = encoder.DefineLabel (); encoder.Call (_withinNewObjectScopeRef); encoder.Branch (ILOpCode.Brtrue, skipLabel); - encoder.LoadArgument (1); // self - encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (ctorRef); - encoder.OpCode (ILOpCode.Pop); + if (activationCtor.IsOnLeafType) { + encoder.LoadArgument (1); // self + encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (ctorRef); + encoder.OpCode (ILOpCode.Pop); + } else { + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (targetTypeRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_getUninitializedObjectRef); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (targetTypeRef); + + encoder.LoadArgument (1); // self + encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer + encoder.Call (ctorRef); + } encoder.MarkLabel (skipLabel); encoder.OpCode (ILOpCode.Ret); @@ -805,7 +820,6 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy encodeLocals: null, useBranches: true); } - AddUnmanagedCallersOnlyAttribute (handle); return handle; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index b227292746d..4b46fee009c 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -147,39 +147,6 @@ internal bool TryCreatePeer (Type type, IntPtr handle, JniHandleOwnership transf return GetProxyForManagedType (type)?.GetContainerFactory (); } - /// - /// Creates a managed peer instance for a Java object being constructed. - /// Called from generated UCO constructor wrappers (nctor_*_uco). - /// - internal static void ActivateInstance (IntPtr self, Type targetType) - { - var instance = s_instance; - if (instance is null) { - throw new InvalidOperationException ("TrimmableTypeMap has not been initialized."); - } - - // Look up the proxy via JNI class name → TypeMap dictionary. - // We can't use targetType.GetCustomAttribute() because the - // self-application attribute is on the proxy type, not the target type. - var selfRef = new JniObjectReference (self); - var jniClass = JniEnvironment.Types.GetObjectClass (selfRef); - var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); - JniObjectReference.Dispose (ref jniClass); - - if (className is null || !instance._typeMap.TryGetValue (className, out var proxyType)) { - throw new InvalidOperationException ( - $"Failed to create peer for type '{targetType.FullName}' (jniClass='{className}'). " + - "Ensure the type has a generated proxy in the TypeMap assembly."); - } - - var proxy = proxyType.GetCustomAttribute (inherit: false); - if (proxy is null || proxy.CreateInstance (self, JniHandleOwnership.DoNotTransfer) is null) { - throw new InvalidOperationException ( - $"Failed to create peer for type '{targetType.FullName}'. " + - "Ensure the type has a generated proxy in the TypeMap assembly."); - } - } - [UnmanagedCallersOnly] static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 10cedc025ed..9853f392a56 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -189,7 +189,7 @@ public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () } [Fact] - public void Generate_InheritedCtor_UcoUsesGuardWithoutReflectionFallback () + public void Generate_InheritedCtor_UcoUsesGuardAndInlinedActivation () { var peers = ScanFixtures (); var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); @@ -202,7 +202,8 @@ public void Generate_InheritedCtor_UcoUsesGuardWithoutReflectionFallback () var memberNames = GetMemberRefNames (reader); Assert.Contains ("get_WithinNewObjectScope", memberNames); - Assert.Contains ("ActivateInstance", memberNames); + Assert.Contains ("GetUninitializedObject", memberNames); + Assert.DoesNotContain ("ActivateInstance", memberNames); Assert.DoesNotContain ("ActivatePeerFromJavaConstructor", memberNames); } From e62046529960a9f63aedf8a3b936ac95b135b46b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 9 Apr 2026 18:23:52 +0200 Subject: [PATCH 4/5] [TrimmableTypeMap] Remove invalid proxy attribute metadata Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyEmitter.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 63ccba17203..a74f4c4955f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -368,7 +368,7 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }), encoder => { @@ -376,7 +376,6 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary Date: Thu, 9 Apr 2026 22:36:09 +0200 Subject: [PATCH 5/5] [TrimmableTypeMap] Strengthen RegisterNatives regression test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TypeMapAssemblyGeneratorTests.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 9853f392a56..3141793ba84 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -441,14 +441,26 @@ public void Generate_AcwProxy_HasRegisterNativesAndUcoMethods () using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); - var memberNames = GetMemberRefNames (reader); + var proxyType = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Single (t => + reader.GetString (t.Namespace) == "_TypeMap.Proxies" && + reader.GetString (t.Name) == "MyApp_MainActivity_Proxy"); + var proxyMethodNames = proxyType.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Select (m => reader.GetString (m.Name)) + .ToList (); + Assert.Contains ("RegisterNatives", proxyMethodNames); + Assert.Contains (proxyMethodNames, name => name.Contains ("_uco_")); - // RegisterNatives is a method definition on the proxy type, not a member reference - var methodDefs = reader.MethodDefinitions + var privateImplDetailsType = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Single (t => reader.GetString (t.Name) == ""); + var privateImplMethodNames = privateImplDetailsType.GetMethods () .Select (h => reader.GetMethodDefinition (h)) .Select (m => reader.GetString (m.Name)) .ToList (); - Assert.Contains ("RegisterNatives", methodDefs); + Assert.DoesNotContain ("RegisterNatives", privateImplMethodNames); } [Fact]