Commit d3d3a1b
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: https://github.com/xamarin/monodroid/commit/326509e56d4e582c53bbe5dfe6d5c741a27f1af5
Context: https://github.com/xamarin/monodroid/commit/940136ebf1318a7c57a855e2728ce2703c0240af
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-L1601 parent 8221b7d commit d3d3a1b
File tree
7 files changed
+270
-30
lines changed- src
- Java.Interop
- Java.Interop
- Java.Runtime.Environment/Java.Interop
- tests/Java.Interop-Tests/Java.Interop
7 files changed
+270
-30
lines changedLines changed: 26 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
401 | 401 | | |
402 | 402 | | |
403 | 403 | | |
404 | | - | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
405 | 425 | | |
406 | 426 | | |
407 | 427 | | |
408 | 428 | | |
409 | 429 | | |
410 | 430 | | |
411 | 431 | | |
412 | | - | |
| 432 | + | |
| 433 | + | |
413 | 434 | | |
414 | 435 | | |
415 | 436 | | |
| |||
421 | 442 | | |
422 | 443 | | |
423 | 444 | | |
424 | | - | |
| 445 | + | |
425 | 446 | | |
426 | | - | |
| 447 | + | |
427 | 448 | | |
428 | | - | |
| 449 | + | |
429 | 450 | | |
430 | 451 | | |
431 | 452 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
| 7 | + | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| |||
Lines changed: 4 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
60 | | - | |
61 | | - | |
62 | | - | |
63 | 60 | | |
64 | 61 | | |
65 | 62 | | |
| |||
80 | 77 | | |
81 | 78 | | |
82 | 79 | | |
83 | | - | |
| 80 | + | |
84 | 81 | | |
85 | 82 | | |
86 | 83 | | |
| |||
91 | 88 | | |
92 | 89 | | |
93 | 90 | | |
94 | | - | |
| 91 | + | |
95 | 92 | | |
96 | 93 | | |
97 | 94 | | |
98 | | - | |
| 95 | + | |
| 96 | + | |
99 | 97 | | |
100 | 98 | | |
101 | 99 | | |
| |||
Lines changed: 20 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
| 2 | + | |
2 | 3 | | |
3 | 4 | | |
4 | 5 | | |
| |||
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
19 | 25 | | |
20 | 26 | | |
21 | 27 | | |
22 | 28 | | |
23 | 29 | | |
24 | | - | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
25 | 43 | | |
26 | | - | |
| 44 | + | |
27 | 45 | | |
28 | 46 | | |
29 | 47 | | |
| |||
Lines changed: 27 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
2 | 5 | | |
3 | 6 | | |
4 | 7 | | |
| |||
24 | 27 | | |
25 | 28 | | |
26 | 29 | | |
27 | | - | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
28 | 36 | | |
29 | 37 | | |
30 | | - | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
31 | 46 | | |
32 | 47 | | |
33 | 48 | | |
34 | 49 | | |
35 | 50 | | |
36 | 51 | | |
37 | 52 | | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
38 | 56 | | |
39 | 57 | | |
40 | 58 | | |
41 | 59 | | |
| 60 | + | |
| 61 | + | |
42 | 62 | | |
43 | 63 | | |
44 | 64 | | |
| |||
47 | 67 | | |
48 | 68 | | |
49 | 69 | | |
| 70 | + | |
| 71 | + | |
50 | 72 | | |
51 | 73 | | |
52 | 74 | | |
53 | 75 | | |
54 | 76 | | |
55 | 77 | | |
56 | 78 | | |
57 | | - | |
| 79 | + | |
58 | 80 | | |
59 | 81 | | |
60 | 82 | | |
| |||
63 | 85 | | |
64 | 86 | | |
65 | 87 | | |
66 | | - | |
| 88 | + | |
67 | 89 | | |
68 | 90 | | |
69 | 91 | | |
| |||
Lines changed: 132 additions & 11 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
| 2 | + | |
2 | 3 | | |
3 | 4 | | |
4 | 5 | | |
| |||
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
10 | 15 | | |
11 | 16 | | |
12 | 17 | | |
13 | | - | |
| 18 | + | |
14 | 19 | | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
19 | 61 | | |
20 | 62 | | |
21 | 63 | | |
22 | | - | |
| 64 | + | |
23 | 65 | | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
29 | 150 | | |
30 | 151 | | |
31 | 152 | | |
| |||
0 commit comments