Skip to content

Commit d3d3a1b

Browse files
authored
[Java.Interop] JNIEnv::NewObject and Replaceable instances (#1323)
Context: 3043d89 Context: dec35f5 Context: dotnet/android#9862 Context: dotnet/android#9862 (comment) Context: dotnet/android#10004 Context: xamarin/monodroid@326509e Context: xamarin/monodroid@940136e Ever get the feeling that everything is inextricably related? JNI has two pattens for create an instance of a Java type: 1. [`JNIEnv::NewObject(jclass clazz, jmethodID methodID, const jvalue* args)`][0] 2. [`JNIEnv::AllocObject(jclass clazz)`][1] + [`JNIEnv::CallNonvirtualVoidMethod(jobject obj, jclass clazz, jmethodID methodID, const jvalue* args)`][2] In both patterns: * `clazz` is the `java.lang.Class` of the type to create. * `methodID` is the constructor to execute * `args` are the constructor arguments. In .NET terms: * `JNIEnv::NewObject()` is equivalent to using `System.Reflection.ConstructorInfo.Invoke(object?[]?)`, while * `JNIEnv::AllocObject()` + `JNIEnv::CallNonvirtualVoidMethod()` is equivalent to using `System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(Type)` + `System.Reflection.MethodBase.Invoke(object?, object?[]?)`. Why prefer one over the other? When hand-writing your JNI code, `JNIEnv::NewObject()` is easier. This is less of a concern when a code generator is used. The *real* reason to *avoid* `JNIEnv::NewObject()` whenever possible is the [Java Activation][3] scenario, summarized as the "are you sure you want to do this?" [^1] scenario of invoking a virtual method from the constructor: class Base { public Base() { VirtualMethod(); } public virtual void VirtualMethod() {} } class Derived : Base { public override void VirtualMethod() {} } Java and C# are identical here: when a constructor invokes a virtual method, the most derived method implementation is used, which will occur *before* the constructor of the derived type has *started* execution. (With lots of quibbling about field initializers…) Thus, assume you have a Java `CallVirtualFromConstructorBase` type, which has its constructor Do The Wrong Thing™ and invoke a virtual method from the constructor, and that method is overridden in C#? // Java public class CallVirtualFromConstructorBase { public CallVirtualFromConstructorBase(int value) { calledFromConstructor(value); } public void calledFromConstructor(int value) { } } // C# public class CallVirtualFromConstructorDerived : CallVirtualFromConstructorBase { public CallVirtualFromConstructorDerived(int value) : base (value) { } public override void CalledFromConstructor(int value) { } } What happens with: var p = new CallVirtualFromConstructorDerived(42); The answer depends on whether or not `JNIEnv::NewObject()` is used. If `JNIEnv::NewObject()` is *not* used (the default!) 1. `CallVirtualFromConstructorDerived(int)` constructor begins execution, immediately calls `base(value)`. 2. `CallVirtualFromConstructorBase(int)` constructor runs, uses `JNIEnv::AllocObject()` to *create* (but not construct!) Java `CallVirtualFromConstructorDerived` instance. 3. `JavaObject.Construct(ref JniObjectReference, JniObjectReferenceOptions)` invoked, creating a mapping between the C# instance created in (1) and the Java instance created in (2). 4. `CallVirtualFromConstructorBase(int)` C# constructor calls `JniPeerMembers.InstanceMethods.FinishGenericCreateInstance()`, which eventually invokes `JNIEnv::CallNonvirtualVoidMethod()` with the Java `CallVirtualFromConstructorDerived(int)` ctor. 5. Java `CallVirtualFromConstructorDerived(int)` constructor invokes Java `CallVirtualFromConstructorBase(int)` constructor, which invokes `CallVirtualFromConstructorDerived.calledFromConstructor()`. 6. Marshal method (356485e) for `CallVirtualFromConstructorBase.CalledFromConstructor()` invoked, *immediately* calls `JniRuntime.JniValueManager.GetPeer()` (e288589) to obtain an instance upon which to invoke `.CalledFromConstructor()`, finds the instance mapping from (3), invokes `CallVirtualFromConstructorDerived.CalledFromConstructor()` override. 7. Marshal Method for `CalledFromConstructor()` returns, Java `CallVirtualFromConstructorBase(int)` constructor finishes, Java `CallVirtualFromConstructorDerived(int)` constructor finishes, `JNIEnv::CallNonvirtualVoidMethod()` finishes. 8. `CallVirtualFromConstructorDerived` instance finishes construction. If `JNIEnv::NewObject()` is used: 1. `CallVirtualFromConstructorDerived(int)` constructor begins execution, immediately calls `base(value)`. Note that this is the first created `CallVirtualFromConstructorDerived` instance, but it hasn't been registered yet. 2. `CallVirtualFromConstructorBase(int)` constructor runs, uses `JNIEnv::NewObject()` to construct Java `CallVirtualFromConstructorDerived` instance. 3. `JNIEnv::NewObject()` invokes Java `CallVirtualFromConstructorDerived(int)` constructor, which invokes `CallVirtualFromConstructorBase(int)` constructor, which invokes `CallVirtualFromConstructorDerived.calledFromConstructor()`. 4. Marshal method (356485e) for `CallVirtualFromConstructorBase.CalledFromConstructor()` invoked, *immediately* calls `JniRuntime.JniValueManager.GetPeer()` (e288589) to obtain an instance upon which to invoke `.CalledFromConstructor()`. Here is where things go "off the rails" compared to the `JNIEnv::AllocObject()` code path: There is no such instance -- we're still in the middle of constructing it! -- so we look for an "activation constructor". 5. `CallVirtualFromConstructorDerived(ref JniObjectReference, JniObjectReferenceOptions)` activation constructor executed. This is the *second* `CallVirtualFromConstructorDerived` instance created, and registers a mapping from the Java instance that we started constructing in (3) to what we'll call the "activation intermediary". The activation intermediary instance is marked as "Replaceable". 6. `CallVirtualFromConstructorDerived.CalledFromConstructor()` method override invoked on the activation intermediary. 7. Marshal Method for `CalledFromConstructor()` returns, Java `CallVirtualFromConstructorBase(int)` constructor finishes, Java `CallVirtualFromConstructorDerived(int)` constructor finishes, `JNIEnv::NewObject()` returns instance. 8. C# `CallVirtualFromConstructorBase(int)` constructor calls `JavaObject.Construct(ref JniObjectReference, JniObjectReferenceOptions)`, to create a mapping between (3) and (1). In .NET for Android, this causes the C# instance created in (1) to *replace* the C# instance created in (5), which allows "Replaceable" instance to be replaced. In dotnet/java-interop, this replacement *didn't* happen, which meant that `ValueManager.PeekPeer(p.PeerReference)` would return the activation intermediary, *not* `p`, which confuses everyone. 9. `CallVirtualFromConstructorDerived` instance finishes construction. For awhile, dotnet/java-interop did not fully support this scenario around `JNIEnv::NewObject()`. Additionally, support for using `JNIEnv::NewObject()` as part of `JniPeerMembers.JniInstanceMethods.StartCreateInstance()` was *removed* in dec35f5. Which brings us to dotnet/android#9862: where there is an observed "race condition" around `Android.App.Application` subclass creation. *Two* instances of `AndroidApp` were created, one from the "normal" app startup: at crc647fae2f69c19dcd0d.AndroidApp.n_onCreate(Native Method) at crc647fae2f69c19dcd0d.AndroidApp.onCreate(AndroidApp.java:25) at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1316) and another from an `androidx.work.WorkerFactory`: at mono.android.TypeManager.n_activate(Native Method) at mono.android.TypeManager.Activate(TypeManager.java:7) at crc647fae2f69c19dcd0d.SyncWorker.<init>(SyncWorker.java:23) at java.lang.reflect.Constructor.newInstance0(Native Method) at java.lang.reflect.Constructor.newInstance(Constructor.java:343) at androidx.work.WorkerFactory.createWorkerWithDefaultFallback(WorkerFactory.java:95) However, what was odd about this "race condition" was that the *second* instance created would reliably win! Further investigation suggested that this was less of a "race condition" and more a bug in `AndroidValueManager`, wherein when "Replaceable" instances were created, an existing instance would *always* be replaced, even if the new instance was also Replaceable! This feels bananas; yes, Replaceable should be replaceable, but the expectation was that it would be replaced by *non*-Replaceable instances, not just any instance that came along later. Update `JniRuntimeJniValueManagerContract` to add a new `CreatePeer_ReplaceableDoesNotReplace()` test to codify the desired semantic that Replaceable instances do not replace Replaceable instances. Surprisingly, this new test did not fail on java-interop, as `ManagedValueManager.AddPeer()` bails early when `PeekPeer()` finds a value, while `AndroidValueManager.AddPeer()` does not bail early. An obvious fix for `CreatePeer_ReplaceableDoesNotReplace()` within dotnet/android would be to adopt the "`AddPeer()` calls `PeekPeer()`" logic from java-interop. The problem is that doing so breaks [`ObjectTest.JnienvCreateInstance_RegistersMultipleInstances()`][5], as seen in dotnet/android#10004! `JnienvCreateInstance_RegistersMultipleInstances()` in turn fails when `PeekPeer()` is used because follows the `JNIEnv::NewObject()` [construction codepath][6]! public CreateInstance_OverrideAbsListView_Adapter (Context context) : base ( JNIEnv.CreateInstance ( JcwType, "(Landroid/content/Context;)V", new JValue (context)), JniHandleOwnership.TransferLocalRef) { AdapterValue = new ArrayAdapter (context, 0); } as `JNIEnv.CreateInstance()` uses `JNIEnv.NewObject()`. We thus have a conundrum: how do we fix *both* `CreatePeer_ReplaceableDoesNotReplace()` *and* `JnienvCreateInstance_RegistersMultipleInstances()`? The answer is to add proper support for the `JNIEnv::NewObject()` construction scenario to dotnet/java-interop, which in turn requires "lowering" the setting of `.Replaceable`. Previously, we would set `.Replaceable` *after* the activation constructor was invoked: // dotnet/android TypeManager.CreateInstance(), paraphrasing var result = CreateProxy (type, handle, transfer); result.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); return result; This is *too late*, as during execution of the activation constructor, the instance thinks it *isn't* replaceable, and thus creation of a new instance via the activation constructor will replace an already existing replaceable instance; it's not until *after* the constructor finished executing that we'd set `.Replaceable`. To fix this, update `JniRuntime.JniValueManager.TryCreatePeerInstance()` to first create an *uninitialized* instance, set `.Replaceable`, and *then* invoke the activation constructor. This allows `JniRuntime.JniValueManager.AddPeer()` to check to see if the new value is also replaceable, and ignore the replacement if appropriate. This in turn requires replacing: partial class /* JniRuntime. */ JniValueManager { protected virtual IJavaPeerable? TryCreatePeer () ref JniObjectReference reference, JniObjectReferenceOptions options, Type type); } with: partial class /* JniRuntime. */ JniValueManager { protected virtual bool TryConstructPeer () IJavaPeerable self, ref JniObjectReference reference, JniObjectReferenceOptions options, Type type); } This is fine because we haven't shipped `TryCreatePeer()` in a stable release yet. [^1]: See also [Framework Design Guidelines > Constructor Design][4]: > ❌ AVOID calling virtual members on an object inside its constructor. > Calling a virtual member will cause the most derived override to be > called, even if the constructor of the most derived type has not > been fully run yet. [0]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#NewObject [1]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#AllocObject [2]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#CallNonvirtual_type_Method_routines [3]: https://learn.microsoft.com/en-us/previous-versions/xamarin/android/internals/architecture#java-activation [4]: https://learn.microsoft.com/dotnet/standard/design-guidelines/constructor [5]: https://github.com/dotnet/android/blob/9ad492a42b384519a8b1f1987adae82335536d9c/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Lang/ObjectTest.cs#L68-L79 [6]: https://github.com/dotnet/android/blob/9ad492a42b384519a8b1f1987adae82335536d9c/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Lang/ObjectTest.cs#L151-L160
1 parent 8221b7d commit d3d3a1b

File tree

7 files changed

+270
-30
lines changed

7 files changed

+270
-30
lines changed

src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs

+26-5
Original file line numberDiff line numberDiff line change
@@ -401,15 +401,36 @@ static Type GetPeerType ([DynamicallyAccessedMembers (Constructors)] Type type)
401401
Type type)
402402
{
403403
type = Runtime.TypeManager.GetInvokerType (type) ?? type;
404-
return TryCreatePeer (ref reference, options, type);
404+
405+
var self = GetUninitializedObject (type);
406+
var constructed = false;
407+
try {
408+
constructed = TryConstructPeer (self, ref reference, options, type);
409+
} finally {
410+
if (!constructed) {
411+
GC.SuppressFinalize (self);
412+
self = null;
413+
}
414+
}
415+
return self;
416+
417+
static IJavaPeerable GetUninitializedObject (
418+
[DynamicallyAccessedMembers (Constructors)]
419+
Type type)
420+
{
421+
var v = (IJavaPeerable) System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject (type);
422+
v.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable);
423+
return v;
424+
}
405425
}
406426

