Skip to content

Commit c8d3e08

Browse files
committed
[Java.Interop] JavaObject: GREFs by default, not LREFs.
Fixes: #6 Context: https://trello.com/c/EXxOMZOD/397-add-java-lang-object-unregisterfromvm After presenting parts of the Java.Interop design to various other Xamarin employees at the 2014 Xummit, I got lots of pushback about the idea of not registering JavaObject instances by default and using JNI Local References by default. The problem/fear: it would be a usability nightmare. Single-threaded use kinda/sorta works...if you never leave the method. If another thread tries to access your JavaObject instance, you blow up. If you call Java code which calls back into managed code tries to grab that instance, you may blow up, as the value wasn't registered. It's not good. Then there's the future Android side of things: Android only permits 512 JNI local references; allocate more, and you abort. That's *really* not good. Partially revert commit 1caf077: don't use JNI Local References, use JNI Global References, *always*. (This fixes Android.) Additionally, register JavaObject and JavaException instances by default. Allow them to be *unregistered* via the new IJavaPeerable.UnregisterWithRuntime() method, *or* the new JniObjectReferenceOptions.DoNotRegisterWithRuntime enum value. The former will remove an existing registration; the latter will prevent it from being registered AT ALL [0]. [0]: This is a lie: we'll register the instance for the duration of the constructor, the unregister it automatically [1]. [1]: I'm not sure if this is a good idea or not. On the one time, we *need* the registration within the constructor to support Java-side invocation of virtual members: the instance MUST be registered so that we lookup the appropriate instance for virtual method dispatch at a later point in time. However, it also means that it *is* being registered, if only for a short period, and thus when sharing an instance from multiple threads, each trying to register/unregister, things may fail badly; see e.g. [2, 3] This idea requires further consideration; yet another reason JavaObject isn't ready for Xamarin.Android integration. [2]: https://bugzilla.xamarin.com/show_bug.cgi?id=14999 [3]: xamarin/monodroid@8138649
1 parent 0953980 commit c8d3e08

17 files changed

+48
-81
lines changed

src/Java.Interop.Dynamic/Tests/Java.Interop.Dynamic/DynamicJavaInstanceTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ public void Constructor ()
2626
public void DisposeWithJavaObjectDisposesObject ([Values (true, false)] bool register)
2727
{
2828
var native = new JavaObject ();
29-
if (register) {
30-
native.RegisterWithVM ();
29+
if (!register) {
30+
native.UnregisterFromRuntime ();
3131
}
3232

3333
var instance = new DynamicJavaInstance (native);

src/Java.Interop.Export/Tests/Java.Interop/ExportedMemberBuilderTest.cs

-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ public void AddExportMethods ()
3939
Assert.IsTrue (ExportTest.StaticActionInt32StringCalled);
4040

4141
using (var o = CreateExportTest (t)) {
42-
o.RegisterWithVM ();
4342
t.GetInstanceMethod ("testMethods", "()V").InvokeVirtualVoidMethod (o.PeerReference);
4443
Assert.IsTrue (o.HelloCalled);
4544
o.Dispose ();

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public interface IJavaPeerable : IDisposable
77
JniObjectReference PeerReference {get;}
88
JniPeerMembers JniPeerMembers {get;}
99

10-
void RegisterWithVM ();
10+
void UnregisterFromRuntime ();
1111
void DisposeUnlessRegistered ();
1212
}
1313
}

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,11 @@ protected SetSafeHandleCompletion SetPeerReference (ref JniObjectReference handl
130130
a => new SetSafeHandleCompletion (a));
131131
}
132132

133-
public void RegisterWithVM ()
133+
public void UnregisterFromRuntime ()
134134
{
135-
JniEnvironment.Runtime.RegisterObject (this);
135+
if (!PeerReference.IsValid)
136+
throw new ObjectDisposedException (GetType ().FullName);
137+
JniEnvironment.Runtime.UnRegisterObject (this);
136138
}
137139

138140
public void Dispose ()

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,11 @@ protected SetPeerReferenceCompletion SetPeerReference (ref JniObjectReference ha
7171
a => new SetPeerReferenceCompletion (a));
7272
}
7373

74-
public void RegisterWithVM ()
74+
public void UnregisterFromRuntime ()
7575
{
76-
JniEnvironment.Runtime.RegisterObject (this);
76+
if (!PeerReference.IsValid)
77+
throw new ObjectDisposedException (GetType ().FullName);
78+
JniEnvironment.Runtime.UnRegisterObject (this);
7779
}
7880

