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/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index d9bb30c1ecf..44b5bff4f85 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, (byte) ILOpCode.Call, (byte) ILOpCode.Callvirt); + } + + 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 8ece123b853..ec37cbff9af 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -1173,28 +1173,61 @@ 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 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"); } [Fact] 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..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,12 +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", - - // Open generic type handling differs from non-trimmable - "Java.InteropTests.JnienvTest.NewOpenGenericTypeThrows", - // Throwable subclass registration "Java.InteropTests.JnienvTest.ActivatedDirectThrowableSubclassesShouldBeRegistered",