diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index ddec657a085..ba1dbc484f7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -316,6 +316,51 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, bodyOffset, default); } + /// + /// Emits a method body with full support for exception regions (try/catch/finally). + /// The callback receives both the and the + /// so it can emit IL and register exception regions + /// (e.g. via cfb.AddCatchRegion / cfb.AddFinallyRegion) in one pass. + /// A is always created for this overload. + /// + public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, + Action encodeSig, + Action emitIL, + Action? encodeLocals) + { + _sigBlob.Clear (); + encodeSig (new BlobEncoder (_sigBlob)); + // Capture the sig blob handle before emitIL, because emitIL callbacks + // may call AddMemberRef which clears and repopulates _sigBlob. + var sigBlobHandle = Metadata.GetOrAddBlob (_sigBlob); + + StandaloneSignatureHandle localSigHandle = default; + if (encodeLocals != null) { + var localSigBlob = new BlobBuilder (32); + encodeLocals (localSigBlob); + localSigHandle = Metadata.AddStandaloneSignature (Metadata.GetOrAddBlob (localSigBlob)); + } + + _codeBlob.Clear (); + var cfb = new ControlFlowBuilder (); + var encoder = new InstructionEncoder (_codeBlob, cfb); + emitIL (encoder, cfb); + + while (ILBuilder.Count % 4 != 0) { + ILBuilder.WriteByte (0); + } + var bodyEncoder = new MethodBodyStreamEncoder (ILBuilder); + int bodyOffset = localSigHandle.IsNil + ? bodyEncoder.AddMethodBody (encoder) + : bodyEncoder.AddMethodBody (encoder, maxStack: 8, localSigHandle, MethodBodyAttributes.InitLocals); + + return Metadata.AddMethodDefinition ( + attrs, MethodImplAttributes.IL, + Metadata.GetOrAddString (name), + sigBlobHandle, + bodyOffset, default); + } + /// /// Builds a TypeSpec for a closed generic type with a single type argument. /// For example, MakeGenericTypeSpec(openAttrRef, javaLangObjectRef) produces diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 9bb04468b6c..eaacbd34678 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -100,6 +100,13 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _jniNativeMethodRef; TypeReferenceHandle _jniEnvironmentRef; TypeReferenceHandle _jniEnvironmentTypesRef; + TypeReferenceHandle _jniTransitionRef; + TypeReferenceHandle _jniRuntimeRef; + TypeReferenceHandle _exceptionRef; + + MemberReferenceHandle _beginMarshalMethodRef; + MemberReferenceHandle _endMarshalMethodRef; + MemberReferenceHandle _onUserUnhandledExceptionRef; TypeReferenceHandle _readOnlySpanOpenRef; TypeSpecificationHandle _readOnlySpanOfJniNativeMethodSpec; MemberReferenceHandle _jniNativeMethodCtorRef; @@ -205,6 +212,12 @@ void EmitTypeReferences () metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniEnvironment")); _jniEnvironmentTypesRef = metadata.AddTypeReference (_jniEnvironmentRef, default, metadata.GetOrAddString ("Types")); + _jniTransitionRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniTransition")); + _jniRuntimeRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniRuntime")); + _exceptionRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Exception")); // ReadOnlySpan — TypeSpec for generic instantiation _readOnlySpanOpenRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, @@ -295,6 +308,31 @@ void EmitMemberReferences () // Pre-compute the UCO attribute blob — it's always the same 4 bytes (prolog + no named args) _ucoAttrBlobHandle = _pe.BuildAttributeBlob (b => { }); + // JniEnvironment.BeginMarshalMethod(nint jnienv, out JniTransition, out JniRuntime?) -> bool + _beginMarshalMethodRef = _pe.AddMemberRef (_jniEnvironmentRef, "BeginMarshalMethod", + sig => sig.MethodSignature ().Parameters (3, + rt => rt.Type ().Boolean (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type (isByRef: true).Type (_jniTransitionRef, true); + p.AddParameter ().Type (isByRef: true).Type (_jniRuntimeRef, false); + })); + + // JniEnvironment.EndMarshalMethod(ref JniTransition) -> void + _endMarshalMethodRef = _pe.AddMemberRef (_jniEnvironmentRef, "EndMarshalMethod", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type (isByRef: true).Type (_jniTransitionRef, true))); + + // JniRuntime.OnUserUnhandledException(ref JniTransition, Exception) -> void + _onUserUnhandledExceptionRef = _pe.AddMemberRef (_jniRuntimeRef, "OnUserUnhandledException", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type (isByRef: true).Type (_jniTransitionRef, true); + p.AddParameter ().Type ().Type (_exceptionRef, false); + })); + EmitTypeMapAttributeCtorRef (); EmitTypeMapAssociationAttributeCtorRef (); EmitJavaPeerAliasesAttributeCtorRef (); @@ -841,88 +879,191 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy var ctorRef = AddJavaInteropActivationCtorRef ( activationCtor.IsOnLeafType ? targetTypeRef : _pe.ResolveTypeRef (activationCtor.DeclaringType)); + // Locals: + // 0: JniTransition (envp) — out-parameter for BeginMarshalMethod + // 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 => { - // Skip activation if the object is being created from managed code - // (e.g., JNIEnv.StartCreateInstance / JNIEnv.NewObject). - var skipLabel = encoder.DefineLabel (); - encoder.Call (_withinNewObjectScopeRef); - encoder.Branch (ILOpCode.Brtrue, skipLabel); - + (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { if (!activationCtor.IsOnLeafType) { - encoder.OpCode (ILOpCode.Ldtoken); - encoder.Token (targetTypeRef); - encoder.Call (_getTypeFromHandleRef); - encoder.Call (_getUninitializedObjectRef); - encoder.OpCode (ILOpCode.Castclass); - encoder.Token (targetTypeRef); + enc.OpCode (ILOpCode.Ldtoken); + enc.Token (targetTypeRef); + enc.Call (_getTypeFromHandleRef); + enc.Call (_getUninitializedObjectRef); + enc.OpCode (ILOpCode.Castclass); + enc.Token (targetTypeRef); } - encoder.LoadLocalAddress (0); - encoder.LoadArgument (1); // self - encoder.Call (_jniObjectReferenceCtorRef); + enc.LoadLocalAddress (3); // jniRef + enc.LoadArgument (1); // self + enc.Call (_jniObjectReferenceCtorRef); if (activationCtor.IsOnLeafType) { - encoder.LoadLocalAddress (0); - encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (ctorRef); - encoder.OpCode (ILOpCode.Pop); + enc.LoadLocalAddress (3); // ref jniRef + enc.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy + enc.OpCode (ILOpCode.Newobj); + enc.Token (ctorRef); + enc.OpCode (ILOpCode.Pop); } else { - encoder.LoadLocalAddress (0); - encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy - encoder.Call (ctorRef); + enc.LoadLocalAddress (3); // ref jniRef + enc.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy + enc.Call (ctorRef); } - - encoder.MarkLabel (skipLabel); - encoder.OpCode (ILOpCode.Ret); - }, - EncodeJniObjectReferenceLocal, - useBranches: true); + }), + EncodeUcoConstructorLocals_JavaInterop); } else { var ctorRef = AddActivationCtorRef ( activationCtor.IsOnLeafType ? targetTypeRef : _pe.ResolveTypeRef (activationCtor.DeclaringType)); + // Locals: + // 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 => { - // Skip activation if the object is being created from managed code - var skipLabel = encoder.DefineLabel (); - encoder.Call (_withinNewObjectScopeRef); - encoder.Branch (ILOpCode.Brtrue, skipLabel); - + (encoder, cfb) => EmitUcoConstructorBodyWithMarshal (encoder, cfb, enc => { if (activationCtor.IsOnLeafType) { - encoder.LoadArgument (1); // self - encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (ctorRef); - encoder.OpCode (ILOpCode.Pop); + enc.LoadArgument (1); // self + enc.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer + enc.OpCode (ILOpCode.Newobj); + enc.Token (ctorRef); + enc.OpCode (ILOpCode.Pop); } else { - encoder.OpCode (ILOpCode.Ldtoken); - encoder.Token (targetTypeRef); - encoder.Call (_getTypeFromHandleRef); - encoder.Call (_getUninitializedObjectRef); - encoder.OpCode (ILOpCode.Castclass); - encoder.Token (targetTypeRef); - - encoder.LoadArgument (1); // self - encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer - encoder.Call (ctorRef); + enc.OpCode (ILOpCode.Ldtoken); + enc.Token (targetTypeRef); + enc.Call (_getTypeFromHandleRef); + enc.Call (_getUninitializedObjectRef); + enc.OpCode (ILOpCode.Castclass); + enc.Token (targetTypeRef); + + enc.LoadArgument (1); // self + enc.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer + enc.Call (ctorRef); } - - encoder.MarkLabel (skipLabel); - encoder.OpCode (ILOpCode.Ret); - }, - encodeLocals: null, - useBranches: true); + }), + EncodeUcoConstructorLocals_Standard); } AddUnmanagedCallersOnlyAttribute (handle); return handle; } + /// + /// Emits the common try/catch/finally marshal-method wrapper pattern used by all + /// non-generic UCO constructor bodies: + /// + /// if (!JniEnvironment.BeginMarshalMethod(jnienv, out envp, out runtime)) return; + /// try { + /// if (!JniEnvironment.WithinNewObjectScope) { [emitActivation] } + /// } catch (Exception e) { + /// runtime?.OnUserUnhandledException(ref envp, e); + /// } finally { + /// JniEnvironment.EndMarshalMethod(ref envp); + /// } + /// + /// Locals 0 (JniTransition envp) and 1 (JniRuntime? runtime) must be declared by the caller. + /// Local 2 (Exception e) must also be declared. Any activation-specific locals start at index 3. + /// + void EmitUcoConstructorBodyWithMarshal (InstructionEncoder encoder, ControlFlowBuilder cfb, Action emitActivation) + { + var skipLabel = encoder.DefineLabel (); + var tryStart = encoder.DefineLabel (); + var catchStart = encoder.DefineLabel (); + var finallyStart = encoder.DefineLabel (); + var afterAll = encoder.DefineLabel (); + var endCatch = encoder.DefineLabel (); + + // Preamble: call BeginMarshalMethod; skip everything if it returns false. + encoder.LoadArgument (0); // jnienv + encoder.LoadLocalAddress (0); // out JniTransition (local 0) + encoder.LoadLocalAddress (1); // out JniRuntime? (local 1) + encoder.Call (_beginMarshalMethodRef); + encoder.Branch (ILOpCode.Brfalse, afterAll); + + // TRY — check WithinNewObjectScope, then run activation code. + encoder.MarkLabel (tryStart); + encoder.Call (_withinNewObjectScopeRef); + encoder.Branch (ILOpCode.Brtrue, skipLabel); + + emitActivation (encoder); + + encoder.MarkLabel (skipLabel); + encoder.Branch (ILOpCode.Leave, afterAll); + + // CATCH (System.Exception e) + encoder.MarkLabel (catchStart); + encoder.StoreLocal (2); // e = exception (local 2) + encoder.LoadLocal (1); // load runtime (__r) + encoder.Branch (ILOpCode.Brfalse, endCatch); + encoder.LoadLocal (1); // __r for callvirt + encoder.LoadLocalAddress (0); // ref envp + encoder.LoadLocal (2); // e + encoder.OpCode (ILOpCode.Callvirt); + encoder.Token (_onUserUnhandledExceptionRef); + encoder.MarkLabel (endCatch); + encoder.Branch (ILOpCode.Leave, afterAll); + + // FINALLY + encoder.MarkLabel (finallyStart); + encoder.LoadLocalAddress (0); // ref envp + encoder.Call (_endMarshalMethodRef); + encoder.OpCode (ILOpCode.Endfinally); + + // AFTER (both finallyEnd and the early-return target) + encoder.MarkLabel (afterAll); + encoder.OpCode (ILOpCode.Ret); + + // Register exception regions: + // Catch region: try [tryStart, catchStart), handler [catchStart, finallyStart) + // Finally region: try [tryStart, finallyStart), handler [finallyStart, afterAll) + cfb.AddCatchRegion (tryStart, catchStart, catchStart, finallyStart, _exceptionRef); + cfb.AddFinallyRegion (tryStart, finallyStart, finallyStart, afterAll); + } + + /// + /// LOCAL_SIG for UCO constructors without JavaInterop-style activation. + /// Locals: 0=JniTransition, 1=JniRuntime, 2=Exception. + /// + void EncodeUcoConstructorLocals_Standard (BlobBuilder blob) + { + blob.WriteByte (0x07); // LOCAL_SIG + blob.WriteCompressedInteger (3); + // 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_SIG for UCO constructors with JavaInterop-style activation. + /// Locals: 0=JniTransition, 1=JniRuntime, 2=Exception, 3=JniObjectReference. + /// + void EncodeUcoConstructorLocals_JavaInterop (BlobBuilder blob) + { + blob.WriteByte (0x07); // LOCAL_SIG + blob.WriteCompressedInteger (4); + // 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)); + } + void EmitRegisterNatives (List registrations, Dictionary wrapperHandles) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 0d3cfa8c1eb..98b28be0591 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -124,6 +124,13 @@ private protected static JavaPeerInfo MakeInterfacePeer ( }; } + private protected static MethodDefinitionHandle FindNctorUcoMethod (MetadataReader reader) => + reader.MethodDefinitions.FirstOrDefault (h => { + var name = reader.GetString (reader.GetMethodDefinition (h).Name); + return name.StartsWith ("nctor_", StringComparison.Ordinal) && + name.EndsWith ("_uco", StringComparison.Ordinal); + }); + private protected static List GetTypeRefNames (MetadataReader reader) => reader.TypeReferences .Select (h => reader.GetTypeReference (h)) @@ -135,4 +142,23 @@ private protected static List GetMemberRefNames (MetadataReader reader) .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) .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) + { + 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 ((ilBytes[i] == 0x28 || ilBytes[i] == 0x6F) && + 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 4e369aefc57..80f1dafc1f8 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -991,6 +991,145 @@ public void Generate_AliasHolder_HasDeserializableAliasKeys () } } + [Fact] + public void Generate_UcoConstructor_BodyUsesMarshalMethodPattern () + { + // Verify that UCO constructor bodies wrap activation in BeginMarshalMethod/EndMarshalMethod + // with try/catch/finally so that exceptions cannot cross the JNI boundary (causing SIGABRT). + var peer = MakeAcwPeer ("test/UcoCtorExc", "Test.UcoCtorExc", "TestAsm"); + using var stream = GenerateAssembly (new [] { peer }, "UcoCtorMarshalTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + // Member refs for the marshal-method pattern must be present. + var memberNames = GetMemberRefNames (reader); + Assert.Contains ("BeginMarshalMethod", memberNames); + Assert.Contains ("EndMarshalMethod", memberNames); + Assert.Contains ("OnUserUnhandledException", memberNames); + + // Type refs for the marshal-method locals must be present. + var typeNames = GetTypeRefNames (reader); + Assert.Contains ("JniTransition", typeNames); + Assert.Contains ("JniRuntime", typeNames); + Assert.Contains ("Exception", typeNames); + + // Find the nctor_*_uco method. + var nctorMethodHandle = FindNctorUcoMethod (reader); + Assert.False (nctorMethodHandle.IsNil, "Expected a nctor_*_uco method in the generated assembly"); + + // The method body must have exception regions: at least one Catch and one Finally. + var nctorMethod = reader.GetMethodDefinition (nctorMethodHandle); + var body = pe.GetMethodBody (nctorMethod.RelativeVirtualAddress); + Assert.NotNull (body); + var regions = body.ExceptionRegions; + Assert.True (regions.Length >= 2, + $"UCO constructor should have at least 2 exception regions (catch + finally), found {regions.Length}"); + Assert.Contains (regions, r => r.Kind == ExceptionRegionKind.Catch); + Assert.Contains (regions, r => r.Kind == ExceptionRegionKind.Finally); + + // Verify the method body IL actually calls the marshal-method APIs (not just that the refs exist in the assembly). + var il = pe.GetSectionData (nctorMethod.RelativeVirtualAddress); + var ilBytes = body.GetILBytes (); + Assert.NotNull (ilBytes); + var ilContent = System.Text.Encoding.ASCII.GetString (ilBytes); + // Cross-check: the member refs we found must be referenced from within this method body. + // We verify by checking that the IL contains Call/Callvirt opcodes (0x28/0x6F) with tokens + // pointing to the expected member refs. + 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), "nctor_*_uco IL should call BeginMarshalMethod"); + Assert.True (ILContainsCallToken (ilBytes, endToken), "nctor_*_uco IL should call EndMarshalMethod"); + Assert.True (ILContainsCallToken (ilBytes, exToken), "nctor_*_uco IL should call OnUserUnhandledException"); + } + + [Fact] + public void Generate_UcoConstructor_JiStyle_HasExceptionRegions () + { + // Verify the JavaInterop-style UCO constructor activation path also has exception regions. + var peer = MakeAcwPeer ("test/JiUcoCtorExc", "Test.JiUcoCtorExc", "TestAsm") with { + ActivationCtor = new ActivationCtorInfo { + DeclaringTypeName = "Test.JiUcoCtorExc", + DeclaringAssemblyName = "TestAsm", + Style = ActivationCtorStyle.JavaInterop, + }, + }; + using var stream = GenerateAssembly (new [] { peer }, "JiUcoCtorMarshalTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var nctorMethodHandle = FindNctorUcoMethod (reader); + Assert.False (nctorMethodHandle.IsNil, "Expected a nctor_*_uco method in the generated assembly"); + + var nctorMethod = reader.GetMethodDefinition (nctorMethodHandle); + var body = pe.GetMethodBody (nctorMethod.RelativeVirtualAddress); + Assert.NotNull (body); + var regions = body.ExceptionRegions; + Assert.True (regions.Length >= 2, + $"JavaInterop 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); + } + + [Fact] + public void Generate_UcoConstructor_GenericDefinition_NoExceptionRegions () + { + // 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); + + using var stream = GenerateAssembly (new [] { generic }, "GenericUcoCtorTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var nctorMethodHandle = FindNctorUcoMethod (reader); + + 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. + } + + [Fact] + public void Generate_UcoConstructor_InheritedCtor_HasExceptionRegions () + { + // Verify the non-leaf (inherited) activation path also gets exception regions. + var peers = ScanFixtures (); + var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); + Assert.NotNull (simpleActivity.ActivationCtor); + // SimpleActivity does not declare its own (IntPtr, JniHandleOwnership) ctor, + // so the activation ctor is inherited from Activity (DeclaringTypeName != ManagedTypeName). + Assert.NotEqual (simpleActivity.ActivationCtor.DeclaringTypeName, simpleActivity.ManagedTypeName); + + using var stream = GenerateAssembly (new [] { simpleActivity }, "InheritedUcoCtorExcTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var nctorMethodHandle = FindNctorUcoMethod (reader); + Assert.False (nctorMethodHandle.IsNil, "SimpleActivity (ACW) should have a nctor_*_uco method"); + + var nctorMethod = reader.GetMethodDefinition (nctorMethodHandle); + var body = pe.GetMethodBody (nctorMethod.RelativeVirtualAddress); + Assert.NotNull (body); + var regions = body.ExceptionRegions; + Assert.True (regions.Length >= 2, + $"Inherited-ctor 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); + } + [Fact] public void Generate_ProxyTypes_HaveSelfAppliedAttribute () {