7981
public void Dispose ()

src/Java.Interop/Java.Interop/JavaProxyObject.cs

-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ public static JavaProxyObject GetProxy (object value)
6262
if (CachedValues.TryGetValue (value, out proxy))
6363
return proxy;
6464
proxy = new JavaProxyObject (value);
65-
proxy.RegisterWithVM ();
6665
CachedValues.Add (value, proxy);
6766
return proxy;
6867
}

src/Java.Interop/Java.Interop/JniEnvironment.Errors.cs

-3
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ public static void Throw (Exception e)
1212
var je = e as JavaException;
1313
if (je == null) {
1414
je = new JavaProxyThrowable (e);
15-
// because `je` may cross thread boundaries;
16-
// We'll need to rely on the GC to cleanup
17-
je.RegisterWithVM ();
1815
}
1916
Throw (je.PeerReference);
2017
}

src/Java.Interop/Java.Interop/JniEnvironment.References.cs

-2
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ public static void Dispose (ref JniObjectReference reference, JniObjectReference
3838
}
3939
reference.Invalidate ();
4040
break;
41-
default:
42-
throw new NotImplementedException ("Do not know how to transfer: " + transfer);
4341
}
4442
}
4543

src/Java.Interop/Java.Interop/JniObjectReferenceOptions.cs

+1-4
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@ public enum JniObjectReferenceOptions
88
Invalid = 0,
99
CreateNewReference = 1 << 0, // DoNotTransfer
1010
DisposeSourceReference = 1 << 1, // Transfer
11-
12-
/*
13-
Unregistered = 4,
14-
*/
11+
DoNotRegisterWithRuntime = 1 << 2,
1512
}
1613
}
1714

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

+10-16
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,8 @@ internal void RegisterObject<T> (T value)
327327
value.Registered = true;
328328
}
329329

330-
internal void UnRegisterObject (IJavaPeerableEx value)
330+
internal void UnRegisterObject<T> (T value)
331+
where T : IJavaPeerable, IJavaPeerableEx
331332
{
332333
int key = value.IdentityHashCode;
333334
lock (RegisteredInstances) {
@@ -341,27 +342,23 @@ internal void UnRegisterObject (IJavaPeerableEx value)
341342
}
342343
}
343344

