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 ()
{