-
Notifications
You must be signed in to change notification settings - Fork 56
[Java.Interop] JNIEnv::NewObject and Replaceable instances #1323
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
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.)`
This comment was marked as spam.
This comment was marked as spam.
This comment was marked as spam.
This comment was marked as spam.
…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.
Changes: dotnet/java-interop@8221b7d...d3d3a1b * dotnet/java-interop@d3d3a1bf: [Java.Interop] JNIEnv::NewObject and Replaceable instances (dotnet/java-interop#1323)
Changes: dotnet/java-interop@8221b7d...d3d3a1b * dotnet/java-interop@d3d3a1bf: [Java.Interop] JNIEnv::NewObject and Replaceable instances (dotnet/java-interop#1323)
Changes: dotnet/java-interop@8221b7d...d3d3a1b * dotnet/java-interop@d3d3a1bf: [Java.Interop] JNIEnv::NewObject and Replaceable instances (dotnet/java-interop#1323)
Fixes: #9862 Changes: dotnet/java-interop@8221b7d...d3d3a1b * dotnet/java-interop@d3d3a1bf: [Java.Interop] JNIEnv::NewObject and Replaceable instances (dotnet/java-interop#1323) Context: 5c23bcd Issue #9862 is an observed "race condition" around `Android.App.Application` subclass creation. *Two* instances of `AndroidApp` were created, one from the "normal" app startup: at MailClient.Mobile.Droid.AndroidApp..ctor(IntPtr handle, JniHandleOwnership ownership) … at Java.Lang.Object.GetObject(IntPtr , JniHandleOwnership , Type ) at Java.Lang.Object._GetObject[Application](IntPtr , JniHandleOwnership ) at Java.Lang.Object.GetObject[Application](IntPtr handle, JniHandleOwnership transfer) at Java.Lang.Object.GetObject[Application](IntPtr jnienv, IntPtr handle, JniHandleOwnership transfer) at Android.App.Application.n_OnCreate(IntPtr jnienv, IntPtr native__this) 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`, via parameter: at MailClient.Mobile.Droid.AndroidApp..ctor(IntPtr handle, JniHandleOwnership ownership) … at Java.Lang.Object.GetObject(IntPtr , JniHandleOwnership , Type ) at Android.Runtime.JNIEnv.<>c.<CreateNativeArrayElementToManaged>b__70_9(Type type, IntPtr source, Int32 index) at Android.Runtime.JNIEnv.GetObjectArray(IntPtr , Type[] ) at Java.Interop.TypeManager.n_Activate(IntPtr jnienv, IntPtr jclass, IntPtr typename_ptr, IntPtr signature_ptr, IntPtr jobject, IntPtr parameters_ptr) 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. Aside: a "Replaceable" instance is an instance created via `JniRuntime.JniValueManager.CreatePeer()` / `TypeManager.CreateInstance()`. "Replaceable" instances can be replaced in the `JniRuntime.JniValueManager` by instances created via the constructor, e.g. `new Integer(42)`. JniPeerReference h; using (var x = new Java.Lang.Integer(1)) h = x.PeerReference.NewLocalRef(); // No mapping for `h` in JniValueManager, as x was disposed. var a = JniEnvironment.Runtime.ValueManager.CreatePeer (ref h, JniObjectReferenceOptions.Copy, null); // a is "Replaceable" var b = new Java.Lang.Integer(x.Handle, JniHandleOwnership.DoNotTransfer); // b is *not* Replaceable; created via constructor var p = JniEnvironment.Runtime.ValueManager.PeekPeer(h); // p should be the non-Replaceable value b, *not* a dotnet/java-interop@d3d3a1bf updates `JniRuntimeJniValueManagerContract` to add a new `CreatePeer_ReplaceableDoesNotReplace()` test to codify the desired semantic that Replaceable instances do not replace Replaceable instances. Update `AndroidValueManager` so that the new `CreatePeer_ReplaceableDoesNotReplace()` test passes. Supporting this new semantic required "extending" Replaceable lifetime; it turns out that the existing code within `TypeManager.CreateInstance()`: var result = CreateProxy (type, handle, transfer); if (Runtime.IsGCUserPeer (result.PeerReference.Handle)) { result.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); } return result; sets `.Replaceable` *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 set `.Replaceable`. Address this by updating `CreatePeer()` to split up instance creation: 1. Create an *un-constructed* instance using [`RuntimeHelpers.GetUninitializedObject(Type)`][0]. 2. Set `.Replaceable` on the instance from (1) 3. *Then* invoke the activation constructor. This allows `JniRuntime.JniValueManager.AddPeer()` to reliably determine that an instance is `.Replaceable`, and thus *not* replace a `.Replaceable` instance with another `.Replaceable` instance. dotnet/java-interop@d3d3a1bf does similar things with `JniRuntime.JniValueManager`. Update `ManagedValueManager` to override the new `TryConstructPeer()` method, as `TryCreatePeer()` has been removed. Finally, an "interesting" crash happened when adding the `RuntimeHelpers.GetUninitializedObject()` + invoke constructor fix: E droid.NET_Test: JNI ERROR (app bug): accessed deleted Global 0x3056 F droid.NET_Test: java_vm_ext.cc:570] JNI DETECTED ERROR IN APPLICATION: use of deleted global reference 0x3056 or, when it didn't up and crash, we could see "bizarre" unit test failures from `JnienvTest.MoarThreadingTests()`: I NUnit : 1) Java.InteropTests.JnienvTest.MoarThreadingTests (Mono.Android.NET-Tests) I NUnit : No exception should be thrown [t2]! Got: System.ObjectDisposedException: Cannot access disposed object with JniIdentityHashCode=158880748. I NUnit : Object name: 'Java.Lang.Integer'. I NUnit : at Java.Interop.JniPeerMembers.AssertSelf(IJavaPeerable self) I NUnit : at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeAbstractInt32Method(String encodedMember, IJavaPeerable self, JniArgumentValue* parameters) I NUnit : at Java.Lang.Integer.IntValue() I NUnit : at Java.Lang.Integer.System.IConvertible.ToInt32(IFormatProvider provider) I NUnit : at System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider) I NUnit : at Android.Runtime.JNIEnv.GetObjectArray(IntPtr array_ptr, Type[] element_types) I NUnit : at Java.InteropTests.JnienvTest.<>c__DisplayClass26_0.<MoarThreadingTests>b__1() I NUnit : Expected: null I NUnit : But was: <System.ObjectDisposedException: Cannot access disposed object with JniIdentityHashCode=158880748. or: I NUnit : 1) Java.InteropTests.JnienvTest.MoarThreadingTests (Mono.Android.NET-Tests) I NUnit : No exception should be thrown [t2]! Got: System.ArgumentException: Handle must be valid. (Parameter 'instance') I NUnit : at Java.Interop.JniEnvironment.InstanceMethods.CallIntMethod(JniObjectReference instance, JniMethodInfo method, JniArgumentValue* args) I NUnit : at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeAbstractInt32Method(String encodedMember, IJavaPeerable self, JniArgumentValue* parameters) I NUnit : at Java.Lang.Integer.IntValue() I NUnit : at Java.Lang.Integer.System.IConvertible.ToInt32(IFormatProvider provider) I NUnit : at System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider) I NUnit : at Android.Runtime.JNIEnv.GetObjectArray(IntPtr array_ptr, Type[] element_types) I NUnit : at Java.InteropTests.JnienvTest.<>c__DisplayClass26_0.<MoarThreadingTests>b__1() The thread using the invalid global reference is `t1` in `MoarThreadingTests()`, which uses `JNIEnv.CopyObjectArray<int>(IntPtr, int[])`, which in turn uses `JavaConvert.FromJniHandle<T>()`. Why would that be failing? The answer: consider `JavaConvert.JniHandleConverters`: partial class JavaConvert { static Dictionary<Type, Func<IntPtr, JniHandleOwnership, object>> JniHandleConverters = new() { { typeof (int), (handle, transfer) => { using (var value = new Java.Lang.Integer (handle, transfer | JniHandleOwnership.DoNotRegister)) return value.IntValue (); } }, } } Note that we're invoking the `Integer(IntPtr, JniHandleOwnership)` constructor. As per the above Aside about Replaceable instances, this means that `value` should *replace* any existing mapping within the `JniRuntime.JniValueManager`. *However*, here was *also* provide `JniHandleOwnership.DoNotRegister`, which *prevents* the instance from being placed into the instance mapping, i.e. `Object.PeekObject()` *should not find the instance*. Next, turn to `Object.SetHandle()` from 5c23bcd: partial class /* Java.Lang. */ Object { protected void SetHandle (IntPtr value, JniHandleOwnership transfer) { var reference = new JniObjectReference (value); JNIEnvInit.ValueManager?.ConstructPeer ( this, ref reference, value == IntPtr.Zero ? JniObjectReferenceOptions.None : JniObjectReferenceOptions.Copy); JNIEnv.DeleteRef (value, transfer); } } What's missing? A check for `JniHandleOwnership.DoNotRegister`! Which means that the `new Integer(…)` will be in the instance map, *and replace any existing mappings*, only to be subsequently disposed. This permits the following "total execution order" to happen between threads `t1` and `t2` in `MoarThreadingTests()`: * Main thread: // JNI equivalent to Java `new Object[]{ new Integer (1) }` IntPtr lrefJliArray = JNIEnv.NewObjectArray<int>(new[]{1}) * `t1` Thread: IntPtr lrefInteger = JNIEnv.GetObjectArrayElement(lrefJliArray, 0); var value = new Integer(lrefInteger, .TransferLocalRef); // via JavaConvert.JniHandleConverters * `t2` Thread: IntPtr lrefInteger = JNIEnv.GetObjectArrayElement(lrefJliArray, 0); var x = Java.Lang.Object.GetObject (lrefInteger, .TransferLocalRef); // via JNIEnv.NativeArrayElementToManaged[typeof(IJavaObject)] Note that `x` is the same instance as `value`. * `t1` Thread: int v = value.IntValue(); value.Dispose(); * `t2` Thread: does *anything* with `x`. As `x` and `value` are the same instance, `x` has been disposed by thread `t1`. Depending on where thread scheduling, this would explain both the `ObjectDisposedException` as well as the `JNI ERROR (app bug)`. The fix is to correct the oversight from 5c23bcd, and forward the `JniHandleOwnership.DoNotRegister` flag to `JniObjectReferenceOptions`. *On the way to investigating this…* Update `tests/Mono.Android-Tests` to use `@(AndroidEnvironment)` in Debug builds to set `debug.mono.debug=1`. This allows for filenames and line numbers in stack traces. Update `src/native` so that the GREF log can be created; previously, `adb logcat` would contain: W monodroid: Failed to create directory '/data/user/0/Mono.Android.NET_Tests/files/.__override__/arm64-v8a'. No such file or directory … E monodroid: fopen failed for file /data/user/0/Mono.Android.NET_Tests/files/.__override__/arm64-v8a/grefs.txt: No such file or directory The apparent cause for this is that the ABI is now part of the path, `…/.__override__/arm64-v8a/grefs.txt` and not `…/.__override__/grefs.txt`. Additionally, `create_public_directory()` was a straight `mkdir()` call, which *does not* create intermediate directories. Update `create_public_directory()` to use `create_directory()` instead, which *does* create intermediate directories. This allows `grefs.txt` to be created. Next, the contents of `grefs.txt` occasionally looked "wrong". Turns out, `WriteGlobalReferenceLine()` and `WriteLocalReferenceLine()` didn't consistently append a newline to the message, which impacts e.g. the `Created …` message: Created PeerReference=0x3c06/G IdentityHashCode=0x2e29bd Instance=0xbe0aab36 Instance.Type=Android.OS.Bundle, Java.Type=android/os/Bundle+g+ grefc 19 gwrefc 0 obj-handle 0x706f711035/L -> new-handle 0x3d06/G from thread '<null>'(1) Update `WriteGlobalReferenceLine()` and `WriteLocalReferenceLine()` to always append a newline to the message. [0]: https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.runtimehelpers.getuninitializedobject?view=net-9.0
[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:
↩