344-
internal TCleanup SetObjectPeerReference<T, TCleanup> (T value, ref JniObjectReference reference, JniObjectReferenceOptions transfer, Func<Action, TCleanup> createCleanup)
345+
internal TCleanup SetObjectPeerReference<T, TCleanup> (T value, ref JniObjectReference reference, JniObjectReferenceOptions options, Func<Action, TCleanup> createCleanup)
345346
where T : IJavaPeerable, IJavaPeerableEx
346347
where TCleanup : IDisposable
347348
{
348349
if (!reference.IsValid)
349350
throw new ArgumentException ("handle is invalid.", nameof (reference));
350351

351-
bool register = reference.Flags == JniObjectReferenceFlags.Alloc;
352-
353-
value.SetPeerReference (reference.NewLocalRef ());
354-
JniEnvironment.References.Dispose (ref reference, transfer);
352+
var newRef = reference.NewGlobalRef ();
353+
value.SetPeerReference (newRef);
354+
JniEnvironment.References.Dispose (ref reference, options);
355355

356-
value.IdentityHashCode = JniSystem.IdentityHashCode (value.PeerReference);
356+
value.IdentityHashCode = JniSystem.IdentityHashCode (newRef);
357357

358-
if (register) {
359-
RegisterObject (value);
358+
RegisterObject (value);
359+
if ((options & JniObjectReferenceOptions.DoNotRegisterWithRuntime) == JniObjectReferenceOptions.DoNotRegisterWithRuntime) {
360360
Action unregister = () => {
361361
UnRegisterObject (value);
362-
var o = value.PeerReference;
363-
value.SetPeerReference (o.NewLocalRef ());
364-
JniEnvironment.References.Dispose (ref o);
365362
};
366363
return createCleanup (unregister);
367364
}
@@ -461,7 +458,7 @@ public IJavaPeerable GetObject (ref JniObjectReference reference, JniObjectRefer
461458
return null;
462459

463460
var existing = PeekObject (reference);
464-
if (existing != null && targetType != null && targetType.IsInstanceOfType (existing)) {
461+
if (existing != null && (targetType == null || targetType.IsInstanceOfType (existing))) {
465462
JniEnvironment.References.Dispose (ref reference, transfer);
466463
return existing;
467464
}
@@ -742,9 +739,6 @@ public virtual void RaisePendingException (Exception pendingException)
742739
var je = pendingException as JavaException;
743740
if (je == null) {
744741
je = new JavaProxyThrowable (pendingException);
745-
// because `je` may cross thread boundaries;
746-
// We'll need to rely on the GC to cleanup
747-
je.RegisterWithVM ();
748742
}
749743
JniEnvironment.Exceptions.Throw (je.PeerReference);
750744
#endif // !XA_INTEGRATION

src/Java.Interop/Tests/Java.Interop/InvokeVirtualFromConstructorTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public void InvokeVirtualFromConstructor ()
1414
{
1515
using (var t = new CallVirtualFromConstructorDerived (42)) {
1616
Assert.IsTrue (t.Called);
17-
Assert.IsNull (JniRuntime.Current.PeekObject (t.PeerReference));
17+
Assert.IsNotNull (JniRuntime.Current.PeekObject (t.PeerReference));
1818
}
1919
}
2020
}

src/Java.Interop/Tests/Java.Interop/JavaExceptionTests.cs

-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ static JavaException CreateJavaProxyThrowable (Exception value)
9393
.Assembly
9494
.GetType ("Java.Interop.JavaProxyThrowable", throwOnError :true);
9595
var proxy = (JavaException) Activator.CreateInstance (JavaProxyThrowable_type, value);
96-
proxy.RegisterWithVM ();
9796
return proxy;
9897
}
9998
}

src/Java.Interop/Tests/Java.Interop/JavaObjectTest.cs

+9-35
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,9 @@ public void JavaReferencedInstanceSurvivesCollection ()
1818
using (var t = new JniType ("java/lang/Object")) {
1919
var oldHandle = IntPtr.Zero;
2020
var array = new JavaObjectArray<JavaObject> (1);
21-
array.RegisterWithVM ();
2221
var w = new Thread (() => {
2322
var v = new JavaObject ();
2423
oldHandle = v.PeerReference.Handle;
25-
v.RegisterWithVM ();
2624
array [0] = v;
2725
});
2826
w.Start ();
@@ -34,6 +32,7 @@ public void JavaReferencedInstanceSurvivesCollection ()
3432
Assert.IsNotNull (JniRuntime.Current.PeekObject (first.PeerReference));
3533
var f = first.PeerReference;
3634
var o = (JavaObject) JniRuntime.Current.GetObject (ref f, JniObjectReferenceOptions.CreateNewReference);
35+
Assert.AreSame (first, o);
3736
if (oldHandle != o.PeerReference.Handle) {
3837
Console.WriteLine ("Yay, object handle changed; value survived a GC!");
3938
} else {
@@ -45,39 +44,31 @@ public void JavaReferencedInstanceSurvivesCollection ()
4544
}
4645

4746
[Test]
48-
public void RegisterWithVM ()
47+
public void UnregisterFromRuntime ()
4948
{
5049
int registeredCount = JniRuntime.Current.GetSurfacedObjects ().Count;
5150
JniObjectReference l;
5251
JavaObject o;
5352
using (o = new JavaObject ()) {
5453
l = o.PeerReference.NewLocalRef ();
55-
Assert.AreEqual (JniObjectReferenceType.Local, o.PeerReference.Type);
56-
Assert.AreEqual (registeredCount, JniRuntime.Current.GetSurfacedObjects ().Count);
57-
Assert.IsNull (JniRuntime.Current.PeekObject (l));
58-
o.RegisterWithVM ();
59-
Assert.AreNotSame (l, o.PeerReference);
6054
Assert.AreEqual (JniObjectReferenceType.Global, o.PeerReference.Type);
61-
JniEnvironment.References.Dispose (ref l);
62-
l = o.PeerReference.NewLocalRef ();
63-
Assert.AreEqual (registeredCount + 1, JniRuntime.Current.GetSurfacedObjects ().Count);
55+
Assert.AreEqual (registeredCount+1, JniRuntime.Current.GetSurfacedObjects ().Count);
56+
Assert.IsNotNull (JniRuntime.Current.PeekObject (l));
57+
Assert.AreNotSame (l, o.PeerReference);
6458
Assert.AreSame (o, JniRuntime.Current.PeekObject (l));
6559
}
6660
Assert.AreEqual (registeredCount, JniRuntime.Current.GetSurfacedObjects ().Count);
6761
Assert.IsNull (JniRuntime.Current.PeekObject (l));
6862
JniEnvironment.References.Dispose (ref l);
69-
Assert.Throws<ObjectDisposedException> (() => o.RegisterWithVM ());
63+
Assert.Throws<ObjectDisposedException> (() => o.UnregisterFromRuntime ());
7064
}
7165

7266
[Test]
7367
public void RegisterWithVM_ThrowsOnDuplicateEntry ()
7468
{
7569
using (var original = new JavaObject ()) {
76-
original.RegisterWithVM ();
7770
var p = original.PeerReference;
78-
var alias = new JavaObject (ref p, JniObjectReferenceOptions.CreateNewReference);
79-
Assert.Throws<NotSupportedException> (() => alias.RegisterWithVM ());
80-
alias.Dispose ();
71+
Assert.Throws<NotSupportedException> (() => new JavaObject (ref p, JniObjectReferenceOptions.CreateNewReference));
8172
}
8273
}
8374

@@ -90,7 +81,6 @@ public void UnreferencedInstanceIsCollected ()
9081
var v = new JavaObject ();
9182
oldHandle = v.PeerReference.NewWeakGlobalRef ();
9283
r = new WeakReference (v);
93-
v.RegisterWithVM ();
9484
});
9585
t.Start ();
9686
t.Join ();
@@ -127,6 +117,7 @@ public void Dispose_Finalized ()
127117
t.Join ();
128118
GC.Collect ();
129119
GC.WaitForPendingFinalizers ();
120+
GC.Collect ();
130121
GC.WaitForPendingFinalizers ();
131122
Assert.IsFalse (d);
132123
Assert.IsTrue (f);
@@ -144,7 +135,7 @@ public void ObjectDisposed ()
144135

