diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs index a5383701a1e..8dcc2b06479 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs @@ -200,15 +200,34 @@ protected virtual bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (tr return null; } - var peeked = PeekPeer (reference); - if (peeked != null && - (targetType == null || - targetType.IsAssignableFrom (peeked.GetType ()))) { - return peeked; + var existing = PeekPeer (reference); + if (IsCompatiblePeer (existing, targetType)) { + return existing; } - return CreatePeer (ref reference, JniObjectReferenceOptions.Copy, targetType); + + var created = CreatePeer (ref reference, JniObjectReferenceOptions.Copy, targetType); + + // Creating a peer registers it, and registration -- not creation -- decides + // which peer is canonical for a given Java instance. Concurrent callers can + // therefore each create a peer while only one of them wins registration, so + // always hand back the registered winner and discard the loser. Serializing + // creation instead is not an option: CreatePeer() runs activation + // constructors and Java class loading, so a lock held across it would invert + // against Java monitors. + var registered = PeekPeer (reference); + if (!ReferenceEquals (created, registered) && IsCompatiblePeer (registered, targetType)) { + if (created != null) { + DisposePeerUnlessReferenced (created); + } + return registered; + } + + return created; } + static bool IsCompatiblePeer (IJavaPeerable? peer, Type? targetType) + => peer != null && (targetType == null || targetType.IsAssignableFrom (peer.GetType ())); + public abstract IJavaPeerable? CreatePeer ( ref JniObjectReference reference, JniObjectReferenceOptions transfer, diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniValueManagerTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniValueManagerTests.cs index a712105d7be..2a60bded9dd 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniValueManagerTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniRuntime.JniValueManagerTests.cs @@ -2,6 +2,8 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Java.Interop; @@ -30,6 +32,132 @@ public void CreateValue () } } + [Test] + public void GetPeer_ReturnsRegisteredPeerConcurrently () + { + using (var source = new JavaObject ()) + using (var vm = new ConcurrentGetPeerValueManager (source.JniPeerMembers)) + using (var start = new Barrier (2)) { + vm.OnSetRuntime (JniRuntime.CurrentRuntime); + vm.InitialPeekBarrier = start; + + var first = Task.Run (() => vm.GetPeer (source.PeerReference)); + var second = Task.Run (() => vm.GetPeer (source.PeerReference)); + Task.WaitAll (first, second); + + // Both callers miss the cache and create a peer -- creation is deliberately + // not serialized, because CreatePeer() runs activation constructors. Only one + // of them wins registration, and GetPeer() must hand that winner to both. + Assert.AreEqual (2, vm.CreatePeerCount); + Assert.AreSame (vm.RegisteredPeer, first.Result); + Assert.AreSame (first.Result, second.Result); + } + } + + class ConcurrentGetPeerValueManager : MyValueManager { + + readonly JniPeerMembers peerMembers; + IJavaPeerable registeredPeer; + int peekPeerCount; + int createPeerCount; + + public ConcurrentGetPeerValueManager (JniPeerMembers peerMembers) + { + this.peerMembers = peerMembers; + } + + public Barrier InitialPeekBarrier { get; set; } + + public int CreatePeerCount => createPeerCount; + + public IJavaPeerable RegisteredPeer => Volatile.Read (ref registeredPeer); + + public override IJavaPeerable PeekPeer (JniObjectReference reference) + { + var peer = Volatile.Read (ref registeredPeer); + // Hold both callers at their *first* peek so both are guaranteed to miss + // before either one registers a peer. + if (Interlocked.Increment (ref peekPeerCount) <= 2 && InitialPeekBarrier != null) { + var barrier = InitialPeekBarrier; + if (!barrier.SignalAndWait (TimeSpan.FromSeconds (10))) { + throw new TimeoutException ("Timed out waiting for concurrent GetPeer() calls."); + } + } + return peer; + } + + public override IJavaPeerable CreatePeer ( + ref JniObjectReference reference, + JniObjectReferenceOptions transfer, + [DynamicallyAccessedMembers (Constructors)] + Type targetType) + { + Interlocked.Increment (ref createPeerCount); + // Each peer owns its own reference, so discarding the loser cannot + // invalidate the winner or the source object. + var peer = new TestPeer (reference.NewGlobalRef (), peerMembers); + Interlocked.CompareExchange (ref registeredPeer, peer, null); + return peer; + } + + } + + class TestPeer : IJavaPeerable { + + JniObjectReference reference; + int identityHashCode; + JniManagedPeerStates state; + + public TestPeer (JniObjectReference reference, JniPeerMembers peerMembers) + { + this.reference = reference; + JniPeerMembers = peerMembers; + } + + public int JniIdentityHashCode => identityHashCode; + + public JniObjectReference PeerReference => reference; + + public JniPeerMembers JniPeerMembers { get; } + + public JniManagedPeerStates JniManagedPeerState => state; + + public void SetJniIdentityHashCode (int value) + { + identityHashCode = value; + } + + public void SetPeerReference (JniObjectReference value) + { + reference = value; + } + + public void SetJniManagedPeerState (JniManagedPeerStates value) + { + state = value; + } + + public void UnregisterFromRuntime () + { + } + + public void DisposeUnlessReferenced () + { + } + + public void Disposed () + { + } + + public void Finalized () + { + } + + public void Dispose () + { + } + } + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "MyValueManager intentionally uses reflection-backed value manager behavior for tests.")] [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "MyValueManager intentionally uses reflection-backed value manager behavior for tests.")] class MyValueManager : JniRuntime.ReflectionJniValueManager { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 17865148a4e..4ef713e2172 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -108,6 +108,7 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _getActivationPeerRef; MemberReferenceHandle _setActivationPeerReferenceRef; MemberReferenceHandle _markActivationPeerReplaceableRef; + MemberReferenceHandle _markCreatedPeerReplaceableRef; MemberReferenceHandle _waitForBridgeProcessingRef; MemberReferenceHandle _androidEnvironmentUnhandledExceptionRef; MemberReferenceHandle _ucoAttrCtorRef; @@ -432,6 +433,11 @@ void EmitMemberReferences () rt => rt.Void (), p => p.AddParameter ().Type ().IntPtr ())); + _markCreatedPeerReplaceableRef = _pe.AddMemberRef (_javaPeerProxyRef, "MarkCreatedPeerReplaceable", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().Type (_iJavaPeerableRef, false))); + _waitForBridgeProcessingRef = _pe.AddMemberRef (_androidRuntimeInternalRef, "WaitForBridgeProcessing", sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { })); @@ -800,25 +806,55 @@ void EmitCreateInstanceGenericDefinition () }); } + /// + /// Emits CreateInstance for XA-style activation (leaf type): + /// var obj = (TargetType)RuntimeHelpers.GetUninitializedObject(typeof(TargetType)); + /// JavaPeerProxy.MarkCreatedPeerReplaceable(obj); + /// obj.Ctor(handle, ownership); + /// return obj; + /// + /// + /// The peer is allocated uninitialized and marked replaceable before the activation + /// constructor runs, because that constructor is what registers the peer. See + /// JavaPeerProxy.MarkCreatedPeerReplaceable(). This mirrors + /// TypeManager.CreateProxy() in the non-trimmable typemap implementation. + /// void EmitCreateInstanceViaNewobj (EntityHandle typeRef) { var ctorRef = AddActivationCtorRef (typeRef); EmitCreateInstanceBody (encoder => { + EmitUninitializedReplaceablePeer (encoder, typeRef); + + encoder.OpCode (ILOpCode.Dup); encoder.OpCode (ILOpCode.Ldarg_1); encoder.OpCode (ILOpCode.Ldarg_2); - encoder.NewObject (ctorRef, parameterCount: 2); + encoder.Call (ctorRef, parameterCount: 2, isInstance: true); + encoder.Return (returnsValue: true); }); } + /// + /// Emits, leaving the new instance on the stack: + /// var obj = (TargetType)RuntimeHelpers.GetUninitializedObject(typeof(TargetType)); + /// JavaPeerProxy.MarkCreatedPeerReplaceable(obj); + /// + void EmitUninitializedReplaceablePeer (PEAssemblyBuilder.TrackedInstructionEncoder encoder, EntityHandle typeRef) + { + encoder.LoadToken (typeRef); + encoder.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); + encoder.Call (_getUninitializedObjectRef, parameterCount: 1, returnsValue: true); + encoder.CastClass (typeRef); + + encoder.OpCode (ILOpCode.Dup); + encoder.Call (_markCreatedPeerReplaceableRef, parameterCount: 1); + } + void EmitCreateInstanceInheritedCtor (EntityHandle targetTypeRef, ActivationCtorData activationCtor) { var baseActivationCtorRef = AddActivationCtorRef (_pe.ResolveTypeRef (activationCtor.DeclaringType)); EmitCreateInstanceBody (encoder => { - encoder.LoadToken (targetTypeRef); - encoder.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); - encoder.Call (_getUninitializedObjectRef, parameterCount: 1, returnsValue: true); - encoder.CastClass (targetTypeRef); + EmitUninitializedReplaceablePeer (encoder, targetTypeRef); encoder.OpCode (ILOpCode.Dup); encoder.OpCode (ILOpCode.Ldarg_1); @@ -831,16 +867,22 @@ void EmitCreateInstanceInheritedCtor (EntityHandle targetTypeRef, ActivationCtor /// /// Emits CreateInstance for JavaInterop-style activation (leaf type): + /// var result = (TargetType)RuntimeHelpers.GetUninitializedObject(typeof(TargetType)); + /// JavaPeerProxy.MarkCreatedPeerReplaceable(result); /// var jniRef = new JniObjectReference(handle); - /// var result = new TargetType(ref jniRef, JniObjectReferenceOptions.Copy); + /// result.Ctor(ref jniRef, JniObjectReferenceOptions.Copy); /// JNIEnv.DeleteRef(handle, ownership); /// return result; /// + /// + /// See for why the instance is allocated + /// uninitialized and marked replaceable before its activation constructor runs. + /// void EmitCreateInstanceViaJavaInteropNewobj (EntityHandle typeRef) { var ctorRef = AddJavaInteropActivationCtorRef (typeRef); EmitCreateInstanceBodyWithLocals ( - EncodeJniObjectReferenceAndObjectLocals, + EncodeJniObjectReferenceLocal, encoder => { // var jniRef = new JniObjectReference(handle, JniObjectReferenceType.Invalid); encoder.LoadLocalAddress (0); @@ -848,18 +890,20 @@ void EmitCreateInstanceViaJavaInteropNewobj (EntityHandle typeRef) encoder.LoadConstantI4 (0); // JniObjectReferenceType.Invalid encoder.Call (_jniObjectReferenceCtorRef, parameterCount: 2, isInstance: true); - // var result = new TargetType(ref jniRef, JniObjectReferenceOptions.Copy); + EmitUninitializedReplaceablePeer (encoder, typeRef); + + // result.Ctor(ref jniRef, JniObjectReferenceOptions.Copy); + // The result stays on the stack across JNIEnv.DeleteRef() below. + encoder.OpCode (ILOpCode.Dup); encoder.LoadLocalAddress (0); encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy - encoder.NewObject (ctorRef, parameterCount: 2); - encoder.StoreLocal (1); // save result + encoder.Call (ctorRef, parameterCount: 2, isInstance: true); // JNIEnv.DeleteRef(handle, ownership); encoder.OpCode (ILOpCode.Ldarg_1); // handle encoder.OpCode (ILOpCode.Ldarg_2); // ownership encoder.Call (_jniEnvDeleteRefRef, parameterCount: 2); - encoder.LoadLocal (1); // load result encoder.Return (returnsValue: true); }); } @@ -879,10 +923,8 @@ void EmitCreateInstanceInheritedJavaInteropCtor (EntityHandle targetTypeRef, Act EncodeJniObjectReferenceLocal, encoder => { // var obj = (TargetType)RuntimeHelpers.GetUninitializedObject(typeof(TargetType)); - encoder.LoadToken (targetTypeRef); - encoder.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); - encoder.Call (_getUninitializedObjectRef, parameterCount: 1, returnsValue: true); - encoder.CastClass (targetTypeRef); + // JavaPeerProxy.MarkCreatedPeerReplaceable(obj); + EmitUninitializedReplaceablePeer (encoder, targetTypeRef); // dup obj (one copy for the call, one for the return) encoder.OpCode (ILOpCode.Dup); @@ -916,18 +958,6 @@ void EncodeJniObjectReferenceLocal (BlobBuilder blob) blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniObjectReferenceRef)); } - void EncodeJniObjectReferenceAndObjectLocals (BlobBuilder blob) - { - // LOCAL_SIG header (0x07), count = 2: - // local 0: JniObjectReference (valuetype) - // local 1: object (for storing the newobj result across the DeleteRef call) - blob.WriteByte ((byte) SignatureKind.LocalVariables); - blob.WriteCompressedInteger (2); // 2 local variables - blob.WriteByte ((byte) SignatureTypeKind.ValueType); - blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniObjectReferenceRef)); - blob.WriteByte ((byte) SignatureTypeCode.Object); - } - MemberReferenceHandle AddJavaInteropActivationCtorRef (EntityHandle declaringTypeRef) { return _pe.AddMemberRef (declaringTypeRef, ".ctor", diff --git a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs index f6d966df235..3465a10dde5 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs @@ -110,6 +110,30 @@ public static void MarkActivationPeerReplaceable (IntPtr jniSelf) peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); } + /// + /// Marks an implicitly created peer as replaceable, before its activation + /// constructor runs. + /// + /// + /// Generated overrides call this on the + /// uninitialized instance before invoking the activation constructor, because the + /// constructor is what registers the peer and + /// JniRuntime.JniValueManager.AddPeer() arbitrates between an incoming and + /// an already registered peer using . + /// A peer that only becomes replaceable *after* it is registered would let a second + /// implicitly created peer evict it, so two threads wrapping the same Java instance + /// could each end up holding a different wrapper. + /// + /// This mirrors TypeManager.CreateProxy(), which pre-marks the uninitialized + /// instance for the same reason. + /// + public static void MarkCreatedPeerReplaceable (IJavaPeerable peer) + { + ArgumentNullException.ThrowIfNull (peer); + + peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); + } + static bool IsActivationPeer (IJavaPeerable peer) { var state = peer.JniManagedPeerState; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs index 18f4ef0aaa9..e0134c7b084 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs @@ -136,7 +136,9 @@ public static void AddPeer (IJavaPeerable value) !value.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable)) { peer.Dispose (); peers [i] = new ReferenceTrackingHandle (value); - } else if (JniEnvironment.Runtime.ObjectReferenceManager.LogGlobalReferenceMessages) { + } else if (JniEnvironment.Runtime.ObjectReferenceManager.LogGlobalReferenceMessages && + (!target.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable) || + !value.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable))) { WarnNotReplacing (key, value, target); } GC.KeepAlive (target); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index f887629cab6..9c8761c46b7 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; using Android.Runtime; @@ -395,7 +396,13 @@ targetType is not null && return null; } - return (IJavaPeerable) ctor.Invoke ([handle, transfer]); + // Allocate and mark the peer before running the activation constructor, which is + // what registers it; see JavaPeerProxy.MarkCreatedPeerReplaceable(). Generated + // JavaPeerProxy.CreateInstance() overrides do the same thing. + var peer = (IJavaPeerable) RuntimeHelpers.GetUninitializedObject (closedType); + JavaPeerProxy.MarkCreatedPeerReplaceable (peer); + ctor.Invoke (peer, [handle, transfer]); + return peer; } static void MarkCreatedPeer (IJavaPeerable peer) diff --git a/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt index 7fdf466ee6b..2015895ea3b 100644 --- a/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt @@ -4375,6 +4375,7 @@ static Android.Widget.PhotoPicker.PhotoPickerSelectionParams.Creator.get -> Andr static Android.Widget.PhotoPicker.PhotoPickerUiCustomizationParams.Creator.get -> Android.OS.IParcelableCreator! static Java.Interop.JavaPeerProxy.GetActivationPeer(nint jniSelf) -> Java.Interop.IJavaPeerable? static Java.Interop.JavaPeerProxy.MarkActivationPeerReplaceable(nint jniSelf) -> void +static Java.Interop.JavaPeerProxy.MarkCreatedPeerReplaceable(Java.Interop.IJavaPeerable! peer) -> void static Java.Interop.JavaPeerProxy.SetActivationPeerReference(Java.Interop.IJavaPeerable! peer, nint jniSelf) -> void static Java.Interop.JavaPeerProxy.ShouldSkipActivation(nint jniSelf) -> bool static Java.Lang.Character.UnicodeBlock.CjkUnifiedIdeographsExtensionI.get -> Java.Lang.Character.UnicodeBlock? diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index d448de126f5..506918dfb66 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -425,6 +425,38 @@ public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () Assert.True (ctorRefs.Count >= 2, "Should have ctor refs for proxy base + target type"); } + [Fact] + public void Generate_LeafCtor_CreateInstanceMarksPeerReplaceableBeforeActivation () + { + var peers = ScanFixtures (); + // ClickableView has its own (IntPtr, JniHandleOwnership) ctor + var clickableView = peers.First (p => p.JavaName == "my/app/ClickableView"); + + using var stream = GenerateAssembly (new [] { clickableView }, "LeafCtorPreMarkTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + AssertCreateInstancePreMarksPeer (pe, reader, "MyApp_ClickableView_Proxy", "ClickableView"); + } + + [Fact] + public void Generate_LeafJavaInteropCtor_CreateInstanceMarksPeerReplaceableBeforeActivation () + { + var peer = MakeAcwPeer ("test/JiLeafTarget", "Test.JiLeafTarget", "TestAsm") with { + ActivationCtor = new ActivationCtorInfo { + DeclaringTypeName = "Test.JiLeafTarget", + DeclaringAssemblyName = "TestAsm", + Style = ActivationCtorStyle.JavaInterop, + }, + }; + + using var stream = GenerateAssembly (new [] { peer }, "LeafJiCtorPreMarkTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + AssertCreateInstancePreMarksPeer (pe, reader, "Test_JiLeafTarget_Proxy", "JiLeafTarget"); + } + [Fact] public void Generate_InheritedCtor_CreateInstanceDoesNotActivate () { @@ -2698,6 +2730,61 @@ static byte[] GetNctorUcoIL (PEReader pe, MetadataReader reader) } static void AssertCreateInstanceReturnsNull (PEReader pe, MetadataReader reader, string proxyTypeName) + { + var ilBytes = GetCreateInstanceIL (pe, reader, proxyTypeName); + Assert.Equal (new [] { (byte) ILOpCode.Ldnull, (byte) ILOpCode.Ret }, ilBytes); + } + + /// + /// Asserts that CreateInstance allocates the peer uninitialized and marks it replaceable + /// *before* invoking the activation constructor, rather than using newobj. + /// The activation constructor is what registers the peer, and + /// JniRuntime.JniValueManager.AddPeer() arbitrates using + /// JniManagedPeerStates.Replaceable: a peer that only becomes replaceable after it + /// is registered can be evicted by a second implicitly created peer, so two threads + /// wrapping the same Java instance could each keep a different wrapper. + /// + static void AssertCreateInstancePreMarksPeer ( + PEReader pe, + MetadataReader reader, + string proxyTypeName, + string targetTypeShortName) + { + var ilBytes = GetCreateInstanceIL (pe, reader, proxyTypeName); + + Assert.True ( + AllMemberRefHandles (reader) + .Where (h => reader.GetString (reader.GetMemberReference (h).Name) == "GetUninitializedObject") + .Any (h => ILContainsCallToken (ilBytes, MetadataTokens.GetToken (h))), + "CreateInstance should allocate the peer via RuntimeHelpers.GetUninitializedObject()"); + + Assert.True ( + AllMemberRefHandles (reader) + .Where (h => reader.GetString (reader.GetMemberReference (h).Name) == "MarkCreatedPeerReplaceable") + .Any (h => ILContainsCallToken (ilBytes, MetadataTokens.GetToken (h))), + "CreateInstance should call JavaPeerProxy.MarkCreatedPeerReplaceable()"); + + var activationCtors = AllMemberRefHandles (reader) + .Where (h => { + var mref = reader.GetMemberReference (h); + if (reader.GetString (mref.Name) != ".ctor" || mref.Parent.Kind != HandleKind.TypeReference) { + return false; + } + var typeRef = reader.GetTypeReference ((TypeReferenceHandle) mref.Parent); + return reader.GetString (typeRef.Name) == targetTypeShortName; + }) + .ToList (); + Assert.NotEmpty (activationCtors); + + Assert.True ( + activationCtors.Any (h => ILContainsCallToken (ilBytes, MetadataTokens.GetToken (h))), + $"CreateInstance should `call` the {targetTypeShortName} activation ctor on the pre-marked instance"); + Assert.All (activationCtors, h => Assert.False ( + ILContainsNewobjToken (ilBytes, MetadataTokens.GetToken (h)), + $"CreateInstance must not `newobj` {targetTypeShortName}: the peer would register before it is marked replaceable")); + } + + static byte[] GetCreateInstanceIL (PEReader pe, MetadataReader reader, string proxyTypeName) { var proxyTypeHandle = reader.TypeDefinitions.First (h => { var type = reader.GetTypeDefinition (h); @@ -2712,7 +2799,7 @@ static void AssertCreateInstanceReturnsNull (PEReader pe, MetadataReader reader, Assert.NotNull (body); var ilBytes = body.GetILBytes (); Assert.NotNull (ilBytes); - Assert.Equal (new [] { (byte) ILOpCode.Ldnull, (byte) ILOpCode.Ret }, ilBytes!); + return ilBytes!; } static List AllMemberRefHandles (MetadataReader reader) =>