From 6f24e4024b98bdcf12321bb5b4cec035f16afa20 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 3 May 2026 21:55:18 +0200 Subject: [PATCH 1/6] 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 67761e0d617..cc955df8db4 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -101,6 +101,8 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _jniObjectReferenceCtorRef; MemberReferenceHandle _jniEnvDeleteRefRef; MemberReferenceHandle _shouldSkipActivationRef; + MemberReferenceHandle _activateDefaultConstructorRef; + MemberReferenceHandle _markActivationPeerReplaceableRef; MemberReferenceHandle _waitForBridgeProcessingRef; MemberReferenceHandle _androidEnvironmentUnhandledExceptionRef; MemberReferenceHandle _ucoAttrCtorRef; @@ -363,6 +365,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 => { })); @@ -1041,6 +1056,22 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy return openGenericHandle; } + 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 ( @@ -1077,6 +1108,8 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy enc.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy enc.Call (ctorRef, parameterCount: 2, isInstance: true); } + enc.LoadArgument (1); // self + enc.Call (_markActivationPeerReplaceableRef); }), EncodeUcoConstructorLocals_JavaInterop); } else { @@ -1106,6 +1139,8 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy enc.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer enc.Call (ctorRef, parameterCount: 2, isInstance: true); } + 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 0eacc4eddad..13fa0962b58 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 @@ -301,14 +301,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"); @@ -324,7 +321,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 966903726aa..b03e62bb551 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 @@ -34,12 +34,6 @@ protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) // net.dot.jni.test.CallVirtualFromConstructorDerived Java class not in APK "Java.InteropTests.InvokeVirtualFromConstructorTests", - // 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 324b7f0a2af9b5a123d5a67aa8692b7f5aae1ce9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 3 May 2026 22:31:31 +0200 Subject: [PATCH 2/6] 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 cc955df8db4..4662a3798aa 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -99,9 +99,9 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _getUninitializedObjectRef; MemberReferenceHandle _notSupportedExceptionCtorRef; MemberReferenceHandle _jniObjectReferenceCtorRef; + MemberReferenceHandle _setPeerReferenceRef; MemberReferenceHandle _jniEnvDeleteRefRef; MemberReferenceHandle _shouldSkipActivationRef; - MemberReferenceHandle _activateDefaultConstructorRef; MemberReferenceHandle _markActivationPeerReplaceableRef; MemberReferenceHandle _waitForBridgeProcessingRef; MemberReferenceHandle _androidEnvironmentUnhandledExceptionRef; @@ -348,6 +348,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. @@ -365,14 +370,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 (), @@ -933,6 +930,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); @@ -1057,17 +1062,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; } @@ -1261,6 +1285,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); From b22aef280ca6a3b7679d760a19df1fc3bfb89a9e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 12 May 2026 08:19:40 +0200 Subject: [PATCH 3/6] Port default-ctor nctor IL to TrackedInstructionEncoder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After rebasing onto main (which merged #11260 adding maxstack tracking), the new 0-arg nctor codegen path in EmitUcoMethod was using the pre-#11260 bare OpCode + Token style. Switch to the tracked encoder helpers (LoadToken, CastClass, Call(parameterCount,…), Callvirt(parameterCount,…)) so the emitted method's maxstack is computed correctly, matching every other IL site in TypeMapAssemblyEmitter. Also update Generate_InheritedCtor_* / Generate_InheritedJavaInteropCtor_* generator tests to reflect the new nctor contract: the default-ctor branch references the target type's parameterless .ctor(), SetPeerReference and MarkActivationPeerReplaceable — and no longer references the legacy (IntPtr, JniHandleOwnership) / (JniObjectReference&, JniObjectReferenceOptions) activation ctor on the inherited base. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyEmitter.cs | 23 ++++++++----------- .../TypeMapAssemblyGeneratorTests.cs | 21 +++++++++++++---- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 4662a3798aa..fc7bbf647fd 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -1067,29 +1067,26 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, encodeSig, (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { - enc.OpCode (ILOpCode.Ldtoken); - enc.Token (targetTypeRef); - enc.Call (_getTypeFromHandleRef); - enc.Call (_getUninitializedObjectRef); - enc.OpCode (ILOpCode.Castclass); - enc.Token (targetTypeRef); + enc.LoadToken (targetTypeRef); + enc.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); + enc.Call (_getUninitializedObjectRef, parameterCount: 1, returnsValue: true); + enc.CastClass (targetTypeRef); enc.StoreLocal (4); enc.LoadLocalAddress (3); // jniRef enc.LoadArgument (1); // self enc.LoadConstantI4 (0); // JniObjectReferenceType.Invalid - enc.Call (_jniObjectReferenceCtorRef); + enc.Call (_jniObjectReferenceCtorRef, parameterCount: 2, isInstance: true); enc.LoadLocal (4); enc.LoadLocal (3); - enc.OpCode (ILOpCode.Callvirt); - enc.Token (_setPeerReferenceRef); + enc.Callvirt (_setPeerReferenceRef, parameterCount: 1); enc.LoadLocal (4); - enc.Call (defaultCtorRef); + enc.Call (defaultCtorRef, parameterCount: 0, isInstance: true); enc.LoadArgument (1); // self - enc.Call (_markActivationPeerReplaceableRef); + enc.Call (_markActivationPeerReplaceableRef, parameterCount: 1); }), blob => EncodeUcoConstructorLocals_DefaultConstructor (blob, targetTypeRef)); AddUnmanagedCallersOnlyAttribute (defaultCtorHandle); @@ -1133,7 +1130,7 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy enc.Call (ctorRef, parameterCount: 2, isInstance: true); } enc.LoadArgument (1); // self - enc.Call (_markActivationPeerReplaceableRef); + enc.Call (_markActivationPeerReplaceableRef, parameterCount: 1); }), EncodeUcoConstructorLocals_JavaInterop); } else { @@ -1164,7 +1161,7 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy enc.Call (ctorRef, parameterCount: 2, isInstance: true); } enc.LoadArgument (1); // self - enc.Call (_markActivationPeerReplaceableRef); + enc.Call (_markActivationPeerReplaceableRef, parameterCount: 1); }), EncodeUcoConstructorLocals_Standard); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index e84d8bcf392..008ce57994d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -288,7 +288,7 @@ public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () } [Fact] - public void Generate_InheritedCtor_ReferencesGuardAndActivationCtor () + public void Generate_InheritedCtor_NctorUcoCallsDefaultConstructor () { var peers = ScanFixtures (); var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); @@ -302,18 +302,24 @@ public void Generate_InheritedCtor_ReferencesGuardAndActivationCtor () Assert.Contains ("ShouldSkipActivation", memberNames); Assert.Contains ("GetUninitializedObject", memberNames); + Assert.Contains ("SetPeerReference", memberNames); + Assert.Contains ("MarkActivationPeerReplaceable", memberNames); Assert.DoesNotContain ("Invoke", memberNames); Assert.DoesNotContain ("ActivateInstance", memberNames); Assert.DoesNotContain ("ActivatePeerFromJavaConstructor", memberNames); - Assert.NotEmpty (FindCtorMemberRefs (reader, "Android.App", "Activity", + // The new no-arg nctor codegen calls the target type's parameterless .ctor() + // directly, not the legacy (IntPtr, JniHandleOwnership) activation ctor on the base. + Assert.NotEmpty (FindCtorMemberRefs (reader, "MyApp", "SimpleActivity")); + Assert.Empty (FindCtorMemberRefs (reader, "Android.App", "Activity", "System.IntPtr", "Android.Runtime.JniHandleOwnership")); + var nctorMethodHandle = FindNctorUcoMethod (reader); Assert.False (nctorMethodHandle.IsNil, "SimpleActivity should have a nctor_*_uco method"); } [Fact] - public void Generate_InheritedJavaInteropCtor_ReferencesActivationCtor () + public void Generate_InheritedJavaInteropCtor_NctorUcoCallsDefaultConstructor () { var peer = MakeAcwPeer ("test/JiInheritedTarget", "Test.JiInheritedTarget", "TestAsm") with { ActivationCtor = new ActivationCtorInfo { @@ -332,10 +338,17 @@ public void Generate_InheritedJavaInteropCtor_ReferencesActivationCtor () var memberNames = GetMemberRefNames (reader); Assert.Contains ("GetUninitializedObject", memberNames); + Assert.Contains ("SetPeerReference", memberNames); + Assert.Contains ("MarkActivationPeerReplaceable", memberNames); Assert.DoesNotContain ("Invoke", memberNames); - Assert.NotEmpty (FindCtorMemberRefs (reader, "Test", "JiInheritedBase", + // The new no-arg nctor codegen calls the target type's parameterless .ctor() + // directly, not the JI-style (JniObjectReference&, JniObjectReferenceOptions) + // activation ctor on the inherited base. + Assert.NotEmpty (FindCtorMemberRefs (reader, "Test", "JiInheritedTarget")); + Assert.Empty (FindCtorMemberRefs (reader, "Test", "JiInheritedBase", "Java.Interop.JniObjectReference&", "Java.Interop.JniObjectReferenceOptions")); + var nctorMethodHandle = FindNctorUcoMethod (reader); Assert.False (nctorMethodHandle.IsNil, "The ACW peer should have a nctor_*_uco method"); } From ba3d68afe6bc8593b43bb74d35c7c5e7e2893046 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 12 May 2026 13:23:18 +0200 Subject: [PATCH 4/6] Implement trimmable constructor activation parity Add broad constructor activation coverage and generate direct constructor UCO activation for default and parameterized Java constructors. The generated path now reuses activation peers, skips managed construction recursion, forwards JNI constructor arguments, and stamps the JNI identity hash on freshly activated peers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/JniSignatureHelper.cs | 5 +- .../Generator/Model/TypeMapAssemblyData.cs | 11 + .../Generator/ModelBuilder.cs | 2 + .../Generator/TypeMapAssemblyEmitter.cs | 236 ++++- .../Scanner/JavaPeerInfo.cs | 15 + .../Scanner/JavaPeerScanner.cs | 25 + .../Java.Interop/JavaPeerProxy.cs | 29 +- .../Generator/FixtureTestBase.cs | 1 + .../TypeMapAssemblyGeneratorTests.cs | 97 ++- .../ConstructorActivationTests.cs | 807 ++++++++++++++++++ .../Mono.Android.NET-Tests.csproj | 1 + .../test/ConstructorActivationBase.java | 21 + 12 files changed, 1225 insertions(+), 25 deletions(-) create mode 100644 tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs create mode 100644 tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/ConstructorActivationBase.java diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index c9b620ff18f..afa9ea3c3d9 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -109,14 +109,15 @@ static JniParamKind ParseSingleType (string sig, ref int i) /// Encodes a JNI type as its CLR equivalent for [UnmanagedCallersOnly] UCO wrapper signatures. /// /// - /// JNI boolean (Z) maps to byte (unsigned, blittable for the JNI ABI). + /// JNI boolean (Z) maps to byte and JNI char (C) maps to ushort, + /// preserving the JNI ABI with blittable UCO parameter types. /// public static void EncodeClrType (SignatureTypeEncoder encoder, JniParamKind kind) { switch (kind) { case JniParamKind.Boolean: encoder.Byte (); break; // JNI jboolean is unsigned byte; blittable for UCO case JniParamKind.Byte: encoder.SByte (); break; - case JniParamKind.Char: encoder.Char (); break; + case JniParamKind.Char: encoder.UInt16 (); break; case JniParamKind.Short: encoder.Int16 (); break; case JniParamKind.Int: encoder.Int32 (); break; case JniParamKind.Long: encoder.Int64 (); break; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index b9126586bf4..c30e9006513 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -204,6 +204,7 @@ sealed record UcoMethodData /// JNI method signature, e.g., "(Landroid/os/Bundle;)V". Used to determine CLR parameter types. /// public required string JniSignature { get; init; } + } /// @@ -228,6 +229,16 @@ sealed record UcoConstructorData /// JNI constructor signature, e.g., "(Landroid/content/Context;)V". Used for RegisterNatives registration. /// public required string JniSignature { get; init; } + + /// + /// True if the target type declares a public parameterless managed constructor. + /// + public bool HasPublicParameterlessConstructor { get; init; } + + /// + /// Managed constructor parameter type names, in declaration order. + /// + public IReadOnlyList ManagedParameterTypes { get; init; } = []; } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 7547c5ac38f..2cbd9f2564a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -365,6 +365,8 @@ static void BuildUcoConstructors (JavaPeerInfo peer, JavaPeerProxyData proxy) ManagedTypeName = peer.ManagedTypeName, AssemblyName = peer.AssemblyName, }, + HasPublicParameterlessConstructor = peer.HasPublicParameterlessConstructor, + ManagedParameterTypes = ctor.ManagedParameterTypes, }); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index fc7bbf647fd..28aa87644c9 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -87,7 +87,9 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _jniObjectReferenceOptionsRef; TypeReferenceHandle _iAndroidCallableWrapperRef; TypeReferenceHandle _jniEnvRef; + TypeReferenceHandle _javaLangObjectRef; TypeReferenceHandle _systemTypeRef; + TypeReferenceHandle _systemArrayRef; TypeReferenceHandle _runtimeTypeHandleRef; TypeReferenceHandle _jniTypeRef; TypeReferenceHandle _notSupportedExceptionRef; @@ -99,9 +101,13 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _getUninitializedObjectRef; MemberReferenceHandle _notSupportedExceptionCtorRef; MemberReferenceHandle _jniObjectReferenceCtorRef; - MemberReferenceHandle _setPeerReferenceRef; MemberReferenceHandle _jniEnvDeleteRefRef; + MemberReferenceHandle _jniEnvGetStringRef; + MemberReferenceHandle _jniEnvGetArrayRef; + MemberReferenceHandle _javaLangObjectGetObjectRef; MemberReferenceHandle _shouldSkipActivationRef; + MemberReferenceHandle _getActivationPeerRef; + MemberReferenceHandle _setActivationPeerReferenceRef; MemberReferenceHandle _markActivationPeerReplaceableRef; MemberReferenceHandle _waitForBridgeProcessingRef; MemberReferenceHandle _androidEnvironmentUnhandledExceptionRef; @@ -230,6 +236,8 @@ void EmitTypeReferences () metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JniHandleOwnership")); _jniEnvRef = metadata.AddTypeReference (_pe.MonoAndroidRef, metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JNIEnv")); + _javaLangObjectRef = metadata.AddTypeReference (_pe.MonoAndroidRef, + metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object")); _jniObjectReferenceRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniObjectReference")); _jniObjectReferenceTypeRef = metadata.AddTypeReference (_javaInteropRef, @@ -240,6 +248,8 @@ void EmitTypeReferences () metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IAndroidCallableWrapper")); _systemTypeRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Type")); + _systemArrayRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Array")); _runtimeTypeHandleRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("RuntimeTypeHandle")); _jniTypeRef = metadata.AddTypeReference (_javaInteropRef, @@ -348,11 +358,6 @@ 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. @@ -364,12 +369,51 @@ void EmitMemberReferences () p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); })); + _jniEnvGetStringRef = _pe.AddMemberRef (_jniEnvRef, "GetString", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Type ().String (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + })); + + _jniEnvGetArrayRef = _pe.AddMemberRef (_jniEnvRef, "GetArray", + sig => sig.MethodSignature ().Parameters (3, + rt => rt.Type ().Type (_systemArrayRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); + + _javaLangObjectGetObjectRef = _pe.AddMemberRef (_javaLangObjectRef, "GetObject", + sig => sig.MethodSignature ().Parameters (3, + rt => rt.Type ().Type (_iJavaPeerableRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); + // JavaPeerProxy.ShouldSkipActivation(IntPtr) -> bool (static method) _shouldSkipActivationRef = _pe.AddMemberRef (_javaPeerProxyNonGenericRef, "ShouldSkipActivation", sig => sig.MethodSignature ().Parameters (1, rt => rt.Type ().Boolean (), p => { p.AddParameter ().Type ().IntPtr (); })); + _getActivationPeerRef = _pe.AddMemberRef (_javaPeerProxyNonGenericRef, "GetActivationPeer", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Type ().Type (_iJavaPeerableRef, false), + p => { p.AddParameter ().Type ().IntPtr (); })); + + _setActivationPeerReferenceRef = _pe.AddMemberRef (_javaPeerProxyNonGenericRef, "SetActivationPeerReference", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().Type (_iJavaPeerableRef, false); + p.AddParameter ().Type ().IntPtr (); + })); + _markActivationPeerReplaceableRef = _pe.AddMemberRef (_javaPeerProxyNonGenericRef, "MarkActivationPeerReplaceable", sig => sig.MethodSignature ().Parameters (1, rt => rt.Void (), @@ -938,6 +982,18 @@ MemberReferenceHandle AddDefaultCtorRef (EntityHandle declaringTypeRef) p => { })); } + MemberReferenceHandle AddManagedCtorRef (EntityHandle declaringTypeRef, IReadOnlyList parameterTypes, string defaultAssemblyName) + { + var blob = new BlobBuilder (32); + blob.WriteByte (0x20); // HASTHIS + blob.WriteCompressedInteger (parameterTypes.Count); + blob.WriteByte (0x01); // ELEMENT_TYPE_VOID + foreach (var parameterType in parameterTypes) { + WriteManagedTypeSignature (blob, parameterType, defaultAssemblyName); + } + return _pe.Metadata.AddMemberReference (declaringTypeRef, _pe.Metadata.GetOrAddString (".ctor"), _pe.Metadata.GetOrAddBlob (blob)); + } + MethodDefinitionHandle EmitUcoMethod (UcoMethodData uco, JavaPeerProxyData proxy) { var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature); @@ -1061,27 +1117,33 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy return openGenericHandle; } - if (jniParams.Count == 0) { + if (jniParams.Count == 0 && uco.HasPublicParameterlessConstructor) { var defaultCtorRef = AddDefaultCtorRef (targetTypeRef); var defaultCtorHandle = _pe.EmitBody (uco.WrapperName, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, encodeSig, (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { + var havePeer = enc.DefineLabel (); + + enc.LoadArgument (1); + enc.Call (_getActivationPeerRef, parameterCount: 1, returnsValue: true); + enc.CastClass (targetTypeRef); + enc.StoreLocal (4); + + enc.LoadLocal (4); + enc.Branch (ILOpCode.Brtrue, havePeer); + enc.LoadToken (targetTypeRef); enc.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); enc.Call (_getUninitializedObjectRef, parameterCount: 1, returnsValue: true); enc.CastClass (targetTypeRef); enc.StoreLocal (4); - enc.LoadLocalAddress (3); // jniRef - enc.LoadArgument (1); // self - enc.LoadConstantI4 (0); // JniObjectReferenceType.Invalid - enc.Call (_jniObjectReferenceCtorRef, parameterCount: 2, isInstance: true); - enc.LoadLocal (4); - enc.LoadLocal (3); - enc.Callvirt (_setPeerReferenceRef, parameterCount: 1); + enc.LoadArgument (1); // self + enc.Call (_setActivationPeerReferenceRef, parameterCount: 2); + enc.MarkLabel (havePeer); enc.LoadLocal (4); enc.Call (defaultCtorRef, parameterCount: 0, isInstance: true); @@ -1093,6 +1155,47 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy return defaultCtorHandle; } + if (jniParams.Count > 0 && uco.ManagedParameterTypes.Count == jniParams.Count) { + var ctorRef = AddManagedCtorRef (targetTypeRef, uco.ManagedParameterTypes, uco.TargetType.AssemblyName); + var managedCtorHandle = _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { + var havePeer = enc.DefineLabel (); + + enc.LoadArgument (1); + enc.Call (_getActivationPeerRef, parameterCount: 1, returnsValue: true); + enc.CastClass (targetTypeRef); + enc.StoreLocal (4); + + enc.LoadLocal (4); + enc.Branch (ILOpCode.Brtrue, havePeer); + + enc.LoadToken (targetTypeRef); + enc.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); + enc.Call (_getUninitializedObjectRef, parameterCount: 1, returnsValue: true); + enc.CastClass (targetTypeRef); + enc.StoreLocal (4); + + enc.LoadLocal (4); + enc.LoadArgument (1); // self + enc.Call (_setActivationPeerReferenceRef, parameterCount: 2); + + enc.MarkLabel (havePeer); + enc.LoadLocal (4); + for (int i = 0; i < uco.ManagedParameterTypes.Count; i++) { + EmitManagedConstructorArgument (enc, uco.ManagedParameterTypes [i], jniParams [i], i + 2, uco.TargetType.AssemblyName); + } + enc.Call (ctorRef, uco.ManagedParameterTypes.Count, isInstance: true); + + enc.LoadArgument (1); // self + enc.Call (_markActivationPeerReplaceableRef, parameterCount: 1); + }), + blob => EncodeUcoConstructorLocals_DefaultConstructor (blob, targetTypeRef)); + AddUnmanagedCallersOnlyAttribute (managedCtorHandle); + return managedCtorHandle; + } + MethodDefinitionHandle handle; if (activationCtor.Style == ActivationCtorStyle.JavaInterop) { var ctorRef = AddJavaInteropActivationCtorRef ( @@ -1241,6 +1344,111 @@ void EmitUcoConstructorBodyWithMarshal (TrackedInstructionEncoder encoder, Contr cfb.AddFinallyRegion (tryStart, finallyStart, finallyStart, afterAll); } + void EmitManagedConstructorArgument (TrackedInstructionEncoder encoder, string managedType, JniParamKind jniKind, int argumentIndex, string defaultAssemblyName) + { + if (jniKind != JniParamKind.Object) { + encoder.LoadArgument (argumentIndex); + return; + } + + if (managedType == "System.String") { + encoder.LoadArgument (argumentIndex); + encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer + encoder.Call (_jniEnvGetStringRef, parameterCount: 2, returnsValue: true); + return; + } + + if (TryGetSzArrayElementType (managedType, out var elementType)) { + var arrayType = ResolveManagedTypeHandle (managedType, defaultAssemblyName); + var elementTypeHandle = ResolveManagedTypeHandle (elementType, defaultAssemblyName); + + encoder.LoadArgument (argumentIndex); + encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer + encoder.LoadToken (elementTypeHandle); + encoder.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); + encoder.Call (_jniEnvGetArrayRef, parameterCount: 3, returnsValue: true); + encoder.CastClass (arrayType); + return; + } + + var managedTypeHandle = ResolveManagedTypeHandle (managedType, defaultAssemblyName); + encoder.LoadArgument (argumentIndex); + encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer + encoder.LoadToken (managedTypeHandle); + encoder.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); + encoder.Call (_javaLangObjectGetObjectRef, parameterCount: 3, returnsValue: true); + encoder.CastClass (managedTypeHandle); + } + + EntityHandle ResolveManagedTypeHandle (string managedType, string defaultAssemblyName) + { + if (TryGetSzArrayElementType (managedType, out var elementType)) { + var blob = new BlobBuilder (32); + blob.WriteByte (0x1D); // ELEMENT_TYPE_SZARRAY + WriteManagedTypeSignature (blob, elementType, defaultAssemblyName); + return _pe.Metadata.AddTypeSpecification (_pe.Metadata.GetOrAddBlob (blob)); + } + + return _pe.ResolveTypeRef (new TypeRefData { + ManagedTypeName = managedType, + AssemblyName = GetAssemblyNameForManagedType (managedType, defaultAssemblyName), + }); + } + + static bool TryGetSzArrayElementType (string managedType, out string elementType) + { + if (managedType.EndsWith ("[]", StringComparison.Ordinal)) { + elementType = managedType.Substring (0, managedType.Length - 2); + return true; + } + + elementType = ""; + return false; + } + + void WriteManagedTypeSignature (BlobBuilder blob, string managedType, string defaultAssemblyName) + { + if (TryGetSzArrayElementType (managedType, out var elementType)) { + blob.WriteByte (0x1D); // ELEMENT_TYPE_SZARRAY + WriteManagedTypeSignature (blob, elementType, defaultAssemblyName); + return; + } + + switch (managedType) { + case "System.Boolean": blob.WriteByte (0x02); return; + case "System.Char": blob.WriteByte (0x03); return; + case "System.SByte": blob.WriteByte (0x04); return; + case "System.Byte": blob.WriteByte (0x05); return; + case "System.Int16": blob.WriteByte (0x06); return; + case "System.UInt16": blob.WriteByte (0x07); return; + case "System.Int32": blob.WriteByte (0x08); return; + case "System.UInt32": blob.WriteByte (0x09); return; + case "System.Int64": blob.WriteByte (0x0A); return; + case "System.UInt64": blob.WriteByte (0x0B); return; + case "System.Single": blob.WriteByte (0x0C); return; + case "System.Double": blob.WriteByte (0x0D); return; + case "System.String": blob.WriteByte (0x0E); return; + case "System.Object": blob.WriteByte (0x1C); return; + } + + var typeHandle = ResolveManagedTypeHandle (managedType, defaultAssemblyName); + blob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeHandle)); + } + + static string GetAssemblyNameForManagedType (string managedType, string defaultAssemblyName) + { + if (managedType.StartsWith ("System.", StringComparison.Ordinal)) { + return "System.Runtime"; + } + if (managedType.StartsWith ("Android.", StringComparison.Ordinal) || + managedType.StartsWith ("Java.", StringComparison.Ordinal) || + managedType.StartsWith ("Javax.", StringComparison.Ordinal)) { + return "Mono.Android"; + } + return defaultAssemblyName; + } + /// /// LOCAL_SIG for UCO constructors without JavaInterop-style activation. /// Locals: 0=JniTransition, 1=JniRuntime, 2=Exception. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index ee285b42798..9dceb6d1f62 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -138,6 +138,11 @@ public sealed record JavaPeerInfo /// public bool IsGenericDefinition { get; init; } + /// + /// True if this non-abstract type declares a public parameterless managed constructor. + /// + public bool HasPublicParameterlessConstructor { get; init; } + /// /// Android component attribute data ([Activity], [Service], [BroadcastReceiver], [ContentProvider], /// [Application], [Instrumentation]) if present on this type. Used for manifest generation. @@ -224,6 +229,11 @@ public sealed record MarshalMethodInfo /// public string? SuperArgumentsString { get; init; } + /// + /// Managed method parameter type names, in declaration order. + /// + public IReadOnlyList ManagedParameterTypes { get; init; } = []; + /// /// True if this method was collected from an implemented interface /// (Pass 4: CollectInterfaceMethodImplementations), not from the type itself. @@ -267,6 +277,11 @@ public sealed record JavaConstructorInfo /// Null for [Register] constructors. /// public string? SuperArgumentsString { get; init; } + + /// + /// Managed constructor parameter type names, in declaration order. + /// + public IReadOnlyList ManagedParameterTypes { get; init; } = []; } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index dda7460271c..1cd0c54d612 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -268,6 +268,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A InvokerTypeName = invokerTypeName, InvokerActivationCtorStyle = invokerActivationCtorStyle, IsGenericDefinition = isGenericDefinition, + HasPublicParameterlessConstructor = !isAbstract && !isGenericDefinition && HasPublicParameterlessConstructor (typeDef, index), ComponentAttribute = ToComponentInfo (attrInfo), }; @@ -563,6 +564,7 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, ManagedMethodName = ".ctor", NativeCallbackName = "n_ctor", IsConstructor = true, + ManagedParameterTypes = [], }); alreadyRegisteredSignatures.Add (signature); } @@ -614,6 +616,7 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, NativeCallbackName = "n_ctor", IsConstructor = true, SuperArgumentsString = "", + ManagedParameterTypes = sig.ParameterTypes, }); alreadyRegisteredSignatures.Add (jniSignature); } @@ -892,6 +895,7 @@ static void AddMarshalMethod (List methods, RegisterInfo regi bool isExport = exportInfo is not null; string managedName = index.Reader.GetString (methodDef.Name); string jniSignature = registerInfo.Signature ?? "()V"; + var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); string declaringTypeName = ""; string declaringAssemblyName = ""; @@ -911,6 +915,7 @@ static void AddMarshalMethod (List methods, RegisterInfo regi JavaAccess = isExport ? GetJavaAccess (methodDef.Attributes & MethodAttributes.MemberAccessMask) : null, ThrownNames = exportInfo?.ThrownNames, SuperArgumentsString = exportInfo?.SuperArgumentsString, + ManagedParameterTypes = sig.ParameterTypes, }); } @@ -1179,6 +1184,25 @@ string ManagedTypeToJniDescriptor (string managedType) return null; } + static bool HasPublicParameterlessConstructor (TypeDefinition typeDef, AssemblyIndex index) + { + foreach (var methodHandle in typeDef.GetMethods ()) { + var method = index.Reader.GetMethodDefinition (methodHandle); + if (index.Reader.GetString (method.Name) != ".ctor") { + continue; + } + if ((method.Attributes & MethodAttributes.MemberAccessMask) != MethodAttributes.Public) { + continue; + } + var sig = method.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + if (sig.ParameterTypes.Length == 0) { + return true; + } + } + + return false; + } + static ActivationCtorStyle? FindActivationCtorOnType (TypeDefinition typeDef, AssemblyIndex index) { foreach (var methodHandle in typeDef.GetMethods ()) { @@ -1553,6 +1577,7 @@ static List BuildJavaConstructors (List JniSignature = mm.JniSignature, ConstructorIndex = ctorIndex, SuperArgumentsString = mm.SuperArgumentsString, + ManagedParameterTypes = mm.ManagedParameterTypes, }); ctorIndex++; } diff --git a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs index 190f1c597f7..fec5337d347 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs @@ -96,12 +96,24 @@ public static bool ShouldSkipActivation (IntPtr jniSelf) { var reference = new JniObjectReference (jniSelf, JniObjectReferenceType.Invalid); var peer = JniEnvironment.Runtime.ValueManager.PeekPeer (reference); - if (peer == null) { - return false; + if (peer != null && !IsActivationPeer (peer)) { + return true; } - var state = peer.JniManagedPeerState; - return (state & JniManagedPeerStates.Activatable) != JniManagedPeerStates.Activatable - && (state & JniManagedPeerStates.Replaceable) != JniManagedPeerStates.Replaceable; + return JniEnvironment.WithinNewObjectScope; + } + + public static IJavaPeerable? GetActivationPeer (IntPtr jniSelf) + { + var reference = new JniObjectReference (jniSelf, JniObjectReferenceType.Invalid); + var peer = JniEnvironment.Runtime.ValueManager.PeekPeer (reference); + return peer != null && IsActivationPeer (peer) ? peer : null; + } + + public static void SetActivationPeerReference (IJavaPeerable peer, IntPtr jniSelf) + { + var reference = new JniObjectReference (jniSelf, JniObjectReferenceType.Invalid); + peer.SetPeerReference (reference); + peer.SetJniIdentityHashCode (JniEnvironment.References.GetIdentityHashCode (reference)); } public static void MarkActivationPeerReplaceable (IntPtr jniSelf) @@ -114,6 +126,13 @@ public static void MarkActivationPeerReplaceable (IntPtr jniSelf) peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); } + + static bool IsActivationPeer (IJavaPeerable peer) + { + var state = peer.JniManagedPeerState; + return (state & JniManagedPeerStates.Activatable) == JniManagedPeerStates.Activatable + || (state & JniManagedPeerStates.Replaceable) == JniManagedPeerStates.Replaceable; + } } /// diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 44b5bff4f85..a4e0bbdb746 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -102,6 +102,7 @@ private protected static JavaPeerInfo MakeAcwPeer (string jniName, string manage IsConstructor = true, }, }, + HasPublicParameterlessConstructor = true, }; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 008ce57994d..41bb175354e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -302,7 +302,7 @@ public void Generate_InheritedCtor_NctorUcoCallsDefaultConstructor () Assert.Contains ("ShouldSkipActivation", memberNames); Assert.Contains ("GetUninitializedObject", memberNames); - Assert.Contains ("SetPeerReference", memberNames); + Assert.Contains ("SetActivationPeerReference", memberNames); Assert.Contains ("MarkActivationPeerReplaceable", memberNames); Assert.DoesNotContain ("Invoke", memberNames); Assert.DoesNotContain ("ActivateInstance", memberNames); @@ -338,7 +338,7 @@ public void Generate_InheritedJavaInteropCtor_NctorUcoCallsDefaultConstructor () var memberNames = GetMemberRefNames (reader); Assert.Contains ("GetUninitializedObject", memberNames); - Assert.Contains ("SetPeerReference", memberNames); + Assert.Contains ("SetActivationPeerReference", memberNames); Assert.Contains ("MarkActivationPeerReplaceable", memberNames); Assert.DoesNotContain ("Invoke", memberNames); @@ -685,7 +685,7 @@ public void Generate_AcwProxy_HasUnmanagedCallersOnlyAttribute () [Theory] [InlineData (1, 0x05)] // Boolean → byte (unsigned) for JNI ABI [InlineData (2, 0x04)] // Byte → sbyte - [InlineData (3, 0x03)] // Char → char + [InlineData (3, 0x07)] // Char → uint16 (blittable JNI jchar) [InlineData (4, 0x06)] // Short → int16 [InlineData (5, 0x08)] // Int → int32 [InlineData (6, 0x0A)] // Long → int64 @@ -752,7 +752,6 @@ public void EncodeClrTypeForCallback_Void_Throws () [Theory] [InlineData (2)] // Byte - [InlineData (3)] // Char [InlineData (4)] // Short [InlineData (5)] // Int [InlineData (6)] // Long @@ -1173,6 +1172,96 @@ public void Generate_UcoConstructor_HasMarshalMethodMetadataAndExceptionRegions Assert.Contains (regions, r => r.Kind == ExceptionRegionKind.Finally); } + [Fact] + public void Generate_UcoConstructor_ParameterizedPrimitiveCtorCallsManagedConstructor () + { + string jniSignature = "(ZBCSIJFD)V"; + var managedTypes = new [] { + "System.Boolean", + "System.SByte", + "System.Char", + "System.Int16", + "System.Int32", + "System.Int64", + "System.Single", + "System.Double", + }; + var peer = MakeAcwPeer ("test/PrimitiveCtorArgs", "Test.PrimitiveCtorArgs", "TestAsm") with { + JavaConstructors = new List { + new JavaConstructorInfo { + ConstructorIndex = 0, + JniSignature = jniSignature, + ManagedParameterTypes = managedTypes, + }, + }, + MarshalMethods = new List { + new MarshalMethodInfo { + JniName = "", + NativeCallbackName = "n_ctor", + JniSignature = jniSignature, + ManagedMethodName = ".ctor", + IsConstructor = true, + ManagedParameterTypes = managedTypes, + }, + }, + }; + + using var stream = GenerateAssembly (new [] { peer }, "ParameterizedPrimitiveCtorTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + Assert.NotEmpty (FindCtorMemberRefs (reader, "Test", "PrimitiveCtorArgs", managedTypes)); + + var nctorMethod = reader.GetMethodDefinition (FindNctorUcoMethod (reader)); + var nctorSignature = nctorMethod.DecodeSignature (SignatureTypeProvider.Instance, null); + Assert.Equal ( + new [] { "System.IntPtr", "System.IntPtr", "System.Byte", "System.SByte", "System.UInt16", "System.Int16", "System.Int32", "System.Int64", "System.Single", "System.Double" }, + nctorSignature.ParameterTypes); + } + + [Fact] + public void Generate_UcoConstructor_ParameterizedObjectCtorUsesExplicitMarshalHelpers () + { + string jniSignature = "(Ljava/lang/String;[I[Ljava/lang/String;Landroid/content/Context;)V"; + var managedTypes = new [] { + "System.String", + "System.Int32[]", + "System.String[]", + "Android.Content.Context", + }; + var peer = MakeAcwPeer ("test/ObjectCtorArgs", "Test.ObjectCtorArgs", "TestAsm") with { + JavaConstructors = new List { + new JavaConstructorInfo { + ConstructorIndex = 0, + JniSignature = jniSignature, + ManagedParameterTypes = managedTypes, + }, + }, + MarshalMethods = new List { + new MarshalMethodInfo { + JniName = "", + NativeCallbackName = "n_ctor", + JniSignature = jniSignature, + ManagedMethodName = ".ctor", + IsConstructor = true, + ManagedParameterTypes = managedTypes, + }, + }, + }; + + using var stream = GenerateAssembly (new [] { peer }, "ParameterizedObjectCtorTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + Assert.NotEmpty (FindCtorMemberRefs (reader, "Test", "ObjectCtorArgs", managedTypes)); + + var memberNames = GetMemberRefNames (reader); + Assert.Contains ("GetString", memberNames); + Assert.Contains ("GetArray", memberNames); + Assert.Contains ("GetObject", memberNames); + Assert.DoesNotContain ("FromJniHandle", memberNames); + } + [Fact] public void Generate_UcoConstructor_JiStyle_HasExceptionRegions () { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs new file mode 100644 index 00000000000..70b007c5bf4 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs @@ -0,0 +1,807 @@ +using System; + +using Android.App; +using Android.Content; +using Android.Runtime; +using Android.Util; +using Android.Views; + +using Java.Interop; + +using NUnit.Framework; + +namespace Java.InteropTests +{ + [TestFixture] + public class ConstructorActivationTests + { + [Test] + public void JavaSideDefaultConstructorRunsOnceAndRegistersPeer () + { + ConstructorActivationDefault.Reset (); + + using (var instance = CreateFromJava ("()V")) { + Assert.IsNotNull (instance); + Assert.AreEqual (1, ConstructorActivationDefault.ConstructorInvocations); + Assert.AreEqual (1, instance.ConstructorOrdinal); + AssertRegisteredSame (instance); + } + } + + [Test] + public void ManagedConstructionDoesNotReenterThroughJavaConstructorActivation () + { + ConstructorActivationDefault.Reset (); + + using (var instance = new ConstructorActivationDefault ()) { + Assert.IsNotNull (instance); + Assert.AreEqual (1, ConstructorActivationDefault.ConstructorInvocations); + Assert.AreEqual (1, instance.ConstructorOrdinal); + AssertRegisteredSame (instance); + } + } + + [Test] + public void CreateInstanceDoesNotActivateManagedConstructorInsideNewObjectScope () + { + ConstructorActivationDefault.Reset (); + + IntPtr handle = JNIEnv.CreateInstance (typeof (ConstructorActivationDefault), "()V"); + try { + var peer = JniEnvironment.Runtime.ValueManager.PeekPeer (new JniObjectReference (handle)); + + Assert.AreEqual (0, ConstructorActivationDefault.ConstructorInvocations); + Assert.IsNull (peer); + } finally { + JNIEnv.DeleteLocalRef (handle); + } + } + + [Test] + public void JavaSideConstructorReinvokesExistingActivatablePeer () + { + ConstructorActivationDefault.Reset (); + + using (var instance = new ConstructorActivationDefault ()) { + var peer = (IJavaPeerable) instance; + peer.SetJniManagedPeerState (instance.JniManagedPeerState | JniManagedPeerStates.Activatable | JniManagedPeerStates.Replaceable); + + JNIEnv.FinishCreateInstance (instance.Handle, "()V"); + + Assert.AreEqual (2, ConstructorActivationDefault.ConstructorInvocations); + Assert.AreEqual (2, instance.ConstructorOrdinal); + AssertRegisteredSame (instance); + } + } + + [Test] + public void JavaSideContextConstructorForwardsArgument () + { + ConstructorActivationContextView.Reset (); + + using (var instance = CreateFromJava ( + "(Landroid/content/Context;)V", + new JValue (Application.Context))) { + Assert.AreEqual (1, ConstructorActivationContextView.ConstructorInvocations); + Assert.IsNotNull (instance.ContextValue); + Assert.AreEqual (Application.Context.Handle, instance.ContextValue.Handle); + } + } + + [Test] + public void JavaSideContextAttributeSetConstructorForwardsArguments () + { + ConstructorActivationAttributeSetView.Reset (); + + using (var instance = CreateFromJava ( + "(Landroid/content/Context;Landroid/util/AttributeSet;)V", + new JValue (Application.Context), + JValue.Zero)) { + Assert.AreEqual (1, ConstructorActivationAttributeSetView.ConstructorInvocations); + Assert.IsNotNull (instance.ContextValue); + Assert.AreEqual (Application.Context.Handle, instance.ContextValue.Handle); + Assert.IsNull (instance.AttributeSetValue); + } + } + + [Test] + public void JavaSideContextAttributeSetStyleConstructorForwardsArguments () + { + ConstructorActivationStyledView.Reset (); + + using (var instance = CreateFromJava ( + "(Landroid/content/Context;Landroid/util/AttributeSet;I)V", + new JValue (Application.Context), + JValue.Zero, + new JValue (42))) { + Assert.AreEqual (1, ConstructorActivationStyledView.ConstructorInvocations); + Assert.IsNotNull (instance.ContextValue); + Assert.AreEqual (Application.Context.Handle, instance.ContextValue.Handle); + Assert.IsNull (instance.AttributeSetValue); + Assert.AreEqual (42, instance.DefStyleAttrValue); + } + } + + [Test] + public void JavaSideBooleanConstructorForwardsTrue () + { + ConstructorActivationMarshalObject.Reset (); + + using (var instance = CreateFromJava ("(Z)V", new JValue (true))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual (true, instance.BooleanValue); + } + } + + [Test] + public void JavaSideBooleanConstructorForwardsFalse () + { + ConstructorActivationMarshalObject.Reset (); + + using (var instance = CreateFromJava ("(Z)V", new JValue (false))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual (false, instance.BooleanValue); + } + } + + [Test] + public void JavaSideByteConstructorForwardsSignedValue () + { + ConstructorActivationMarshalObject.Reset (); + + using (var instance = CreateFromJava ("(B)V", new JValue ((sbyte) -12))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual ((sbyte) -12, instance.ByteValue); + } + } + + [Test] + public void JavaSideCharConstructorForwardsValue () + { + ConstructorActivationMarshalObject.Reset (); + + using (var instance = CreateFromJava ("(C)V", new JValue ('Q'))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual ('Q', instance.CharValue); + } + } + + [Test] + public void JavaSideShortConstructorForwardsValue () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var instance = CreateFromJava ("(S)V", new JValue ((short) -1234))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual ((short) -1234, instance.ShortValue); + } + } + + [Test] + public void JavaSideIntConstructorForwardsValue () + { + ConstructorActivationMarshalObject.Reset (); + + using (var instance = CreateFromJava ("(I)V", new JValue (0x1234567))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual (0x1234567, instance.IntValue); + } + } + + [Test] + public void JavaSideLongConstructorForwardsValue () + { + ConstructorActivationMarshalObject.Reset (); + + using (var instance = CreateFromJava ("(J)V", new JValue (0x123456789L))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual (0x123456789L, instance.LongValue); + } + } + + [Test] + public void JavaSideFloatConstructorForwardsValue () + { + ConstructorActivationMarshalObject.Reset (); + + using (var instance = CreateFromJava ("(F)V", new JValue (12.5f))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual (12.5f, instance.FloatValue); + } + } + + [Test] + public void JavaSideDoubleConstructorForwardsValue () + { + ConstructorActivationMarshalObject.Reset (); + + using (var instance = CreateFromJava ("(D)V", new JValue (-42.25))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual (-42.25, instance.DoubleValue); + } + } + + [Test] + public void JavaSideStringConstructorForwardsValue () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var value = new Java.Lang.String ("hello constructor")) + using (var instance = CreateFromJava ("(Ljava/lang/String;)V", new JValue (value))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual ("hello constructor", instance.StringValue); + } + } + + [Test] + public void JavaSideStringConstructorForwardsNull () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var instance = CreateFromJava ("(Ljava/lang/String;)V", JValue.Zero)) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.IsNull (instance.StringValue); + } + } + + [Test] + public void JavaSideTwoStringConstructorForwardsValues () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var first = new Java.Lang.String ("first")) + using (var second = new Java.Lang.String ("second")) + using (var instance = CreateFromJava ( + "(Ljava/lang/String;Ljava/lang/String;)V", + new JValue (first), + new JValue (second))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual ("first", instance.FirstStringValue); + Assert.AreEqual ("second", instance.SecondStringValue); + } + } + + [Test] + public void JavaSideTwoStringConstructorForwardsNullSecondValue () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var first = new Java.Lang.String ("first")) + using (var instance = CreateFromJava ( + "(Ljava/lang/String;Ljava/lang/String;)V", + new JValue (first), + JValue.Zero)) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual ("first", instance.FirstStringValue); + Assert.IsNull (instance.SecondStringValue); + } + } + + [Test] + public void JavaSideIntArrayConstructorForwardsValues () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var instance = CreateFromJavaWithLocalArray ( + "([I)V", + JNIEnv.NewArray (new [] { 1, 2, 3, 5 }))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual (new [] { 1, 2, 3, 5 }, instance.IntArrayValue); + } + } + + [Test] + public void JavaSideIntArrayConstructorForwardsEmptyArray () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var instance = CreateFromJavaWithLocalArray ( + "([I)V", + JNIEnv.NewArray (Array.Empty ()))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.IsNotNull (instance.IntArrayValue); + Assert.AreEqual (0, instance.IntArrayValue.Length); + } + } + + [Test] + public void JavaSideIntArrayConstructorForwardsNull () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var instance = CreateFromJava ("([I)V", JValue.Zero)) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.IsNull (instance.IntArrayValue); + } + } + + [Test] + public void JavaSideBooleanArrayConstructorForwardsValues () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var instance = CreateFromJavaWithLocalArray ( + "([Z)V", + JNIEnv.NewArray (new [] { true, false, true }))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual (new [] { true, false, true }, instance.BooleanArrayValue); + } + } + + [Test] + public void JavaSideByteArrayConstructorForwardsValues () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var instance = CreateFromJavaWithLocalArray ( + "([B)V", + JNIEnv.NewArray (new byte [] { 1, 127, 255 }))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual (new byte [] { 1, 127, 255 }, instance.ByteArrayValue); + } + } + + [Test] + public void JavaSideStringArrayConstructorForwardsValues () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var instance = CreateFromJavaWithLocalArray ( + "([Ljava/lang/String;)V", + JNIEnv.NewArray (new [] { "red", "green", "blue" }))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual (new [] { "red", "green", "blue" }, instance.StringArrayValue); + } + } + + [Test] + public void JavaSideStringArrayConstructorForwardsNullElement () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var instance = CreateFromJavaWithLocalArray ( + "([Ljava/lang/String;)V", + JNIEnv.NewArray (new [] { "red", null, "blue" }))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual (new [] { "red", null, "blue" }, instance.StringArrayValue); + } + } + + [Test] + public void JavaSideStringArrayConstructorForwardsNull () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var instance = CreateFromJava ("([Ljava/lang/String;)V", JValue.Zero)) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.IsNull (instance.StringArrayValue); + } + } + + [Test] + public void JavaSideObjectArrayConstructorForwardsValues () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var first = new Java.Lang.String ("object-array-value")) + using (var instance = CreateFromJavaWithLocalArray ( + "([Ljava/lang/Object;)V", + JNIEnv.NewArray (new Java.Lang.Object [] { first, Application.Context }))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.IsNotNull (instance.ObjectArrayValue); + Assert.AreEqual (2, instance.ObjectArrayValue.Length); + Assert.AreEqual ("object-array-value", instance.ObjectArrayValue [0].ToString ()); + Assert.AreEqual (Application.Context.Handle, instance.ObjectArrayValue [1].Handle); + } + } + + [Test] + public void JavaSideObjectArrayConstructorForwardsNull () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var instance = CreateFromJava ("([Ljava/lang/Object;)V", JValue.Zero)) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.IsNull (instance.ObjectArrayValue); + } + } + + [Test] + public void JavaSideNestedIntArrayConstructorForwardsValues () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var instance = CreateFromJavaWithLocalArray ( + "([[I)V", + JNIEnv.NewArray (new [] { + new [] { 1, 2 }, + new [] { 3, 4, 5 }, + }))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.IsNotNull (instance.NestedIntArrayValue); + Assert.AreEqual (new [] { 1, 2 }, instance.NestedIntArrayValue [0]); + Assert.AreEqual (new [] { 3, 4, 5 }, instance.NestedIntArrayValue [1]); + } + } + + static void AssumeTrimmableConstructorParameterMarshalling () + { + if (!Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) { + Assert.Ignore ("Legacy TypeManager.n_Activate does not marshal string, short, or array constructor parameters; this case validates trimmable constructor UCO parameter marshalling."); + } + } + + static T CreateFromJava (string constructorSignature, params JValue [] arguments) + where T : Java.Lang.Object + { + var instance = JNIEnv.StartCreateInstance (typeof (T), constructorSignature, arguments); + JNIEnv.FinishCreateInstance (instance, constructorSignature, arguments); + var result = Java.Lang.Object.GetObject (instance, JniHandleOwnership.TransferLocalRef); + Assert.IsNotNull (result); + return result; + } + + static T CreateFromJavaWithLocalArray (string constructorSignature, IntPtr array) + where T : Java.Lang.Object + { + try { + return CreateFromJava (constructorSignature, new JValue (array)); + } finally { + JNIEnv.DeleteLocalRef (array); + } + } + + static void AssertRegisteredSame (T instance) + where T : Java.Lang.Object + { + var registered = Java.Lang.Object.GetObject (instance.Handle, JniHandleOwnership.DoNotTransfer); + try { + Assert.AreSame (instance, registered); + if (Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) { + Assert.AreEqual (Java.Lang.JavaSystem.IdentityHashCode (instance), instance.JniIdentityHashCode); + } + } finally { + if (registered != null && !object.ReferenceEquals (instance, registered)) + registered.Dispose (); + } + } + } + + [Register ("net/dot/android/test/ConstructorActivationBase", DoNotGenerateAcw = true)] + public class ConstructorActivationBase : Java.Lang.Object + { + public ConstructorActivationBase () + { + } + + public ConstructorActivationBase (bool value) + { + } + + public ConstructorActivationBase (sbyte value) + { + } + + public ConstructorActivationBase (char value) + { + } + + public ConstructorActivationBase (short value) + { + } + + public ConstructorActivationBase (int value) + { + } + + public ConstructorActivationBase (long value) + { + } + + public ConstructorActivationBase (float value) + { + } + + public ConstructorActivationBase (double value) + { + } + + public ConstructorActivationBase (string value) + { + } + + public ConstructorActivationBase (string first, string second) + { + } + + public ConstructorActivationBase (int[] value) + { + } + + public ConstructorActivationBase (bool[] value) + { + } + + public ConstructorActivationBase (byte[] value) + { + } + + public ConstructorActivationBase (string[] value) + { + } + + public ConstructorActivationBase (Java.Lang.Object[] value) + { + } + + public ConstructorActivationBase (int[][] value) + { + } + + public ConstructorActivationBase (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + } + } + + [Register ("net/dot/android/test/ConstructorActivationDefault")] + public class ConstructorActivationDefault : Java.Lang.Object + { + public static int ConstructorInvocations; + + public int ConstructorOrdinal; + + public ConstructorActivationDefault () + { + ConstructorOrdinal = ++ConstructorInvocations; + } + + public static void Reset () + { + ConstructorInvocations = 0; + } + } + + [Register ("net/dot/android/test/ConstructorActivationMarshalObject")] + public class ConstructorActivationMarshalObject : ConstructorActivationBase + { + public static int ConstructorInvocations; + + public bool BooleanValue; + public sbyte ByteValue; + public char CharValue; + public short ShortValue; + public int IntValue; + public long LongValue; + public float FloatValue; + public double DoubleValue; + public string StringValue; + public string FirstStringValue; + public string SecondStringValue; + public int[] IntArrayValue; + public bool[] BooleanArrayValue; + public byte[] ByteArrayValue; + public string[] StringArrayValue; + public Java.Lang.Object[] ObjectArrayValue; + public int[][] NestedIntArrayValue; + + [Register (".ctor", "(Z)V", "")] + public ConstructorActivationMarshalObject (bool value) + : base (value) + { + ConstructorInvocations++; + BooleanValue = value; + } + + [Register (".ctor", "(B)V", "")] + public ConstructorActivationMarshalObject (sbyte value) + : base (value) + { + ConstructorInvocations++; + ByteValue = value; + } + + [Register (".ctor", "(C)V", "")] + public ConstructorActivationMarshalObject (char value) + : base (value) + { + ConstructorInvocations++; + CharValue = value; + } + + [Register (".ctor", "(S)V", "")] + public ConstructorActivationMarshalObject (short value) + : base (value) + { + ConstructorInvocations++; + ShortValue = value; + } + + [Register (".ctor", "(I)V", "")] + public ConstructorActivationMarshalObject (int value) + : base (value) + { + ConstructorInvocations++; + IntValue = value; + } + + [Register (".ctor", "(J)V", "")] + public ConstructorActivationMarshalObject (long value) + : base (value) + { + ConstructorInvocations++; + LongValue = value; + } + + [Register (".ctor", "(F)V", "")] + public ConstructorActivationMarshalObject (float value) + : base (value) + { + ConstructorInvocations++; + FloatValue = value; + } + + [Register (".ctor", "(D)V", "")] + public ConstructorActivationMarshalObject (double value) + : base (value) + { + ConstructorInvocations++; + DoubleValue = value; + } + + [Register (".ctor", "(Ljava/lang/String;)V", "")] + public ConstructorActivationMarshalObject (string value) + : base (value) + { + ConstructorInvocations++; + StringValue = value; + } + + [Register (".ctor", "(Ljava/lang/String;Ljava/lang/String;)V", "")] + public ConstructorActivationMarshalObject (string first, string second) + : base (first, second) + { + ConstructorInvocations++; + FirstStringValue = first; + SecondStringValue = second; + } + + [Register (".ctor", "([I)V", "")] + public ConstructorActivationMarshalObject (int[] value) + : base (value) + { + ConstructorInvocations++; + IntArrayValue = value; + } + + [Register (".ctor", "([Z)V", "")] + public ConstructorActivationMarshalObject (bool[] value) + : base (value) + { + ConstructorInvocations++; + BooleanArrayValue = value; + } + + [Register (".ctor", "([B)V", "")] + public ConstructorActivationMarshalObject (byte[] value) + : base (value) + { + ConstructorInvocations++; + ByteArrayValue = value; + } + + [Register (".ctor", "([Ljava/lang/String;)V", "")] + public ConstructorActivationMarshalObject (string[] value) + : base (value) + { + ConstructorInvocations++; + StringArrayValue = value; + } + + [Register (".ctor", "([Ljava/lang/Object;)V", "")] + public ConstructorActivationMarshalObject (Java.Lang.Object[] value) + : base (value) + { + ConstructorInvocations++; + ObjectArrayValue = value; + } + + [Register (".ctor", "([[I)V", "")] + public ConstructorActivationMarshalObject (int[][] value) + : base (value) + { + ConstructorInvocations++; + NestedIntArrayValue = value; + } + + public static void Reset () + { + ConstructorInvocations = 0; + } + } + + [Register ("net/dot/android/test/ConstructorActivationContextView")] + public class ConstructorActivationContextView : View + { + public static int ConstructorInvocations; + + public Context ContextValue; + + [Register (".ctor", "(Landroid/content/Context;)V", "")] + public ConstructorActivationContextView (Context context) + : base (context) + { + ConstructorInvocations++; + ContextValue = context; + } + + public static void Reset () + { + ConstructorInvocations = 0; + } + } + + [Register ("net/dot/android/test/ConstructorActivationAttributeSetView")] + public class ConstructorActivationAttributeSetView : View + { + public static int ConstructorInvocations; + + public Context ContextValue; + public IAttributeSet AttributeSetValue; + + [Register (".ctor", "(Landroid/content/Context;Landroid/util/AttributeSet;)V", "")] + public ConstructorActivationAttributeSetView (Context context, IAttributeSet attrs) + : base (context, attrs) + { + ConstructorInvocations++; + ContextValue = context; + AttributeSetValue = attrs; + } + + public static void Reset () + { + ConstructorInvocations = 0; + } + } + + [Register ("net/dot/android/test/ConstructorActivationStyledView")] + public class ConstructorActivationStyledView : View + { + public static int ConstructorInvocations; + + public Context ContextValue; + public IAttributeSet AttributeSetValue; + public int DefStyleAttrValue; + + [Register (".ctor", "(Landroid/content/Context;Landroid/util/AttributeSet;I)V", "")] + public ConstructorActivationStyledView (Context context, IAttributeSet attrs, int defStyleAttr) + : base (context, attrs, defStyleAttr) + { + ConstructorInvocations++; + ContextValue = context; + AttributeSetValue = attrs; + DefStyleAttrValue = defStyleAttr; + } + + public static void Reset () + { + ConstructorInvocations = 0; + } + } +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj index f1575d1e45d..6831b511fa6 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj @@ -102,6 +102,7 @@ + diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/ConstructorActivationBase.java b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/ConstructorActivationBase.java new file mode 100644 index 00000000000..e0431569afe --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/ConstructorActivationBase.java @@ -0,0 +1,21 @@ +package net.dot.android.test; + +public class ConstructorActivationBase { + public ConstructorActivationBase() {} + public ConstructorActivationBase(boolean value) {} + public ConstructorActivationBase(byte value) {} + public ConstructorActivationBase(char value) {} + public ConstructorActivationBase(short value) {} + public ConstructorActivationBase(int value) {} + public ConstructorActivationBase(long value) {} + public ConstructorActivationBase(float value) {} + public ConstructorActivationBase(double value) {} + public ConstructorActivationBase(String value) {} + public ConstructorActivationBase(String first, String second) {} + public ConstructorActivationBase(int[] value) {} + public ConstructorActivationBase(boolean[] value) {} + public ConstructorActivationBase(byte[] value) {} + public ConstructorActivationBase(String[] value) {} + public ConstructorActivationBase(Object[] value) {} + public ConstructorActivationBase(int[][] value) {} +} From bb7d9061ac488206219d206e6be261b46af39fb1 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 12 May 2026 13:26:10 +0200 Subject: [PATCH 5/6] Honor invoker activation in constructor UCOs Route generated constructor UCO activation through the invoker type when a proxy represents an abstract or interface peer. Cover both Xamarin.Android and Java.Interop activation constructor styles. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyEmitter.cs | 43 +++++++++++++++++++ .../TypeMapAssemblyGeneratorTests.cs | 37 ++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 28aa87644c9..2f90cd83751 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -1117,6 +1117,49 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy return openGenericHandle; } + if (proxy.InvokerType != null) { + var invokerTypeRef = _pe.ResolveTypeRef (proxy.InvokerType); + MethodDefinitionHandle invokerHandle; + if (proxy.InvokerActivationCtorStyle == ActivationCtorStyle.JavaInterop) { + var ctorRef = AddJavaInteropActivationCtorRef (invokerTypeRef); + invokerHandle = _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { + enc.LoadLocalAddress (3); // jniRef + enc.LoadArgument (1); // self + enc.LoadConstantI4 (0); // JniObjectReferenceType.Invalid + enc.Call (_jniObjectReferenceCtorRef, parameterCount: 2, isInstance: true); + + enc.LoadLocalAddress (3); // ref jniRef + enc.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy + enc.NewObject (ctorRef, parameterCount: 2); + enc.OpCode (ILOpCode.Pop); + + enc.LoadArgument (1); // self + enc.Call (_markActivationPeerReplaceableRef, parameterCount: 1); + }), + EncodeUcoConstructorLocals_JavaInterop); + } else { + var ctorRef = AddActivationCtorRef (invokerTypeRef); + invokerHandle = _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { + enc.LoadArgument (1); // self + enc.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer + enc.NewObject (ctorRef, parameterCount: 2); + enc.OpCode (ILOpCode.Pop); + + enc.LoadArgument (1); // self + enc.Call (_markActivationPeerReplaceableRef, parameterCount: 1); + }), + EncodeUcoConstructorLocals_Standard); + } + AddUnmanagedCallersOnlyAttribute (invokerHandle); + return invokerHandle; + } + if (jniParams.Count == 0 && uco.HasPublicParameterlessConstructor) { var defaultCtorRef = AddDefaultCtorRef (targetTypeRef); var defaultCtorHandle = _pe.EmitBody (uco.WrapperName, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 41bb175354e..2dec274f4e2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -552,6 +552,43 @@ sig.ParameterTypes [0].Contains ("JniObjectReference")) { Assert.True (foundByRefCtor, "Expected to find a JI-style invoker .ctor with byref JniObjectReference parameter"); } + [Fact] + public void Generate_UcoConstructor_InvokerUsesXamarinAndroidActivationCtor () + { + var peer = MakeAcwPeer ("test/AbstractCtorTarget", "Test.AbstractCtorTarget", "TestAsm") with { + InvokerTypeName = "Test.AbstractCtorInvoker", + HasPublicParameterlessConstructor = false, + }; + + using var stream = GenerateAssembly (new [] { peer }, "InvokerCtorUcoTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + Assert.NotEmpty (FindCtorMemberRefs (reader, "Test", "AbstractCtorInvoker", + "System.IntPtr", "Android.Runtime.JniHandleOwnership")); + Assert.Empty (FindCtorMemberRefs (reader, "Test", "AbstractCtorTarget", + "System.IntPtr", "Android.Runtime.JniHandleOwnership")); + } + + [Fact] + public void Generate_UcoConstructor_InvokerUsesJavaInteropActivationCtor () + { + var peer = MakeAcwPeer ("test/AbstractJiCtorTarget", "Test.AbstractJiCtorTarget", "TestAsm") with { + InvokerTypeName = "Test.AbstractJiCtorInvoker", + InvokerActivationCtorStyle = ActivationCtorStyle.JavaInterop, + HasPublicParameterlessConstructor = false, + }; + + using var stream = GenerateAssembly (new [] { peer }, "JiInvokerCtorUcoTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + Assert.NotEmpty (FindCtorMemberRefs (reader, "Test", "AbstractJiCtorInvoker", + "Java.Interop.JniObjectReference&", "Java.Interop.JniObjectReferenceOptions")); + Assert.Empty (FindCtorMemberRefs (reader, "Test", "AbstractJiCtorTarget", + "Java.Interop.JniObjectReference&", "Java.Interop.JniObjectReferenceOptions")); + } + [Fact] public void Generate_JiStyleCtor_EmitsDeleteRefCall () { From 0d7af555aa2b21256c96a41a33546882157d44c7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 12 May 2026 14:03:16 +0200 Subject: [PATCH 6/6] Address constructor activation review feedback Replace parameterless-only constructor metadata with general managed-constructor availability, extract repeated constructor UCO emission through helpers, add a tracked PopValue helper, and expand runtime coverage with mixed multi-argument and throwing constructors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/Model/TypeMapAssemblyData.cs | 8 +- .../Generator/ModelBuilder.cs | 2 +- .../Generator/PEAssemblyBuilder.cs | 6 + .../Generator/TypeMapAssemblyEmitter.cs | 189 +++++++----------- .../Scanner/JavaPeerInfo.cs | 16 +- .../Scanner/JavaPeerScanner.cs | 48 +++-- .../Generator/FixtureTestBase.cs | 4 +- .../TypeMapAssemblyGeneratorTests.cs | 4 +- .../ConstructorActivationTests.cs | 149 ++++++++++++++ .../test/ConstructorActivationBase.java | 3 + 10 files changed, 281 insertions(+), 148 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index c30e9006513..ecd92f53e19 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -231,14 +231,14 @@ sealed record UcoConstructorData public required string JniSignature { get; init; } /// - /// True if the target type declares a public parameterless managed constructor. + /// Managed constructor parameter type names, in declaration order. /// - public bool HasPublicParameterlessConstructor { get; init; } + public IReadOnlyList ManagedParameterTypes { get; init; } = []; /// - /// Managed constructor parameter type names, in declaration order. + /// True when this Java constructor has a matching managed constructor on the target type. /// - public IReadOnlyList ManagedParameterTypes { get; init; } = []; + public bool HasManagedConstructor { get; init; } } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 2cbd9f2564a..21ce7d7d66d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -365,8 +365,8 @@ static void BuildUcoConstructors (JavaPeerInfo peer, JavaPeerProxyData proxy) ManagedTypeName = peer.ManagedTypeName, AssemblyName = peer.AssemblyName, }, - HasPublicParameterlessConstructor = peer.HasPublicParameterlessConstructor, ManagedParameterTypes = ctor.ManagedParameterTypes, + HasManagedConstructor = ctor.HasManagedConstructor, }); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index f48ebd8c72a..18fe552561d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -611,6 +611,12 @@ public void Throw () SetStack (0); } + public void PopValue () + { + Encoder.OpCode (ILOpCode.Pop); + Pop (1); + } + public void OpCode (ILOpCode code) { Encoder.OpCode (code); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 2f90cd83751..1eb16ffbf8b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -974,14 +974,6 @@ MemberReferenceHandle AddActivationCtorRef (EntityHandle declaringTypeRef) })); } - MemberReferenceHandle AddDefaultCtorRef (EntityHandle declaringTypeRef) - { - return _pe.AddMemberRef (declaringTypeRef, ".ctor", - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, - rt => rt.Void (), - p => { })); - } - MemberReferenceHandle AddManagedCtorRef (EntityHandle declaringTypeRef, IReadOnlyList parameterTypes, string defaultAssemblyName) { var blob = new BlobBuilder (32); @@ -1087,9 +1079,8 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy $"UCO constructor wrapper requires an activation ctor for '{uco.TargetType.ManagedTypeName}'"); // UCO constructor wrappers must match the JNI native method signature exactly. - // Only jnienv (arg 0) and self (arg 1) are used — the constructor parameters - // are not forwarded because we create the managed peer using the - // activation ctor (IntPtr, JniHandleOwnership), not the user-visible constructor. + // jnienv and self are followed by the Java constructor parameters, which are + // forwarded when scanner metadata can identify the matching managed constructor. var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature); int paramCount = 2 + jniParams.Count; @@ -1104,14 +1095,12 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy // Open generic types can't be activated because Java construction cannot provide the type arguments. if (proxy.IsGenericDefinition) { - var openGenericHandle = _pe.EmitBody (uco.WrapperName, - MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - encodeSig, - (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { + var openGenericHandle = EmitUcoConstructorBody (uco.WrapperName, encodeSig, + enc => { enc.LoadString (_pe.Metadata.GetOrAddUserString ("Constructing instances of generic types from Java is not supported, as the type parameters cannot be determined.")); enc.NewObject (_notSupportedExceptionCtorRef, parameterCount: 1); enc.Throw (); - }), + }, EncodeUcoConstructorLocals_Standard); AddUnmanagedCallersOnlyAttribute (openGenericHandle); return openGenericHandle; @@ -1122,10 +1111,8 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy MethodDefinitionHandle invokerHandle; if (proxy.InvokerActivationCtorStyle == ActivationCtorStyle.JavaInterop) { var ctorRef = AddJavaInteropActivationCtorRef (invokerTypeRef); - invokerHandle = _pe.EmitBody (uco.WrapperName, - MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - encodeSig, - (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { + invokerHandle = EmitUcoConstructorBody (uco.WrapperName, encodeSig, + enc => { enc.LoadLocalAddress (3); // jniRef enc.LoadArgument (1); // self enc.LoadConstantI4 (0); // JniObjectReferenceType.Invalid @@ -1134,106 +1121,34 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy enc.LoadLocalAddress (3); // ref jniRef enc.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy enc.NewObject (ctorRef, parameterCount: 2); - enc.OpCode (ILOpCode.Pop); + enc.PopValue (); enc.LoadArgument (1); // self enc.Call (_markActivationPeerReplaceableRef, parameterCount: 1); - }), + }, EncodeUcoConstructorLocals_JavaInterop); } else { var ctorRef = AddActivationCtorRef (invokerTypeRef); - invokerHandle = _pe.EmitBody (uco.WrapperName, - MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - encodeSig, - (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { + invokerHandle = EmitUcoConstructorBody (uco.WrapperName, encodeSig, + enc => { enc.LoadArgument (1); // self enc.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer enc.NewObject (ctorRef, parameterCount: 2); - enc.OpCode (ILOpCode.Pop); + enc.PopValue (); enc.LoadArgument (1); // self enc.Call (_markActivationPeerReplaceableRef, parameterCount: 1); - }), + }, EncodeUcoConstructorLocals_Standard); } AddUnmanagedCallersOnlyAttribute (invokerHandle); return invokerHandle; } - if (jniParams.Count == 0 && uco.HasPublicParameterlessConstructor) { - var defaultCtorRef = AddDefaultCtorRef (targetTypeRef); - var defaultCtorHandle = _pe.EmitBody (uco.WrapperName, - MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - encodeSig, - (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { - var havePeer = enc.DefineLabel (); - - enc.LoadArgument (1); - enc.Call (_getActivationPeerRef, parameterCount: 1, returnsValue: true); - enc.CastClass (targetTypeRef); - enc.StoreLocal (4); - - enc.LoadLocal (4); - enc.Branch (ILOpCode.Brtrue, havePeer); - - enc.LoadToken (targetTypeRef); - enc.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); - enc.Call (_getUninitializedObjectRef, parameterCount: 1, returnsValue: true); - enc.CastClass (targetTypeRef); - enc.StoreLocal (4); - - enc.LoadLocal (4); - enc.LoadArgument (1); // self - enc.Call (_setActivationPeerReferenceRef, parameterCount: 2); - - enc.MarkLabel (havePeer); - enc.LoadLocal (4); - enc.Call (defaultCtorRef, parameterCount: 0, isInstance: true); - - enc.LoadArgument (1); // self - enc.Call (_markActivationPeerReplaceableRef, parameterCount: 1); - }), - blob => EncodeUcoConstructorLocals_DefaultConstructor (blob, targetTypeRef)); - AddUnmanagedCallersOnlyAttribute (defaultCtorHandle); - return defaultCtorHandle; - } - - if (jniParams.Count > 0 && uco.ManagedParameterTypes.Count == jniParams.Count) { + if (uco.HasManagedConstructor && uco.ManagedParameterTypes.Count == jniParams.Count) { var ctorRef = AddManagedCtorRef (targetTypeRef, uco.ManagedParameterTypes, uco.TargetType.AssemblyName); - var managedCtorHandle = _pe.EmitBody (uco.WrapperName, - MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - encodeSig, - (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { - var havePeer = enc.DefineLabel (); - - enc.LoadArgument (1); - enc.Call (_getActivationPeerRef, parameterCount: 1, returnsValue: true); - enc.CastClass (targetTypeRef); - enc.StoreLocal (4); - - enc.LoadLocal (4); - enc.Branch (ILOpCode.Brtrue, havePeer); - - enc.LoadToken (targetTypeRef); - enc.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); - enc.Call (_getUninitializedObjectRef, parameterCount: 1, returnsValue: true); - enc.CastClass (targetTypeRef); - enc.StoreLocal (4); - - enc.LoadLocal (4); - enc.LoadArgument (1); // self - enc.Call (_setActivationPeerReferenceRef, parameterCount: 2); - - enc.MarkLabel (havePeer); - enc.LoadLocal (4); - for (int i = 0; i < uco.ManagedParameterTypes.Count; i++) { - EmitManagedConstructorArgument (enc, uco.ManagedParameterTypes [i], jniParams [i], i + 2, uco.TargetType.AssemblyName); - } - enc.Call (ctorRef, uco.ManagedParameterTypes.Count, isInstance: true); - - enc.LoadArgument (1); // self - enc.Call (_markActivationPeerReplaceableRef, parameterCount: 1); - }), + var managedCtorHandle = EmitUcoConstructorBody (uco.WrapperName, encodeSig, + enc => EmitManagedConstructorActivation (enc, targetTypeRef, ctorRef, uco.ManagedParameterTypes, jniParams, uco.TargetType.AssemblyName), blob => EncodeUcoConstructorLocals_DefaultConstructor (blob, targetTypeRef)); AddUnmanagedCallersOnlyAttribute (managedCtorHandle); return managedCtorHandle; @@ -1249,10 +1164,8 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy // 1: JniRuntime? (runtime) — out-parameter for BeginMarshalMethod // 2: Exception (e) — catch variable // 3: JniObjectReference (jniRef) — needed for JavaInterop-style activation - handle = _pe.EmitBody (uco.WrapperName, - MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - encodeSig, - (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { + handle = EmitUcoConstructorBody (uco.WrapperName, encodeSig, + enc => { if (!activationCtor.IsOnLeafType) { enc.LoadToken (targetTypeRef); enc.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); @@ -1269,7 +1182,7 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy enc.LoadLocalAddress (3); // ref jniRef enc.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy enc.NewObject (ctorRef, parameterCount: 2); - enc.OpCode (ILOpCode.Pop); + enc.PopValue (); } else { enc.LoadLocalAddress (3); // ref jniRef enc.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy @@ -1277,7 +1190,7 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy } enc.LoadArgument (1); // self enc.Call (_markActivationPeerReplaceableRef, parameterCount: 1); - }), + }, EncodeUcoConstructorLocals_JavaInterop); } else { var ctorRef = AddActivationCtorRef ( @@ -1287,15 +1200,13 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy // 0: JniTransition (envp) — out-parameter for BeginMarshalMethod // 1: JniRuntime? (runtime) — out-parameter for BeginMarshalMethod // 2: Exception (e) — catch variable - handle = _pe.EmitBody (uco.WrapperName, - MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - encodeSig, - (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { + handle = EmitUcoConstructorBody (uco.WrapperName, encodeSig, + enc => { if (activationCtor.IsOnLeafType) { enc.LoadArgument (1); // self enc.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer enc.NewObject (ctorRef, parameterCount: 2); - enc.OpCode (ILOpCode.Pop); + enc.PopValue (); } else { enc.LoadToken (targetTypeRef); enc.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); @@ -1308,13 +1219,65 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy } enc.LoadArgument (1); // self enc.Call (_markActivationPeerReplaceableRef, parameterCount: 1); - }), + }, EncodeUcoConstructorLocals_Standard); } AddUnmanagedCallersOnlyAttribute (handle); return handle; } + MethodDefinitionHandle EmitUcoConstructorBody ( + string wrapperName, + Action encodeSig, + Action emitActivation, + Action encodeLocals) + { + return _pe.EmitBody (wrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, emitActivation), + encodeLocals); + } + + void EmitManagedConstructorActivation ( + TrackedInstructionEncoder enc, + EntityHandle targetTypeRef, + MemberReferenceHandle ctorRef, + IReadOnlyList managedParameterTypes, + IReadOnlyList jniParams, + string defaultAssemblyName) + { + var havePeer = enc.DefineLabel (); + + enc.LoadArgument (1); + enc.Call (_getActivationPeerRef, parameterCount: 1, returnsValue: true); + enc.CastClass (targetTypeRef); + enc.StoreLocal (4); + + enc.LoadLocal (4); + enc.Branch (ILOpCode.Brtrue, havePeer); + + enc.LoadToken (targetTypeRef); + enc.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); + enc.Call (_getUninitializedObjectRef, parameterCount: 1, returnsValue: true); + enc.CastClass (targetTypeRef); + enc.StoreLocal (4); + + enc.LoadLocal (4); + enc.LoadArgument (1); // self + enc.Call (_setActivationPeerReferenceRef, parameterCount: 2); + + enc.MarkLabel (havePeer); + enc.LoadLocal (4); + for (int i = 0; i < managedParameterTypes.Count; i++) { + EmitManagedConstructorArgument (enc, managedParameterTypes [i], jniParams [i], i + 2, defaultAssemblyName); + } + enc.Call (ctorRef, managedParameterTypes.Count, isInstance: true); + + enc.LoadArgument (1); // self + enc.Call (_markActivationPeerReplaceableRef, parameterCount: 1); + } + /// /// Emits the common try/catch/finally marshal-method wrapper pattern used by all /// non-generic UCO constructor bodies: diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 9dceb6d1f62..97606a4b5af 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -138,11 +138,6 @@ public sealed record JavaPeerInfo /// public bool IsGenericDefinition { get; init; } - /// - /// True if this non-abstract type declares a public parameterless managed constructor. - /// - public bool HasPublicParameterlessConstructor { get; init; } - /// /// Android component attribute data ([Activity], [Service], [BroadcastReceiver], [ContentProvider], /// [Application], [Instrumentation]) if present on this type. Used for manifest generation. @@ -234,6 +229,12 @@ public sealed record MarshalMethodInfo /// public IReadOnlyList ManagedParameterTypes { get; init; } = []; + /// + /// True when this constructor registration maps to an actual managed constructor on the target type. + /// False for constructors inherited from Java base types that only exist to generate Java source. + /// + public bool HasManagedConstructor { get; init; } + /// /// True if this method was collected from an implemented interface /// (Pass 4: CollectInterfaceMethodImplementations), not from the type itself. @@ -282,6 +283,11 @@ public sealed record JavaConstructorInfo /// Managed constructor parameter type names, in declaration order. /// public IReadOnlyList ManagedParameterTypes { get; init; } = []; + + /// + /// True when this Java constructor has a matching managed constructor on the target type. + /// + public bool HasManagedConstructor { get; init; } } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 1cd0c54d612..40959a5de2c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -268,7 +268,6 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A InvokerTypeName = invokerTypeName, InvokerActivationCtorStyle = invokerActivationCtorStyle, IsGenericDefinition = isGenericDefinition, - HasPublicParameterlessConstructor = !isAbstract && !isGenericDefinition && HasPublicParameterlessConstructor (typeDef, index), ComponentAttribute = ToComponentInfo (attrInfo), }; @@ -557,6 +556,7 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, continue; } if (!alreadyRegisteredSignatures.Contains (signature)) { + var managedParameterTypes = TryGetMatchingPublicConstructorParameterTypes (typeDef, index, baseCtor.Method); methods.Add (new MarshalMethodInfo { JniName = baseCtor.RegisterInfo.JniName, JniSignature = signature, @@ -564,7 +564,8 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, ManagedMethodName = ".ctor", NativeCallbackName = "n_ctor", IsConstructor = true, - ManagedParameterTypes = [], + ManagedParameterTypes = managedParameterTypes ?? [], + HasManagedConstructor = managedParameterTypes != null, }); alreadyRegisteredSignatures.Add (signature); } @@ -617,6 +618,7 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, IsConstructor = true, SuperArgumentsString = "", ManagedParameterTypes = sig.ParameterTypes, + HasManagedConstructor = true, }); alreadyRegisteredSignatures.Add (jniSignature); } @@ -624,6 +626,27 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, } } + IReadOnlyList? TryGetMatchingPublicConstructorParameterTypes (TypeDefinition typeDef, AssemblyIndex index, MethodDefinition baseCtor) + { + foreach (var methodHandle in typeDef.GetMethods ()) { + var methodDef = index.Reader.GetMethodDefinition (methodHandle); + if (index.Reader.GetString (methodDef.Name) != ".ctor") { + continue; + } + if ((methodDef.Attributes & MethodAttributes.MemberAccessMask) != MethodAttributes.Public) { + continue; + } + if (!HaveIdenticalParameterTypes (methodDef, baseCtor)) { + continue; + } + + var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + return sig.ParameterTypes; + } + + return null; + } + string? BuildJniCtorSignature (MethodSignature sig) { var sb = new System.Text.StringBuilder (); @@ -916,6 +939,7 @@ static void AddMarshalMethod (List methods, RegisterInfo regi ThrownNames = exportInfo?.ThrownNames, SuperArgumentsString = exportInfo?.SuperArgumentsString, ManagedParameterTypes = sig.ParameterTypes, + HasManagedConstructor = isConstructor, }); } @@ -1184,25 +1208,6 @@ string ManagedTypeToJniDescriptor (string managedType) return null; } - static bool HasPublicParameterlessConstructor (TypeDefinition typeDef, AssemblyIndex index) - { - foreach (var methodHandle in typeDef.GetMethods ()) { - var method = index.Reader.GetMethodDefinition (methodHandle); - if (index.Reader.GetString (method.Name) != ".ctor") { - continue; - } - if ((method.Attributes & MethodAttributes.MemberAccessMask) != MethodAttributes.Public) { - continue; - } - var sig = method.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); - if (sig.ParameterTypes.Length == 0) { - return true; - } - } - - return false; - } - static ActivationCtorStyle? FindActivationCtorOnType (TypeDefinition typeDef, AssemblyIndex index) { foreach (var methodHandle in typeDef.GetMethods ()) { @@ -1578,6 +1583,7 @@ static List BuildJavaConstructors (List ConstructorIndex = ctorIndex, SuperArgumentsString = mm.SuperArgumentsString, ManagedParameterTypes = mm.ManagedParameterTypes, + HasManagedConstructor = mm.HasManagedConstructor, }); ctorIndex++; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index a4e0bbdb746..2355555e9ac 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -91,7 +91,7 @@ private protected static JavaPeerInfo MakeAcwPeer (string jniName, string manage return MakePeerWithActivation (jniName, managedName, asmName) with { DoNotGenerateAcw = false, JavaConstructors = new List { - new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V" }, + new JavaConstructorInfo { ConstructorIndex = 0, JniSignature = "()V", HasManagedConstructor = true }, }, MarshalMethods = new List { new MarshalMethodInfo { @@ -100,9 +100,9 @@ private protected static JavaPeerInfo MakeAcwPeer (string jniName, string manage JniSignature = "()V", ManagedMethodName = ".ctor", IsConstructor = true, + HasManagedConstructor = true, }, }, - HasPublicParameterlessConstructor = true, }; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 2dec274f4e2..63e166b11b3 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -557,7 +557,6 @@ public void Generate_UcoConstructor_InvokerUsesXamarinAndroidActivationCtor () { var peer = MakeAcwPeer ("test/AbstractCtorTarget", "Test.AbstractCtorTarget", "TestAsm") with { InvokerTypeName = "Test.AbstractCtorInvoker", - HasPublicParameterlessConstructor = false, }; using var stream = GenerateAssembly (new [] { peer }, "InvokerCtorUcoTest"); @@ -576,7 +575,6 @@ public void Generate_UcoConstructor_InvokerUsesJavaInteropActivationCtor () var peer = MakeAcwPeer ("test/AbstractJiCtorTarget", "Test.AbstractJiCtorTarget", "TestAsm") with { InvokerTypeName = "Test.AbstractJiCtorInvoker", InvokerActivationCtorStyle = ActivationCtorStyle.JavaInterop, - HasPublicParameterlessConstructor = false, }; using var stream = GenerateAssembly (new [] { peer }, "JiInvokerCtorUcoTest"); @@ -1229,6 +1227,7 @@ public void Generate_UcoConstructor_ParameterizedPrimitiveCtorCallsManagedConstr ConstructorIndex = 0, JniSignature = jniSignature, ManagedParameterTypes = managedTypes, + HasManagedConstructor = true, }, }, MarshalMethods = new List { @@ -1272,6 +1271,7 @@ public void Generate_UcoConstructor_ParameterizedObjectCtorUsesExplicitMarshalHe ConstructorIndex = 0, JniSignature = jniSignature, ManagedParameterTypes = managedTypes, + HasManagedConstructor = true, }, }, MarshalMethods = new List { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs index 70b007c5bf4..1fd51eff2e1 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs @@ -74,6 +74,17 @@ public void JavaSideConstructorReinvokesExistingActivatablePeer () } } + [Test] + public void JavaSideThrowingConstructorPropagatesException () + { + ConstructorActivationThrowing.Reset (); + + var exception = Assert.Catch (() => CreateFromJavaExpectingConstructorException ("()V")); + Assert.IsNotNull (exception); + Assert.AreEqual (1, ConstructorActivationThrowing.ConstructorInvocations); + Assert.IsTrue (exception.ToString ().Contains (ConstructorActivationThrowing.ExceptionMessage)); + } + [Test] public void JavaSideContextConstructorForwardsArgument () { @@ -222,6 +233,23 @@ public void JavaSideDoubleConstructorForwardsValue () } } + [Test] + public void JavaSidePrimitiveMixedConstructorForwardsValues () + { + ConstructorActivationMarshalObject.Reset (); + + using (var instance = CreateFromJava ( + "(IZJ)V", + new JValue (12345), + new JValue (true), + new JValue (9876543210L))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual (12345, instance.MultiIntValue); + Assert.AreEqual (true, instance.MultiBooleanValue); + Assert.AreEqual (9876543210L, instance.MultiLongValue); + } + } + [Test] public void JavaSideStringConstructorForwardsValue () { @@ -282,6 +310,23 @@ public void JavaSideTwoStringConstructorForwardsNullSecondValue () } } + [Test] + public void JavaSideStringIntConstructorForwardsValues () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var text = new Java.Lang.String ("string-int")) + using (var instance = CreateFromJava ( + "(Ljava/lang/String;I)V", + new JValue (text), + new JValue (17))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual ("string-int", instance.StringIntStringValue); + Assert.AreEqual (17, instance.StringIntValue); + } + } + [Test] public void JavaSideIntArrayConstructorForwardsValues () { @@ -323,6 +368,30 @@ public void JavaSideIntArrayConstructorForwardsNull () } } + [Test] + public void JavaSideStringIntArrayConstructorForwardsValues () + { + ConstructorActivationMarshalObject.Reset (); + AssumeTrimmableConstructorParameterMarshalling (); + + using (var label = new Java.Lang.String ("string-array")) + { + IntPtr array = JNIEnv.NewArray (new [] { 8, 13, 21 }); + try { + using (var instance = CreateFromJava ( + "(Ljava/lang/String;[I)V", + new JValue (label), + new JValue (array))) { + Assert.AreEqual (1, ConstructorActivationMarshalObject.ConstructorInvocations); + Assert.AreEqual ("string-array", instance.StringArrayLabel); + Assert.AreEqual (new [] { 8, 13, 21 }, instance.StringArrayValues); + } + } finally { + JNIEnv.DeleteLocalRef (array); + } + } + } + [Test] public void JavaSideBooleanArrayConstructorForwardsValues () { @@ -467,6 +536,20 @@ static T CreateFromJavaWithLocalArray (string constructorSignature, IntPtr ar } } + static void CreateFromJavaExpectingConstructorException (string constructorSignature, params JValue [] arguments) + where T : Java.Lang.Object + { + IntPtr instance = IntPtr.Zero; + try { + instance = JNIEnv.StartCreateInstance (typeof (T), constructorSignature, arguments); + JNIEnv.FinishCreateInstance (instance, constructorSignature, arguments); + } finally { + if (instance != IntPtr.Zero) { + JNIEnv.DeleteLocalRef (instance); + } + } + } + static void AssertRegisteredSame (T instance) where T : Java.Lang.Object { @@ -483,6 +566,25 @@ static void AssertRegisteredSame (T instance) } } + [Register ("net/dot/android/test/ConstructorActivationThrowing")] + public class ConstructorActivationThrowing : Java.Lang.Object + { + public const string ExceptionMessage = "constructor activation throw"; + + public static int ConstructorInvocations; + + public ConstructorActivationThrowing () + { + ConstructorInvocations++; + throw new InvalidOperationException (ExceptionMessage); + } + + public static void Reset () + { + ConstructorInvocations = 0; + } + } + [Register ("net/dot/android/test/ConstructorActivationBase", DoNotGenerateAcw = true)] public class ConstructorActivationBase : Java.Lang.Object { @@ -522,6 +624,10 @@ public ConstructorActivationBase (double value) { } + public ConstructorActivationBase (int number, bool flag, long longValue) + { + } + public ConstructorActivationBase (string value) { } @@ -530,10 +636,18 @@ public ConstructorActivationBase (string first, string second) { } + public ConstructorActivationBase (string text, int number) + { + } + public ConstructorActivationBase (int[] value) { } + public ConstructorActivationBase (string label, int[] value) + { + } + public ConstructorActivationBase (bool[] value) { } @@ -591,10 +705,17 @@ public class ConstructorActivationMarshalObject : ConstructorActivationBase public long LongValue; public float FloatValue; public double DoubleValue; + public int MultiIntValue; + public bool MultiBooleanValue; + public long MultiLongValue; public string StringValue; public string FirstStringValue; public string SecondStringValue; + public string StringIntStringValue; + public int StringIntValue; public int[] IntArrayValue; + public string StringArrayLabel; + public int[] StringArrayValues; public bool[] BooleanArrayValue; public byte[] ByteArrayValue; public string[] StringArrayValue; @@ -665,6 +786,16 @@ public ConstructorActivationMarshalObject (double value) DoubleValue = value; } + [Register (".ctor", "(IZJ)V", "")] + public ConstructorActivationMarshalObject (int number, bool flag, long longValue) + : base (number, flag, longValue) + { + ConstructorInvocations++; + MultiIntValue = number; + MultiBooleanValue = flag; + MultiLongValue = longValue; + } + [Register (".ctor", "(Ljava/lang/String;)V", "")] public ConstructorActivationMarshalObject (string value) : base (value) @@ -682,6 +813,15 @@ public ConstructorActivationMarshalObject (string first, string second) SecondStringValue = second; } + [Register (".ctor", "(Ljava/lang/String;I)V", "")] + public ConstructorActivationMarshalObject (string text, int number) + : base (text, number) + { + ConstructorInvocations++; + StringIntStringValue = text; + StringIntValue = number; + } + [Register (".ctor", "([I)V", "")] public ConstructorActivationMarshalObject (int[] value) : base (value) @@ -690,6 +830,15 @@ public ConstructorActivationMarshalObject (int[] value) IntArrayValue = value; } + [Register (".ctor", "(Ljava/lang/String;[I)V", "")] + public ConstructorActivationMarshalObject (string label, int[] value) + : base (label, value) + { + ConstructorInvocations++; + StringArrayLabel = label; + StringArrayValues = value; + } + [Register (".ctor", "([Z)V", "")] public ConstructorActivationMarshalObject (bool[] value) : base (value) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/ConstructorActivationBase.java b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/ConstructorActivationBase.java index e0431569afe..20283efcfc9 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/ConstructorActivationBase.java +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/java/net/dot/android/test/ConstructorActivationBase.java @@ -10,9 +10,12 @@ public ConstructorActivationBase(int value) {} public ConstructorActivationBase(long value) {} public ConstructorActivationBase(float value) {} public ConstructorActivationBase(double value) {} + public ConstructorActivationBase(int number, boolean flag, long longValue) {} public ConstructorActivationBase(String value) {} public ConstructorActivationBase(String first, String second) {} + public ConstructorActivationBase(String text, int number) {} public ConstructorActivationBase(int[] value) {} + public ConstructorActivationBase(String label, int[] value) {} public ConstructorActivationBase(boolean[] value) {} public ConstructorActivationBase(byte[] value) {} public ConstructorActivationBase(String[] value) {}