145136
// These should throw
146137
Assert.Throws<ObjectDisposedException> (() => o.GetHashCode ());
147-
Assert.Throws<ObjectDisposedException> (() => o.RegisterWithVM ());
138+
Assert.Throws<ObjectDisposedException> (() => o.UnregisterFromRuntime ());
148139
Assert.Throws<ObjectDisposedException> (() => o.ToString ());
149140
Assert.Throws<ObjectDisposedException> (() => o.Equals (o));
150141
}
@@ -184,29 +175,12 @@ public void CrossThreadSharingRequresRegistration ()
184175
JavaObject o = null;
185176
var t = new Thread (() => {
186177
o = new JavaObject ();
187-
o.RegisterWithVM ();
188178
});
189179
t.Start ();
190180
t.Join ();
191181
o.ToString ();
192182
o.Dispose ();
193183
}
194-
195-
[Test]
196-
public void CrossThreadSharingNotSupported ()
197-
{
198-
if (!HaveSafeHandles) {
199-
Assert.Ignore ("SafeHandles not used. Cross-thread sharing can't be checked.");
200-
return;
201-
}
202-
203-
JavaObject o = null;
204-
var t = new Thread (() => o = new JavaObject ());
205-
t.Start ();
206-
t.Join ();
207-
Assert.Throws<NotSupportedException> (() => o.ToString ());
208-
o.Dispose ();
209-
}
210184
}
211185

