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) =>