-
Notifications
You must be signed in to change notification settings - Fork 56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Java.Interop] JNIEnv::NewObject and Replaceable instances #1323
base: main
Are you sure you want to change the base?
Conversation
Context: 3043d89 Context: dotnet/android#9862 Context: dotnet/android#9862 (comment) In dotnet/android#9862, 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 `AndroidRuntime`, wherein when "Replaceable" instances were created, an existing instance would *always* be replaced. Aside: JniManagedPeerStates.Replaceable is from 3043d89: > `JniManagedPeerStates.Replaceable` … means > that the Peer instance was created through the activation constructor. > It additionally means that if two managed instances are created around > the same Java instance, the non-Replaceable instance will be the one > returned by JniRuntime.JniValueManager.PeekObject(). What we're observing in dotnet/android#9862 is that while the Replaceable instance is replaced, it's being replaced by *another* Replaceable instance! This feels bananas; yes, Replaceable should be replacable, but only by *non*-Replaceable instances. Update `JniRuntimeJniValueManagerContract` to add a new `CreatePeer_ReplaceableDoesNotReplace()` test to codify the desired semantic that Replaceable instances do not replace Replaceable instances. Surprisingly, this does not fail on java-interop! Apparently `ManagedValueManager.AddPeer()` bails early when `PeekPeer()` finds a value, while `AndroidRuntime.AddPeer()` does not bail early.
Context: dotnet/java-interop#1323 Context: #9862 (comment) Does It Build™? (The expectation is that it *does* build -- only unit tests are changed in dotnet/java-interop#1323 -- but that the new `JniRuntimeJniValueManagerContract.cs.CreatePeer_ReplaceableDoesNotReplace()` test will fail.)`
Write access |
|
…replacable-semantics
Context: dotnet/java-interop#1323 Context: #9862 (comment) Does It Build™? (The expectation is that it *does* build -- only unit tests are changed in dotnet/java-interop#1323 -- but that the new `JniRuntimeJniValueManagerContract.cs.CreatePeer_ReplaceableDoesNotReplace()` test will fail.)`
Context: dotnet/android#10004 It looks like dotnet/android#10004 is closely tied to dotnet/android#9862. Might as well update tests to hit this behavior!
TODO: proper explanation. See also: * 3043d89 * xamarin/monodroid@326509e * xamarin/monodroid@940136e * dotnet/android#10004
Turns Out™ that this difference is a cause of dotnet/android#10004, causing Further investigation shows that |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.
[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:
JNIEnv::NewObject(jclass clazz, jmethodID methodID, const jvalue* args)
JNIEnv::AllocObject(jclass clazz)
+JNIEnv::CallNonvirtualVoidMethod(jobject obj, jclass clazz, jmethodID methodID, const jvalue* args)
In both patterns:
clazz
is thejava.lang.Class
of the type to create.methodID
is the constructor to executeargs
are the constructor arguments.In .NET terms:
JNIEnv::NewObject()
is equivalent to usingSystem.Reflection.ConstructorInfo.Invoke(object?[]?)
, whileJNIEnv::AllocObject()
+JNIEnv::CallNonvirtualVoidMethod()
isequivalent 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 possibleis the Java Activation scenario, summarized as the "are you sure
you want to do this?" 1 scenario of invoking a virtual method from the
constructor:
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#?
What happens with:
The answer depends on whether or not
JNIEnv::NewObject()
is used.If
JNIEnv::NewObject()
is not used (the default!)CallVirtualFromConstructorDerived(int)
constructor beginsexecution, immediately calls
base(value)
.CallVirtualFromConstructorBase(int)
constructor runs, usesJNIEnv::AllocObject()
to create (but not construct!) JavaCallVirtualFromConstructorDerived
instance.JavaObject.Construct(ref JniObjectReference, JniObjectReferenceOptions)
invoked, creating a mapping between the C# instance created in
(1) and the Java instance created in (2).
CallVirtualFromConstructorBase(int)
C# constructor callsJniPeerMembers.InstanceMethods.FinishGenericCreateInstance()
,which eventually invokes
JNIEnv::CallNonvirtualVoidMethod()
with the Java
CallVirtualFromConstructorDerived(int)
ctor.Java
CallVirtualFromConstructorDerived(int)
constructor invokesJava
CallVirtualFromConstructorBase(int)
constructor, whichinvokes
CallVirtualFromConstructorDerived.calledFromConstructor()
.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.
Marshal Method for
CalledFromConstructor()
returns, JavaCallVirtualFromConstructorBase(int)
constructor finishes,Java
CallVirtualFromConstructorDerived(int)
constructorfinishes,
JNIEnv::CallNonvirtualVoidMethod()
finishes.CallVirtualFromConstructorDerived
instance finishes construction.If
JNIEnv::NewObject()
is used:CallVirtualFromConstructorDerived(int)
constructor beginsexecution, immediately calls
base(value)
.Note that this is the first created
CallVirtualFromConstructorDerived
instance, but it hasn't been registered yet.
CallVirtualFromConstructorBase(int)
constructor runs, usesJNIEnv::NewObject()
to construct JavaCallVirtualFromConstructorDerived
instance.JNIEnv::NewObject()
invokes JavaCallVirtualFromConstructorDerived(int)
constructor, which invokesCallVirtualFromConstructorBase(int)
constructor, which invokesCallVirtualFromConstructorDerived.calledFromConstructor()
.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".
CallVirtualFromConstructorDerived(ref JniObjectReference, JniObjectReferenceOptions)
activation constructor executed.
This is the second
CallVirtualFromConstructorDerived
instancecreated, 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".
CallVirtualFromConstructorDerived.CalledFromConstructor()
methodoverride invoked on the activation intermediary.
Marshal Method for
CalledFromConstructor()
returns, JavaCallVirtualFromConstructorBase(int)
constructor finishes,Java
CallVirtualFromConstructorDerived(int)
constructorfinishes,
JNIEnv::NewObject()
returns instance.C#
CallVirtualFromConstructorBase(int)
constructor callsJavaObject.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 returnthe activation intermediary, not
p
, which confuses everyone.CallVirtualFromConstructorDerived
instance finishes construction.For awhile, dotnet/java-interop did not fully support this scenario
around
JNIEnv::NewObject()
. Additionally, support for usingJNIEnv::NewObject()
as part ofJniPeerMembers.JniInstanceMethods.StartCreateInstance()
wasremoved 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:
and another from an
androidx.work.WorkerFactory
: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 newCreatePeer_ReplaceableDoesNotReplace()
test to codify the desiredsemantic that Replaceable instances do not replace Replaceable
instances.
Surprisingly, this new test did not fail on java-interop, as
ManagedValueManager.AddPeer()
bails early whenPeekPeer()
findsa value, while
AndroidValueManager.AddPeer()
does not bail early.An obvious fix for
CreatePeer_ReplaceableDoesNotReplace()
withindotnet/android would be to adopt the "
AddPeer()
callsPeekPeer()
"logic from java-interop. The problem is that doing so breaks
ObjectTest.JnienvCreateInstance_RegistersMultipleInstances()
,as seen in dotnet/android#10004!
JnienvCreateInstance_RegistersMultipleInstances()
in turn failswhen
PeekPeer()
is used because follows theJNIEnv::NewObject()
construction codepath!
as
JNIEnv.CreateInstance()
usesJNIEnv.NewObject()
.We thus have a conundrum: how do we fix both
CreatePeer_ReplaceableDoesNotReplace()
andJnienvCreateInstance_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: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
, andthen invoke the activation constructor. This allows
JniRuntime.JniValueManager.AddPeer()
to check to see if the newvalue is also replaceable, and ignore the replacement if appropriate.
This in turn requires replacing:
with:
This is fine because we haven't shipped
TryCreatePeer()
in a stablerelease yet.
Footnotes
See also Framework Design Guidelines > Constructor Design:
↩