Skip to content

Commit 3043d89

Browse files
committed
[Java.Interop] Add JniManagedPeerStates, IJavaPeerable.JniManagedPeerState
We want to retrofit Xamarin.Android to use Java.Interop. This in turn requires that Java.Interop be able to do everything that Xamarin.Android does, with the added complication that Java.Interop will be used in Xamarin.Android before major parts are complete (e.g. Activation [8c83f64] and Issue #11 [0]). Fortunately we can look at how Xamarin.Android is architected to see what we might need to provide in advance before it's actually used, which leads us to Xamarin.Android's internal Java.Interop.IJavaObjectEx interface: interface IJavaObjectEx { IntPtr KeyHandle {get; set;} bool IsProxy {get; set;} bool NeedsActivation {get; set;} IntPtr ToLocalJniHandle (); } Of those members, IJavaObjectEx.KeyHandle is already exposed as IJavaPeerable.JniIdentityHashCode, and IJavaObjectEx.ToLocalJniHandle() shouldn't be needed (it was added to incorrectly address a multithreading-related bug). That leaves IJavaObjectEx.IsProxy and IJavaObjectEx.NeedsActivation, which are both involved in Java-side activation. In the interest of avoiding API breaks in the future, we need to support those constructs in Java.Interop *now*, even if they won't be fully utilized until later. Additionally, those names suck for "public" names -- what do they *mean*, sans context? -- and, while things have been reasonably stable here for the past several years, I'm not entirely certain that more such states won't need to be added in the future, so we need to support IJavaObjectEx.IsProxy and IJavaObjectEx.NeedsActivation semantics in an "extensible" manner? The solution? Yet another [Flags] enum! [Flags] public enum JniManagedPeerStates { None, Activatable = (1 << 0), Replaceable = (1 << 1), } The use of a [Flags] enum allows us to add additional states in the future, should we need to do so. JniManagedPeerStates.Activatable is IJavaObjectEx.NeedsActivation, and means that IJavaPeerable.PeerReference was set *before* the constructor was invoked. (Setting IJavaPeerable.PeerReference before the constructor executes is not yet done in Java.Interop.) It means that a future "proper" constructor invocation is assumed to be forthcoming, as during Java activation, IF the Java constructor virtually invokes a method which is overridden in managed code, a managed peer will need to be constructed so that the method override can be invoked, and "later" the "real" constructor will be invoked. https://developer.xamarin.com/guides/android/under_the_hood/architecture/#Java_Activation 1. `new Peer(...)` is invoked from Java. 2. A super class constructor of NativePeer virtually invokes a method overridden by Peer in managed code. 3. The Marshal Method is executed, which needs to delegate the method to *something*, and thus creates a new managed Peer instance through the activation constructor. This created managed peer instance will have the JniManagedPeerStates.Activatable state set. 4. The managed override is executed and returns back to Java. 5. Once all super constructors have finished, the Peer constructor executes com.xamarin.java_interop.ManagedPeer.runConstructor(), 6. ManagedPeer.runConstructor() invokes the appropriate corresponding constructor on the instance created in (3). If the JniManagedPeerStates.Activatable state *isn't* set, then the ManagedPeer.runConstructor() call would be *ignored*. This also means that *two constructors* will be invoked on *one instrance*. The JniManagedPeerStates.Activatable state needs to be set to sanely prevent invoking constructors more than is intended on a given peer instance. JniManagedPeerStates.Replaceable is IJavaObjectEx.IsProxy, and 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(). Normally, JniManagedPeerStates.Replaceable shouldn't be needed, but there is one environment where it is: Android devices which are pre-Honeycomb (API-11). On those devices, JNIEnv.AllocObject() cannot be used (8c4248b), so something very similar but not quite like the above Activation case can happen when constructing instances *from managed code*. In "normal" use -- JNIEnv::AllocObject() works! -- managed construction is: 1. `new Peer()` invokes managed constructor. 2. Managed constructor calls JniPeerMembers.InstanceMethods.StartCreateInstance(), which uses JNIEnv::AllocObject() to create a Java instance *without executing the Java instance constructor*. 3. JniRuntime.JniValueManager.Construct() adds the Java instance from (2) to the mapping table. 4. The constructor calls JniPeerMembers.InstanceMethods.FinishCreateInstance(), which invokes the Java instance constructor, and if a Java instance constructor virtually invokes a method overridden in managed code, the marshal method will find the instance created in (1). When JNIEnv::AllocObject() doesn't work, the above falls down: 1. `new Peer()` invokes managed constructor. 2. Managed constructor calls JniPeerMembers.InstanceMethods.StartCreateInstance(), which uses JNIEnv::NewObject() to create a Java instance *and executes the Java instance constructor*. 3. If a Java constructor virtually invokes a method overridden in managed code, the marshal method will be invoked and won't find an already-existing peer for the Java instance, and thus will create one via JniRuntime.JniValueManager.CreatePeer(). 4. JniRuntime.JniValueManager.CreatePeer() will set the JniManagedPeerStates.Replaceable state on the instance created in (3). Note: at this point there are *two* managed peers which want to "own" the Java instance: the instance created in (1) which is still being constructed (!), and the instance created in (3). 5. The overriding managed method returns control to the Java constructor, which eventually completes execution and returns execution to the Peer managed constructor in (1). 6. The Peer constructor invokes JniRuntime.JniValueManager.Construct() to add the instance from (1) to the instance mapping table, a mapping which *already exists* because of (3). There needs to be a way for (6) to replace the mapping created in (3) with the Peer instance in (1), and JniManagedPeerStates.Replaceable is how that is tracked. (Aside: supporting platforms that have broken JNIEnv::AllocObject() implementations is a GIANT PAIN IN THE ASS.) To use JniManagedPeerStates, update IJavaPeerable to add the following members: partial interface IJavaPeerable { JniManagedPeerStates JniManagedPeerState {get;} void SetJniManagedPeerState (JniManagedPeerStates value); } IJavaPeerable.JniManagedPeerState is the current state of the managed peer. IJavaPeerable.SetJniManagedPeerState() allows updating the current state of the managed peer, permitting the state to be tracked cross activation constructor calls. [0]: #11
1 parent 51f7862 commit 3043d89

File tree

10 files changed

+129
-2
lines changed

10 files changed

+129
-2
lines changed

src/Java.Interop/Documentation/Java.Interop/IJavaPeerable.xml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,19 @@ partial class ExampleBinding : IJavaPeerable {
195195
</remarks>
196196
<seealso cref="M:Java.Interop.IJavaPeerable.SetJniIdentityHashCode" />
197197
</member>>
198+
<member name="P:JniManagedPeerState">
199+
<summary>State of the managed peer.</summary>
200+
<remarks>
201+
<para>
202+
A <see cref="T:Java.Interop.JniManagedPeerStates" /> value providing
203+
the state of the current Managed Peer instance.
204+
<format type="text/html">
205+
(<a href="https://developer.xamarin.com/guides/android/advanced_topics/garbage_collection/#Cross-VM_Object_Collections"
206+
>Definitions</a>)
207+
</format>
208+
</para>
209+
</remarks>
210+
</member>
198211
<member name="P:JniPeerMembers">
199212
<summary>
200213
Member access and invocation support.
@@ -285,6 +298,20 @@ partial class ExampleBinding {
285298
</remarks>
286299
<seealso cref="P:Java.Interop.IJavaPeerable.JniIdentityHashCode" />
287300
</member>
301+
<member name="P:SetJniManagedPeerState">
302+
<summary>Set the state of the managed peer.</summary>
303+
<param name="value">
304+
A <see cref="T:Java.Interop.JniManagedPeerStates" /> value providing
305+
the state of the current Managed Peer instance.
306+
</param>
307+
<remarks>
308+
<para>
309+
This should only be called from a
310+
<see cref="T:Java.Interop.JniRuntime+JniValueManager" /> instance.
311+
</para>
312+
</remarks>
313+
<seealso cref="P:Java.Interop.IJavaPeerable.JniManagedPeerState" />
314+
</member>
288315
<member name="M:SetPeerReference">
289316
<summary>Set the value returned by <c>PeerReference</c>.</summary>
290317
<param name="value">
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?xml version="1.0"?>
2+
<docs>
3+
<member name="T:JniManagedPeerStates">
4+
<summary>
5+
Managed peer states.
6+
</summary>
7+
</member>
8+
<member name="F:Activatable">
9+
<summary>
10+
Managed peer is eligeable for activation.
11+
</summary>
12+
<remarks>
13+
<para>
14+
A managed peer is <i>activatable</i> if it the native peer was
15+
constructed first and a forthcoming constructor invocation is
16+
assumed to be forthcoming.
17+
</para>
18+
</remarks>
19+
</member>
20+
<member name="F:Replaceable">
21+
<summary>
22+
Managed peer is replaceable.
23+
</summary>
24+
<remarks>
25+
<para>
26+
A managed peer is <i>replaceable</i> if it was created through
27+
<see cref="M:Java.Interop.JniRuntime+JniValueManager.CreatePeer" />.
28+
Being "replaceable" means that if another peer instance is created
29+
around the same Java instance, the newly created instance will replace
30+
the replaceable peer in the Java-to-managed instance mapping.
31+
</para>
32+
</remarks>
33+
</member>
34+
</docs>

src/Java.Interop/Java.Interop.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
<Compile Include="Java.Interop\JniEnvironment.Strings.cs" />
7777
<Compile Include="Java.Interop\JniEnvironment.Types.cs" />
7878
<Compile Include="Java.Interop\JniFieldInfo.cs" />
79+
<Compile Include="Java.Interop\JniManagedPeerStates.cs" />
7980
<Compile Include="Java.Interop\JniMethodInfo.cs" />
8081
<Compile Include="Java.Interop\JniObjectReference.cs" />
8182
<Compile Include="Java.Interop\JniPeerMembers.cs" />
@@ -184,5 +185,6 @@
184185
<LastGenOutput>JniPeerMembers.JniMethods.cs</LastGenOutput>
185186
</None>
186187
<None Include="Documentation\Java.Interop\IJavaPeerable.xml" />
188+
<None Include="Documentation\Java.Interop\JniManagedPeerStates.xml" />
187189
</ItemGroup>
188190
</Project>

src/Java.Interop/Java.Interop/IJavaPeerable.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ public interface IJavaPeerable : IDisposable
1919
/// <include file="../Documentation/Java.Interop/IJavaPeerable.xml" path="/docs/member[@name='P:JniPeerMembers']/*" />
2020
JniPeerMembers JniPeerMembers {get;}
2121

22+
/// <include file="../Documentation/Java.Interop/IJavaPeerable.xml" path="/docs/member[@name='P:JniManagedPeerState']/*" />
23+
JniManagedPeerStates JniManagedPeerState {get;}
24+
25+
/// <include file="../Documentation/Java.Interop/IJavaPeerable.xml" path="/docs/member[@name='M:SetJniManagedPeerState']/*" />
26+
void SetJniManagedPeerState (JniManagedPeerStates value);
27+
2228
// Lifetime management
2329
/// <include file="../Documentation/Java.Interop/IJavaPeerable.xml" path="/docs/member[@name='M:UnregisterFromRuntime']/*" />
2430
void UnregisterFromRuntime ();

src/Java.Interop/Java.Interop/JavaException.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ unsafe public class JavaException : Exception, IJavaPeerable
1111
int identity;
1212
string javaStackTrace;
1313

14+
JniManagedPeerStates state;
15+
1416
#if FEATURE_JNIOBJECTREFERENCE_SAFEHANDLES
1517
JniObjectReference reference;
1618
#endif // FEATURE_JNIOBJECTREFERENCE_SAFEHANDLES
@@ -232,6 +234,10 @@ unsafe string _GetJavaStack (JniObjectReference handle)
232234
}
233235
}
234236

237+
JniManagedPeerStates IJavaPeerable.JniManagedPeerState {
238+
get {return state;}
239+
}
240+
235241
void IJavaPeerable.Disposed ()
236242
{
237243
Dispose (disposing: true);
@@ -247,6 +253,11 @@ void IJavaPeerable.SetJniIdentityHashCode (int value)
247253
identity = value;
248254
}
249255

256+
void IJavaPeerable.SetJniManagedPeerState (JniManagedPeerStates value)
257+
{
258+
state = value;
259+
}
260+
250261
void IJavaPeerable.SetPeerReference (JniObjectReference reference)
251262
{
252263
SetPeerReference (ref reference, JniObjectReferenceOptions.Copy);

src/Java.Interop/Java.Interop/JavaObject.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ unsafe public class JavaObject : IJavaPeerable
99

1010
int keyHandle;
1111

12+
JniManagedPeerStates state;
13+
1214
#if FEATURE_JNIOBJECTREFERENCE_SAFEHANDLES
1315
JniObjectReference reference;
1416
#endif // FEATURE_JNIOBJECTREFERENCE_SAFEHANDLES
@@ -44,6 +46,10 @@ public int JniIdentityHashCode {
4446
get {return keyHandle;}
4547
}
4648

49+
public JniManagedPeerStates JniManagedPeerState {
50+
get {return state;}
51+
}
52+
4753
// Note: JniPeerMembers is invoked virtually from the constructor;
4854
// it MUST be valid before the derived constructor executes!
4955
// The pattern MUST be followed.
@@ -136,6 +142,10 @@ public override unsafe string ToString ()
136142
return JniEnvironment.Strings.ToString (ref lref, JniObjectReferenceOptions.CopyAndDispose);
137143
}
138144

145+
JniManagedPeerStates IJavaPeerable.JniManagedPeerState {
146+
get {return state;}
147+
}
148+
139149
void IJavaPeerable.Disposed ()
140150
{
141151
Dispose (disposing: true);
@@ -151,6 +161,11 @@ void IJavaPeerable.SetJniIdentityHashCode (int value)
151161
keyHandle = value;
152162
}
153163

164+
void IJavaPeerable.SetJniManagedPeerState (JniManagedPeerStates value)
165+
{
166+
state = value;
167+
}
168+
154169
void IJavaPeerable.SetPeerReference (JniObjectReference reference)
155170
{
156171
SetPeerReference (ref reference, JniObjectReferenceOptions.Copy);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System;
2+
3+
namespace Java.Interop
4+
{
5+
/// <include file="../Documentation/Java.Interop/JniManagedPeerStates.xml" path="/docs/member[@name='T:JniManagedPeerStates']/*" />
6+
[Flags]
7+
public enum JniManagedPeerStates {
8+
None,
9+
/// <include file="../Documentation/Java.Interop/JniManagedPeerStates.xml" path="/docs/member[@name='F:Activatable']/*" />
10+
Activatable = (1 << 0),
11+
/// <include file="../Documentation/Java.Interop/JniManagedPeerStates.xml" path="/docs/member[@name='F:Replaceable']/*" />
12+
Replaceable = (1 << 1),
13+
}
14+
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ public void Construct (IJavaPeerable value, ref JniObjectReference reference, Jn
6565
if (value == null)
6666
throw new ArgumentNullException (nameof (value));
6767

68+
var activationRef = value.PeerReference;
69+
if (activationRef.IsValid) {
70+
value.SetJniManagedPeerState (value.JniManagedPeerState | JniManagedPeerStates.Activatable);
71+
JniObjectReference.Dispose (ref reference, options);
72+
reference = activationRef;
73+
options = JniObjectReferenceOptions.Copy;
74+
}
75+
6876
if (!reference.IsValid)
6977
throw new ArgumentException ("handle is invalid.", nameof (reference));
7078

@@ -229,7 +237,9 @@ public virtual IJavaPeerable CreatePeer (ref JniObjectReference reference, JniOb
229237
transfer,
230238
};
231239
try {
232-
return (IJavaPeerable) ctor.Invoke (acts);
240+
var peer = (IJavaPeerable) ctor.Invoke (acts);
241+
peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable);
242+
return peer;
233243
} finally {
234244
reference = (JniObjectReference) acts [0];
235245
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public override void Add (IJavaPeerable value)
109109
lock (RegisteredInstances) {
110110
WeakReference<IJavaPeerable> existing;
111111
IJavaPeerable target;
112-
if (RegisteredInstances.TryGetValue (key, out existing) && existing.TryGetTarget (out target))
112+
if (RegisteredInstances.TryGetValue (key, out existing) && existing.TryGetTarget (out target) && !Replaceable (target))
113113
Runtime.ObjectReferenceManager.WriteGlobalReferenceLine (
114114
"Warning: Not registering PeerReference={0} IdentityHashCode=0x{1} Instance={2} Instance.Type={3} Java.Type={4}; " +
115115
"keeping previously registered PeerReference={5} Instance={6} Instance.Type={7} Java.Type={8}.",
@@ -127,6 +127,13 @@ public override void Add (IJavaPeerable value)
127127
}
128128
}
129129

130+
static bool Replaceable (IJavaPeerable peer)
131+
{
132+
if (peer == null)
133+
return true;
134+
return (peer.JniManagedPeerState & JniManagedPeerStates.Replaceable) == JniManagedPeerStates.Replaceable;
135+
}
136+
130137
public override void Remove (IJavaPeerable value)
131138
{
132139
int key = value.JniIdentityHashCode;

xa-gendarme-ignore.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ R: Gendarme.Rules.Design.FlagsShouldNotDefineAZeroValueRule
109109
# > **AVOID** using flag enum values of zero unless the value represents "all flags are cleared" and is named appropriately, as prescribed by the next guideline
110110
# "Invalid" implies "all flags are cleared" to *me*...
111111
# > **DO** name the zero value of flag enums None. For a flag enum, the value must always mean "all flags are cleared."
112+
T: Java.Interop.JniManagedPeerStates
112113
T: Java.Interop.JniObjectReferenceOptions
113114

114115

0 commit comments

Comments
 (0)