212186
class JavaObjectWithNoJavaPeer : JavaObject {

src/Java.Interop/Tests/Java.Interop/JavaVMTest.cs

+3-4
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,11 @@ public void GetRegisteredJavaVM_ExistingInstance ()
6161
public void GetObject_ReturnsAlias ()
6262
{
6363
var local = new JavaObject ();
64+
local.UnregisterFromRuntime ();
6465
Assert.IsNull (JniRuntime.Current.PeekObject (local.PeerReference));
6566
// GetObject must always return a value (unless handle is null, etc.).
66-
// However, since we didn't call local.RegisterWithVM(),
67-
// JavaVM.PeekObject() is null (asserted above), but GetObject() must
67+
// However, since we called local.UnregisterFromRuntime(),
68+
// JniRuntime.PeekObject() is null (asserted above), but GetObject() must
6869
// **still** return _something_.
6970
// In this case, it returns an _alias_.
7071
// TODO: "most derived type" alias generation. (Not relevant here, but...)
@@ -88,8 +89,6 @@ public void GetObject_ReturnsRegisteredInstance ()
8889
JniObjectReference lref;
8990
using (var o = new JavaObject ()) {
9091
lref = o.PeerReference.NewLocalRef ();
91-
Assert.IsNull (JniRuntime.Current.PeekObject (lref));
92-
o.RegisterWithVM ();
9392
Assert.AreSame (o, JniRuntime.Current.PeekObject (lref));
9493
}
9594
// At this point, the Java-side object is kept alive by `lref`,

src/Java.Interop/Tests/Java.Interop/JniTransitionTest.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public void Dispose_ClearsLocalReferences ()
1919
}
2020
JniObjectReference lref;
2121
using (var envp = new JniTransition (JniEnvironment.EnvironmentPointer)) {
22-
lref = new JavaObject ().PeerReference;
22+
lref = new JavaObject ().PeerReference.NewLocalRef ();
2323
Assert.IsTrue (lref.IsValid);
2424
}
2525
Assert.IsFalse (lref.IsValid);

src/Java.Interop/Tests/Java.Interop/TestType.cs

+11-5
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,11 @@ static Delegate GetEqualsThisHandler ()
8686
{
8787
Func<IntPtr, IntPtr, IntPtr, bool> h = (jnienv, n_self, n_value) => {
8888
var jvm = JniEnvironment.Runtime;
89-
var self = jvm.GetObject<TestType>(n_self);
90-
var value = jvm.GetObject (n_value);
89+
var r_self = new JniObjectReference (n_self);
90+
var p = JniEnvironment.Runtime.PeekObject (r_self);
91+
var self = jvm.GetObject<TestType>(ref r_self, JniObjectReferenceOptions.DoNotRegisterWithRuntime);
92+
var r_value = new JniObjectReference (n_self);
93+
var value = jvm.GetObject (ref r_value, JniObjectReferenceOptions.DoNotRegisterWithRuntime);
9194

9295
try {
9396
return self.EqualsThis (value);
@@ -102,7 +105,8 @@ static Delegate GetEqualsThisHandler ()
102105
static Delegate GetInt32ValueHandler ()
103106
{
104107
Func<IntPtr, IntPtr, int> h = (jnienv, n_self) => {
105-
var self = JniEnvironment.Runtime.GetObject<TestType>(n_self);
108+
var r_self = new JniObjectReference (n_self);
109+
var self = JniEnvironment.Runtime.GetObject<TestType>(ref r_self, JniObjectReferenceOptions.DoNotRegisterWithRuntime);
106110
try {
107111
return self.GetInt32Value ();
108112
} finally {
@@ -120,7 +124,8 @@ static Delegate _GetStringValueHandler ()
120124

121125
static IntPtr GetStringValueHandler (IntPtr jnienv, IntPtr n_self, int value)
122126
{
123-
var self = JniEnvironment.Runtime.GetObject<TestType>(n_self);
127+
var r_self = new JniObjectReference (n_self);
128+
var self = JniEnvironment.Runtime.GetObject<TestType>(ref r_self, JniObjectReferenceOptions.DoNotRegisterWithRuntime);
124129
try {
125130
var s = self.GetStringValue (value);
126131
var r = JniEnvironment.Strings.NewString (s);
@@ -136,7 +141,8 @@ static IntPtr GetStringValueHandler (IntPtr jnienv, IntPtr n_self, int value)
136141

137142
static void MethodThrowsHandler (IntPtr jnienv, IntPtr n_self)
138143
{
139-
var self = JniEnvironment.Runtime.GetObject<TestType> (n_self);
144+
var r_self = new JniObjectReference (n_self);
145+
var self = JniEnvironment.Runtime.GetObject<TestType>(ref r_self, JniObjectReferenceOptions.DoNotRegisterWithRuntime);
140146
try {
141147
self.MethodThrows ();
142148
} finally {

src/Java.Interop/Tests/Java.Interop/TestTypeTests.cs

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public void EndArrayTests ()
2929
public void TestCase ()
3030
{
3131
using (var t = new TestType ()) {
32+
t.UnregisterFromRuntime ();
3233
t.RunTests ();
3334
}
3435
}

0 commit comments

Comments
 (0)