407427
const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
408428
static readonly Type ByRefJniObjectReference = typeof (JniObjectReference).MakeByRefType ();
409429
static readonly Type[] JIConstructorSignature = new Type [] { ByRefJniObjectReference, typeof (JniObjectReferenceOptions) };
410430

411431

412-
protected virtual IJavaPeerable? TryCreatePeer (
432+
protected virtual bool TryConstructPeer (
433+
IJavaPeerable self,
413434
ref JniObjectReference reference,
414435
JniObjectReferenceOptions options,
415436
[DynamicallyAccessedMembers (Constructors)]
@@ -421,11 +442,11 @@ static Type GetPeerType ([DynamicallyAccessedMembers (Constructors)] Type type)
421442
reference,
422443
options,
423444
};
424-
var p = (IJavaPeerable) c.Invoke (args);
445+
c.Invoke (self, args);
425446
reference = (JniObjectReference) args [0];
426-
return p;
447+
return true;
427448
}
428-
return null;
449+
return false;
429450
}
430451

431452
public object? CreateValue (

src/Java.Interop/PublicAPI.Unshipped.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ static Java.Interop.JniEnvironment.EndMarshalMethod(ref Java.Interop.JniTransiti
44
virtual Java.Interop.JniRuntime.OnEnterMarshalMethod() -> void
55
virtual Java.Interop.JniRuntime.OnUserUnhandledException(ref Java.Interop.JniTransition transition, System.Exception! e) -> void
66
virtual Java.Interop.JniRuntime.JniTypeManager.GetInvokerTypeCore(System.Type! type) -> System.Type?
7-
virtual Java.Interop.JniRuntime.JniValueManager.TryCreatePeer(ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions options, System.Type! type) -> Java.Interop.IJavaPeerable?
7+
virtual Java.Interop.JniRuntime.JniValueManager.TryConstructPeer(Java.Interop.IJavaPeerable! self, ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions options, System.Type! type) -> bool
88
Java.Interop.JavaException.JavaException(ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions transfer, Java.Interop.JniObjectReference throwableOverride) -> void
99
Java.Interop.JavaException.SetJavaStackTrace(Java.Interop.JniObjectReference peerReferenceOverride = default(Java.Interop.JniObjectReference)) -> void
1010
Java.Interop.JniRuntime.JniTypeManager.GetInvokerType(System.Type! type) -> System.Type?

src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs

+4-6
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,6 @@ public override void AddPeer (IJavaPeerable value)
5757
var r = value.PeerReference;
5858
if (!r.IsValid)
5959
throw new ObjectDisposedException (value.GetType ().FullName);
60-
var o = PeekPeer (value.PeerReference);
61-
if (o != null)
62-
return;
6360

6461
if (r.Type != JniObjectReferenceType.Global) {
6562
value.SetPeerReference (r.NewGlobalRef ());
@@ -80,7 +77,7 @@ public override void AddPeer (IJavaPeerable value)
8077
var p = peers [i];
8178
if (!JniEnvironment.Types.IsSameObject (p.PeerReference, value.PeerReference))
8279
continue;
83-
if (Replaceable (p)) {
80+
if (Replaceable (p, value)) {
8481
peers [i] = value;
8582
} else {
8683
WarnNotReplacing (key, value, p);
@@ -91,11 +88,12 @@ public override void AddPeer (IJavaPeerable value)
9188
}
9289
}
9390

94-
static bool Replaceable (IJavaPeerable peer)
91+
static bool Replaceable (IJavaPeerable peer, IJavaPeerable value)
9592
{
9693
if (peer == null)
9794
return true;
98-
return (peer.JniManagedPeerState & JniManagedPeerStates.Replaceable) == JniManagedPeerStates.Replaceable;
95+
return peer.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable) &&
96+
!value.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable);
9997
}
10098

10199
void WarnNotReplacing (int key, IJavaPeerable ignoreValue, IJavaPeerable keepValue)

tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorBase.cs

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Runtime.CompilerServices;
23

34
using Java.Interop;
45
using Java.Interop.GenericMarshaler;
@@ -16,14 +17,31 @@ public override JniPeerMembers JniPeerMembers {
1617
}
1718

1819
public unsafe CallVirtualFromConstructorBase (int value)
20+
: this (value, useNewObject: false)
21+
{
22+
}
23+
24+
public unsafe CallVirtualFromConstructorBase (int value, bool useNewObject)
1925
: this (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None)
2026
{
2127
if (PeerReference.IsValid)
2228
return;
2329

24-
var peer = JniPeerMembers.InstanceMethods.StartGenericCreateInstance ("(I)V", GetType (), value);
30+
const string __id = "(I)V";
31+
32+
if (useNewObject) {
33+
var ctors = JniPeerMembers.InstanceMethods.GetConstructorsForType (GetType ());
34+
var init = ctors.GetConstructor (__id);
35+
36+
JniArgumentValue* __args = stackalloc JniArgumentValue [1];
37+
__args [0] = new JniArgumentValue (value);
38+
var lref = JniEnvironment.Object.NewObject (ctors.JniPeerType.PeerReference, init, __args);
39+
Construct (ref lref, JniObjectReferenceOptions.CopyAndDispose);
40+
return;
41+
}
42+
var peer = JniPeerMembers.InstanceMethods.StartGenericCreateInstance (__id, GetType (), value);
2543
Construct (ref peer, JniObjectReferenceOptions.CopyAndDispose);
26-
JniPeerMembers.InstanceMethods.FinishGenericCreateInstance ("(I)V", this, value);
44+
JniPeerMembers.InstanceMethods.FinishGenericCreateInstance (__id, this, value);
2745
}
2846

2947
public CallVirtualFromConstructorBase (ref JniObjectReference reference, JniObjectReferenceOptions options)

tests/Java.Interop-Tests/Java.Interop/CallVirtualFromConstructorDerived.cs

+27-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
using System;
1+
#nullable enable
2+
3+
using System;
4+
using System.Runtime.CompilerServices;
25

36
using Java.Interop;
47

@@ -24,21 +27,38 @@ public override JniPeerMembers JniPeerMembers {
2427
public bool InvokedConstructor;
2528

2629
public CallVirtualFromConstructorDerived (int value)
27-
: base (value)
30+
: this (value, useNewObject: false)
31+
{
32+
}
33+
34+
public CallVirtualFromConstructorDerived (int value, bool useNewObject)
35+
: base (value, useNewObject)
2836
{
2937
InvokedConstructor = true;
30-
if (value != calledValue)
38+
39+
if (useNewObject && calledValue != 0) {
40+
// calledValue was set on a *different* instance! So it's 0 here.
41+
throw new ArgumentException (
42+
string.Format ("value '{0}' doesn't match expected value '{1}'.", value, 0),
43+
"value");
44+
}
45+
if (!useNewObject && value != calledValue)
3146
throw new ArgumentException (
3247
string.Format ("value '{0}' doesn't match expected value '{1}'.", value, calledValue),
3348
"value");
3449
}
3550

3651
public bool InvokedActivationConstructor;
3752

53+
public static CallVirtualFromConstructorDerived? Intermediate_FromCalledFromConstructor;
54+
public static CallVirtualFromConstructorDerived? Intermediate_FromActivationConstructor;
55+
3856
public CallVirtualFromConstructorDerived (ref JniObjectReference reference, JniObjectReferenceOptions options)
3957
: base (ref reference, options)
4058
{
4159
InvokedActivationConstructor = true;
60+
61+
Intermediate_FromActivationConstructor = this;
4262
}
4363

4464
public bool Called;
@@ -47,14 +67,16 @@ public override void CalledFromConstructor (int value)
4767
{
4868
Called = true;
4969
calledValue = value;
70+
71+
Intermediate_FromCalledFromConstructor = this;
5072
}
5173

5274
public static unsafe CallVirtualFromConstructorDerived NewInstance (int value)
5375
{
5476
JniArgumentValue* args = stackalloc JniArgumentValue [1];
5577
args [0] = new JniArgumentValue (value);
5678
var o = _members.StaticMethods.InvokeObjectMethod ("newInstance.(I)Lnet/dot/jni/test/CallVirtualFromConstructorDerived;", args);
57-
return JniEnvironment.Runtime.ValueManager.GetValue<CallVirtualFromConstructorDerived> (ref o, JniObjectReferenceOptions.CopyAndDispose);
79+
return JniEnvironment.Runtime.ValueManager.GetValue<CallVirtualFromConstructorDerived> (ref o, JniObjectReferenceOptions.CopyAndDispose)!;
5880
}
5981

6082
delegate void CalledFromConstructorMarshalMethod (IntPtr jnienv, IntPtr n_self, int value);
@@ -63,7 +85,7 @@ static void CalledFromConstructorHandler (IntPtr jnienv, IntPtr n_self, int valu
6385
var envp = new JniTransition (jnienv);
6486
try {
6587
var r_self = new JniObjectReference (n_self);
66-
var self = JniEnvironment.Runtime.ValueManager.GetValue<CallVirtualFromConstructorDerived>(ref r_self, JniObjectReferenceOptions.Copy);
88+
var self = JniEnvironment.Runtime.ValueManager.GetValue<CallVirtualFromConstructorDerived>(ref r_self, JniObjectReferenceOptions.Copy)!;
6789
self.CalledFromConstructor (value);
6890
self.DisposeUnlessReferenced ();
6991
}

tests/Java.Interop-Tests/Java.Interop/InvokeVirtualFromConstructorTests.cs

+132-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Runtime.CompilerServices;
23

34
using Java.Interop;
45

@@ -7,25 +8,145 @@
78
namespace Java.InteropTests {
89

910
[TestFixture]
11+
#if !__ANDROID__
12+
// We want stability around the CallVirtualFromConstructorDerived static fields
13+
[NonParallelizable]
14+
#endif // !__ANDROID__
1015
public class InvokeVirtualFromConstructorTests : JavaVMFixture
1116
{
1217
[Test]
13-
public void InvokeVirtualFromConstructor ()
18+
public void CreateManagedInstanceFirst_WithAllocObject ()
1419
{
15-
using (var t = new CallVirtualFromConstructorDerived (42)) {
16-
Assert.IsTrue (t.Called);
17-
Assert.IsNotNull (JniRuntime.CurrentRuntime.ValueManager.PeekValue (t.PeerReference));
18-
}
20+
CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor = null;
21+
CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor = null;
22+
23+
using var t = new CallVirtualFromConstructorDerived (42);
24+
Assert.IsTrue (
25+
t.Called,
26+
"CalledFromConstructor method override should have been called.");
27+
Assert.IsFalse (
28+
t.InvokedActivationConstructor,
29+
"Activation Constructor should have been called, as calledFromConstructor() is invoked before ManagedPeer.construct().");
30+
Assert.IsTrue (
31+
t.InvokedConstructor,
32+
"(int) constructor should have been called, via ManagedPeer.construct().");
33+
34+
var registered = JniRuntime.CurrentRuntime.ValueManager.PeekValue (t.PeerReference);
35+
var acIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor;
36+
var cfIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor;
37+
38+
Assert.AreSame (t, registered,
39+
"Expected t and registered to be the same instance; " +
40+
$"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " +
41+
$"registered={RuntimeHelpers.GetHashCode (registered).ToString ("x")}");
42+
Assert.IsNull (acIntermediate,
43+
"Activation Constructor should not have been called, because of AllocObject semantics");
44+
Assert.AreSame (t, cfIntermediate,
45+
"Expected t and cfIntermediate to be the same instance; " +
46+
$"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " +
47+
$"cfIntermediate={RuntimeHelpers.GetHashCode (cfIntermediate).ToString ("x")}");
48+
49+
Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor);
50+
Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor);
51+
}
52+
53+
static void Dispose<T> (ref T peer)
54+
where T : class, IJavaPeerable
55+
{
56+
if (peer == null)
57+
return;
58+
59+
peer.Dispose ();
60+
peer = null;
1961
}
2062

2163
[Test]
22-
public void ActivationConstructor ()
64+
public void CreateManagedInstanceFirst_WithNewObject ()
2365
{
24-
var t = CallVirtualFromConstructorDerived.NewInstance (42);
25-
using (t) {
26-
Assert.IsTrue (t.InvokedActivationConstructor);
27-
Assert.IsTrue (t.InvokedConstructor);
28-
}
66+
CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor = null;
67+
CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor = null;
68+
69+
using var t = new CallVirtualFromConstructorDerived (42, useNewObject: true);
70+
Assert.IsFalse (
71+
t.Called,
72+
"CalledFromConstructor method override was called on a different instance.");
73+
Assert.IsFalse (
74+
t.InvokedActivationConstructor,
75+
"Activation Constructor should not have been called, as calledFromConstructor() is invoked before ManagedPeer.construct().");
76+
Assert.IsTrue (
77+
t.InvokedConstructor,
78+
"(int) constructor should have been called, via ManagedPeer.construct().");
79+
80+
var registered = JniRuntime.CurrentRuntime.ValueManager.PeekValue (t.PeerReference);
81+
var acIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor;
82+
var cfIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor;
83+
84+
Assert.AreSame (t, registered,
85+
"Expected t and registered to be the same instance; " +
86+
$"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " +
87+
$"registered={RuntimeHelpers.GetHashCode (registered).ToString ("x")}");
88+
Assert.IsNotNull (acIntermediate,
89+
"Activation Constructor should have been called, because of NewObject");
90+
Assert.IsTrue (
91+
acIntermediate.Called,
92+
"CalledFromConstructor method override should have been called on acIntermediate.");
93+
Assert.IsTrue (
94+
acIntermediate.InvokedActivationConstructor,
95+
"Activation Constructor should have been called on intermediate instance, as calledFromConstructor() is invoked before ManagedPeer.construct().");
96+
Assert.AreNotSame (t, acIntermediate,
97+
"Expected t and registered to be different instances; " +
98+
$"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " +
99+
$"acIntermediate={RuntimeHelpers.GetHashCode (acIntermediate).ToString ("x")}");
100+
Assert.AreNotSame (t, cfIntermediate,
101+
"Expected t and cfIntermediate to be different instances; " +
102+
$"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " +
103+
$"cfIntermediate={RuntimeHelpers.GetHashCode (cfIntermediate).ToString ("x")}");
104+
Assert.AreSame (acIntermediate, cfIntermediate,
105+
"Expected acIntermediate and cfIntermediate to be the same instance; " +
106+
$"acIntermediate={RuntimeHelpers.GetHashCode (acIntermediate).ToString ("x")}, " +
107+
$"cfIntermediate={RuntimeHelpers.GetHashCode (cfIntermediate).ToString ("x")}");
108+
109+
Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor);
110+
Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor);
111+
}
112+
113+
[Test]
114+
public void CreateJavaInstanceFirst ()
115+
{
116+
CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor = null;
117+
CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor = null;
118+
119+
using var t = CallVirtualFromConstructorDerived.NewInstance (42);
120+
121+
Assert.IsTrue (
122+
t.Called,
123+
"CalledFromConstructor method override should have been called.");
124+
Assert.IsTrue (
125+
t.InvokedActivationConstructor,
126+
"Activation Constructor should have been called, as calledFromConstructor() is invoked before ManagedPeer.construct().");
127+
Assert.IsTrue (
128+
t.InvokedConstructor,
129+
"(int) constructor should have been called, via ManagedPeer.construct().");
130+
131+
var registered = JniRuntime.CurrentRuntime.ValueManager.PeekValue (t.PeerReference);
132+
var acIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor;
133+
var cfIntermediate = CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor;
134+
135+
Assert.AreSame (t, registered,
136+
"Expected t and registered to be the same instance; " +
137+
$"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " +
138+
$"registered={RuntimeHelpers.GetHashCode (registered).ToString ("x")}");
139+
Assert.AreSame (t, acIntermediate,
140+
"Expected t and registered to be the same instance; " +
141+
$"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " +
142+
$"acIntermediate={RuntimeHelpers.GetHashCode (acIntermediate).ToString ("x")}");
143+
Assert.AreSame (t, cfIntermediate,
144+
"Expected t and cfIntermediate to be the same instance; " +
145+
$"t={RuntimeHelpers.GetHashCode (t).ToString ("x")}, " +
146+
$"cfIntermediate={RuntimeHelpers.GetHashCode (cfIntermediate).ToString ("x")}");
147+
148+
Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromActivationConstructor);
149+
Dispose (ref CallVirtualFromConstructorDerived.Intermediate_FromCalledFromConstructor);
29150
}
30151
}
31152
}

0 commit comments

Comments
 (0)