From fedc477423345e6da2a85b7de2a02c8617b4909b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 3 May 2026 19:31:30 +0200 Subject: [PATCH 1/7] Reject open generic JNI construction Open generic managed types cannot be constructed from Java because the runtime cannot infer their type arguments. Match the existing TypeManager activation guard in JNIEnv.StartCreateInstance(Type, ...) and re-enable the trimmable tests that cover both managed and Java activation paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 3 +++ .../Mono.Android-Tests/Java.Interop/JnienvTest.cs | 1 - .../Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs | 6 ++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 8104ee308a2..d9ab420856e 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -170,6 +170,9 @@ public static unsafe void FinishCreateInstance (IntPtr instance, IntPtr jclass, public static unsafe IntPtr StartCreateInstance (Type type, string jniCtorSignature, JValue* constructorParameters) { + if (type.IsGenericTypeDefinition) { + throw new NotSupportedException ("Constructing instances of generic types from Java is not supported, as the type parameters cannot be determined."); + } return AllocObject (type); } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs index 148c7dc9383..0eacc4eddad 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 @@ -121,7 +121,6 @@ public void InvokingNullInstanceDoesNotCrashDalvik () } } - // TODO: https://github.com/dotnet/android/issues/11170 — open generic creation should throw but succeeds under trimmable typemap [Test] public void NewOpenGenericTypeThrows () { 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 b2a9ecabeeb..7c77b6b6787 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 @@ -70,6 +70,12 @@ protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) // net.dot.jni.test.GenericHolder Java class not in APK "Java.InteropTests.JniTypeManagerTests.CannotCreateGenericHolderFromJava", + // JniPrimitiveArrayInfo lookup fails for JavaBooleanArray — + // our typemap returns JavaBooleanArray for "Z" via JavaPrimitiveArray<> + // alias, which collides with the legacy GetPrimitiveArrayTypesForSimpleReference + // that expects only primitive CLR types. Out of scope for this PR. + "Java.InteropTests.JniTypeManagerTests.GetType", + // Open generic type handling differs from non-trimmable "Java.InteropTests.JnienvTest.NewOpenGenericTypeThrows", From b357aeeb6b283d4e623d325e35d516b30abbc230 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 3 May 2026 22:39:35 +0200 Subject: [PATCH 2/7] Move open generic rejection to generated activation Match the legacy typemap behavior by rejecting open generic Java construction from the generated constructor activation callback instead of JNIEnv.StartCreateInstance(). This keeps JNIEnv allocation-only and makes Java-side construction fail when the constructor callback runs, just like TypeManager.n_Activate(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyEmitter.cs | 18 +++++++++++------- src/Mono.Android/Android.Runtime/JNIEnv.cs | 3 --- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index ccf1ac857a4..24459237f14 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -976,16 +976,20 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); }); - // Open generic types can't be activated — emit a no-op UCO. + // Open generic types can't be activated because Java construction cannot provide the type arguments. if (proxy.IsGenericDefinition) { - var noopHandle = _pe.EmitBody (uco.WrapperName, + var openGenericHandle = _pe.EmitBody (uco.WrapperName, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, encodeSig, - encoder => { - encoder.OpCode (ILOpCode.Ret); - }); - AddUnmanagedCallersOnlyAttribute (noopHandle); - return noopHandle; + (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { + enc.LoadString (_pe.Metadata.GetOrAddUserString ("Constructing instances of generic types from Java is not supported, as the type parameters cannot be determined.")); + enc.OpCode (ILOpCode.Newobj); + enc.Token (_notSupportedExceptionCtorRef); + enc.OpCode (ILOpCode.Throw); + }), + EncodeUcoConstructorLocals_Standard); + AddUnmanagedCallersOnlyAttribute (openGenericHandle); + return openGenericHandle; } MethodDefinitionHandle handle; diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index d9ab420856e..8104ee308a2 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -170,9 +170,6 @@ public static unsafe void FinishCreateInstance (IntPtr instance, IntPtr jclass, public static unsafe IntPtr StartCreateInstance (Type type, string jniCtorSignature, JValue* constructorParameters) { - if (type.IsGenericTypeDefinition) { - throw new NotSupportedException ("Constructing instances of generic types from Java is not supported, as the type parameters cannot be determined."); - } return AllocObject (type); } From 5a6280ae8917c2d4785c4d3074b1fec78457089a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 3 May 2026 22:50:37 +0200 Subject: [PATCH 3/7] Cover open generic UCO exception marshaling Update the open-generic UCO constructor test to assert the generated callback throws inside the marshal-method wrapper and calls OnUserUnhandledException. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TypeMapAssemblyGeneratorTests.cs | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 8ece123b853..4e5c6dab7f0 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -1173,28 +1173,49 @@ public void Generate_UcoConstructor_JiStyle_HasExceptionRegions () } [Fact] - public void Generate_UcoConstructor_GenericDefinition_NoExceptionRegions () + public void Generate_UcoConstructor_GenericDefinition_ThrowsWithMarshalMethodPattern () { - // Open-generic UCO constructors are no-ops and must NOT have exception regions - // (a single 'ret' is emitted with no surrounding try/catch/finally). - var peers = ScanFixtures (); - var generic = peers.First (p => p.JavaName == "my/app/GenericHolder"); - Assert.True (generic.IsGenericDefinition); + // Open-generic UCO constructors throw inside the same marshal-method wrapper used + // by normal UCO constructors, so the exception is surfaced through + // JniRuntime.OnUserUnhandledException instead of crossing the JNI boundary. + var generic = MakeAcwPeer ("test/GenericHolder", "Test.GenericHolder`1", "TestAsm") with { + IsGenericDefinition = true, + }; using var stream = GenerateAssembly (new [] { generic }, "GenericUcoCtorTest"); using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); var nctorMethodHandle = FindNctorUcoMethod (reader); + Assert.False (nctorMethodHandle.IsNil, "Open-generic ACWs should emit a throwing nctor_*_uco method"); - if (!nctorMethodHandle.IsNil) { - // If a nctor_*_uco method exists for the generic type, it must be a no-op (no exception regions). - var nctorMethod = reader.GetMethodDefinition (nctorMethodHandle); - var body = pe.GetMethodBody (nctorMethod.RelativeVirtualAddress); - Assert.NotNull (body); - Assert.Empty (body.ExceptionRegions); - } - // Open-generic types do not get a nctor_*_uco wrapper — no UCO ctors for generics. + var nctorMethod = reader.GetMethodDefinition (nctorMethodHandle); + var body = pe.GetMethodBody (nctorMethod.RelativeVirtualAddress); + Assert.NotNull (body); + + var regions = body.ExceptionRegions; + Assert.True (regions.Length >= 2, + $"Open-generic UCO constructor should have at least 2 exception regions, found {regions.Length}"); + Assert.Contains (regions, r => r.Kind == ExceptionRegionKind.Catch); + Assert.Contains (regions, r => r.Kind == ExceptionRegionKind.Finally); + + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("NotSupportedException", typeNames); + + var ilBytes = body.GetILBytes (); + Assert.NotNull (ilBytes); + var memberRefHandles = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => MetadataTokens.MemberReferenceHandle (i)) + .ToList (); + var beginHandle = memberRefHandles.First (h => reader.GetString (reader.GetMemberReference (h).Name) == "BeginMarshalMethod"); + var endHandle = memberRefHandles.First (h => reader.GetString (reader.GetMemberReference (h).Name) == "EndMarshalMethod"); + var exHandle = memberRefHandles.First (h => reader.GetString (reader.GetMemberReference (h).Name) == "OnUserUnhandledException"); + int beginToken = MetadataTokens.GetToken (beginHandle); + int endToken = MetadataTokens.GetToken (endHandle); + int exToken = MetadataTokens.GetToken (exHandle); + Assert.True (ILContainsCallToken (ilBytes, beginToken), "open-generic nctor_*_uco IL should call BeginMarshalMethod"); + Assert.True (ILContainsCallToken (ilBytes, endToken), "open-generic nctor_*_uco IL should call EndMarshalMethod"); + Assert.True (ILContainsCallToken (ilBytes, exToken), "open-generic nctor_*_uco IL should call OnUserUnhandledException"); } [Fact] From 4b6aa5e6a4e5dbe195b2e23cfa820d0a4d57db7d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 4 May 2026 09:56:34 +0200 Subject: [PATCH 4/7] Strengthen open generic UCO constructor test Assert the generated nctor_*_uco body constructs and throws NotSupportedException, rather than only checking assembly-level references. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/FixtureTestBase.cs | 28 +++++++++++++++++++ .../TypeMapAssemblyGeneratorTests.cs | 12 ++++++++ 2 files changed, 40 insertions(+) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index d9bb30c1ecf..2767c273507 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -143,4 +143,32 @@ private protected static List GetMemberRefNames (MetadataReader reader) .Select (m => reader.GetString (m.Name)) .ToList (); + /// + /// Returns true if the IL byte stream contains a Call (0x28) or Callvirt (0x6F) instruction + /// whose metadata token matches . + /// + private protected static bool ILContainsCallToken (byte[] ilBytes, int token) + { + return ILContainsOpcodeToken (ilBytes, token, 0x28, 0x6F); + } + + private protected static bool ILContainsNewobjToken (byte[] ilBytes, int token) + { + return ILContainsOpcodeToken (ilBytes, token, (byte) ILOpCode.Newobj); + } + + static bool ILContainsOpcodeToken (byte[] ilBytes, int token, params byte[] opcodes) + { + byte t0 = (byte)(token & 0xFF); + byte t1 = (byte)((token >> 8) & 0xFF); + byte t2 = (byte)((token >> 16) & 0xFF); + byte t3 = (byte)((token >> 24) & 0xFF); + for (int i = 0; i < ilBytes.Length - 4; i++) { + if (opcodes.Contains (ilBytes [i]) && + ilBytes[i + 1] == t0 && ilBytes[i + 2] == t1 && + ilBytes[i + 3] == t2 && ilBytes[i + 4] == t3) + return true; + } + return false; + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 4e5c6dab7f0..ec37cbff9af 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -1207,12 +1207,24 @@ public void Generate_UcoConstructor_GenericDefinition_ThrowsWithMarshalMethodPat var memberRefHandles = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) .Select (i => MetadataTokens.MemberReferenceHandle (i)) .ToList (); + var notSupportedExceptionCtorHandle = memberRefHandles.First (h => { + var member = reader.GetMemberReference (h); + if (reader.GetString (member.Name) != ".ctor" || member.Parent.Kind != HandleKind.TypeReference) { + return false; + } + + var parent = reader.GetTypeReference ((TypeReferenceHandle) member.Parent); + return reader.GetString (parent.Name) == "NotSupportedException"; + }); var beginHandle = memberRefHandles.First (h => reader.GetString (reader.GetMemberReference (h).Name) == "BeginMarshalMethod"); var endHandle = memberRefHandles.First (h => reader.GetString (reader.GetMemberReference (h).Name) == "EndMarshalMethod"); var exHandle = memberRefHandles.First (h => reader.GetString (reader.GetMemberReference (h).Name) == "OnUserUnhandledException"); + int notSupportedExceptionCtorToken = MetadataTokens.GetToken (notSupportedExceptionCtorHandle); int beginToken = MetadataTokens.GetToken (beginHandle); int endToken = MetadataTokens.GetToken (endHandle); int exToken = MetadataTokens.GetToken (exHandle); + Assert.True (ILContainsNewobjToken (ilBytes, notSupportedExceptionCtorToken), "open-generic nctor_*_uco IL should construct NotSupportedException"); + Assert.True (ilBytes.Contains ((byte) ILOpCode.Throw), "open-generic nctor_*_uco IL should throw"); Assert.True (ILContainsCallToken (ilBytes, beginToken), "open-generic nctor_*_uco IL should call BeginMarshalMethod"); Assert.True (ILContainsCallToken (ilBytes, endToken), "open-generic nctor_*_uco IL should call EndMarshalMethod"); Assert.True (ILContainsCallToken (ilBytes, exToken), "open-generic nctor_*_uco IL should call OnUserUnhandledException"); From 5fced2bb89e69cc329851f4839f5acda09e84624 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 4 May 2026 23:09:37 +0200 Subject: [PATCH 5/7] Re-enable open generic trimmable runtime test Remove the stale trimmable typemap exclusion for NewOpenGenericTypeThrows and drop the unrelated GetType exclusion added during rebase conflict resolution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs | 9 --------- 1 file changed, 9 deletions(-) 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 7c77b6b6787..6ca3759d126 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 @@ -70,15 +70,6 @@ protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) // net.dot.jni.test.GenericHolder Java class not in APK "Java.InteropTests.JniTypeManagerTests.CannotCreateGenericHolderFromJava", - // JniPrimitiveArrayInfo lookup fails for JavaBooleanArray — - // our typemap returns JavaBooleanArray for "Z" via JavaPrimitiveArray<> - // alias, which collides with the legacy GetPrimitiveArrayTypesForSimpleReference - // that expects only primitive CLR types. Out of scope for this PR. - "Java.InteropTests.JniTypeManagerTests.GetType", - - // Open generic type handling differs from non-trimmable - "Java.InteropTests.JnienvTest.NewOpenGenericTypeThrows", - // Throwable subclass registration "Java.InteropTests.JnienvTest.ActivatedDirectThrowableSubclassesShouldBeRegistered", From 493ca2d735beeb0ee73c0af9c1a15bc3d1f6d7d6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 5 May 2026 07:28:54 +0200 Subject: [PATCH 6/7] Re-enable generic holder trimmable runtime test Remove the stale trimmable typemap exclusion for CannotCreateGenericHolderFromJava now that open generic Java construction is rejected correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs | 3 --- 1 file changed, 3 deletions(-) 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 6ca3759d126..83124cddee4 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 @@ -67,9 +67,6 @@ protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) "Java.InteropTests.JniPeerMembersTests.ReplacementTypeUsedForMethodLookup", "Java.InteropTests.JniPeerMembersTests.ReplaceStaticMethodName", - // net.dot.jni.test.GenericHolder Java class not in APK - "Java.InteropTests.JniTypeManagerTests.CannotCreateGenericHolderFromJava", - // Throwable subclass registration "Java.InteropTests.JnienvTest.ActivatedDirectThrowableSubclassesShouldBeRegistered", From 1789322d1cc134e747323c659b844b6bddddf7be Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 5 May 2026 11:03:19 -0500 Subject: [PATCH 7/7] Address review: use ILOpCode enum and fix Mono-style spacing - Replace magic bytes 0x28, 0x6F with (byte) ILOpCode.Call, (byte) ILOpCode.Callvirt for consistency with ILContainsNewobjToken - Add missing spaces before [ on array accesses in ILContainsOpcodeToken Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/FixtureTestBase.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 2767c273507..44b5bff4f85 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -149,7 +149,7 @@ private protected static List GetMemberRefNames (MetadataReader reader) /// private protected static bool ILContainsCallToken (byte[] ilBytes, int token) { - return ILContainsOpcodeToken (ilBytes, token, 0x28, 0x6F); + return ILContainsOpcodeToken (ilBytes, token, (byte) ILOpCode.Call, (byte) ILOpCode.Callvirt); } private protected static bool ILContainsNewobjToken (byte[] ilBytes, int token) @@ -165,8 +165,8 @@ static bool ILContainsOpcodeToken (byte[] ilBytes, int token, params byte[] opco byte t3 = (byte)((token >> 24) & 0xFF); for (int i = 0; i < ilBytes.Length - 4; i++) { if (opcodes.Contains (ilBytes [i]) && - ilBytes[i + 1] == t0 && ilBytes[i + 2] == t1 && - ilBytes[i + 3] == t2 && ilBytes[i + 4] == t3) + ilBytes [i + 1] == t0 && ilBytes [i + 2] == t1 && + ilBytes [i + 3] == t2 && ilBytes [i + 4] == t3) return true; } return false;