Skip to content

Commit 4f0ccbb

Browse files
committed
[Java.Interop] Add JavaPeerableRegistrationScope
Fixes: #4 Context: #426 Context: a666a6f Alternate names? * JavaScope * JavaPeerableCleanupPool * JavaPeerCleanup * JavaReferenceCleanup * JniPeerRegistrationScope Issue #426 is an idea to implement a *non*-GC-Bridged `JniRuntime.JniValueManager` type, primarily for use with .NET Core. This was begun in a666a6f. What's missing is the answer to a question: what to do about `JniRuntime.JniValueManager.CollectPeers()`? With a Mono-style GC bridge, `CollectPeers()` is `GC.Collect()`. In a666a6f with .NET Core, `CollectPeers()` calls `IJavaPeerable.Dispose()` on all registered instances, which is "extreme". @jonpryor thought that if there were a *scope-based* way to selectively control which instances were disposed, that might be "better" and more understandable. Plus, this is issue #4! Which brings us to the background for Issue #4, which touches upon [bugzilla 25443][0] and [Google issue 37034307][1]: Java.Interop attempts to provide referential identity for Java objects: if two separate Java methods invocations return the same Java instance, then the bindings of those methods should also return the same instance. This is "fine" on the surface, but has a few related issues: 1. JNI Global References are a limited resource: Android only allows ~52000 JNI Global References to exist, which sounds like a lot, but might not be. 2. Because of (1), it is not uncommon to want to use `using` blocks to invoke `IJavaPeerable.Dispose()` to release JNI Global References. 3. However, because of object identity, you could unexpectedly introduce *cross-thread sharing* of `IJavaPeerable` instances. This might not be at all obvious; for example, in the Android 5 timeframe, [`Typeface.create()`][2] wouldn't necessarily return new Java instances, but may instead return cached instances. Meaning that this: var t1 = new Thread(() => { using var typeface = Typeface.Create("Family", …); // use typeface… }); var t2 = new Thread(() => { using var typeface = Typeface.Create("Family", …); // use typeface… }); t1.Start(); t2.Start(); t1.Join(); t2.Join(); could plausibly result in `ObjectDisposedException`s (or worse), as the threads could be unexpectedly sharing the same bound instance. Which *really* means that you can't reliably call `Dispose()`, unless you *know* you created that instance: using var safe = new Java.Lang.Double(42.0); // I created this, therefore I control all access and can Dispose() it using var unsafe = Java.Lang.Double.ValueOf(42.0); // I have no idea who else may be using this instance! Attempt to address both of these scenarios -- a modicum of .NET Core support, and additional sanity around JNI Global Reference lifetimes -- by introducing a new `JavaPeerableRegistrationScope` type, which introduces a scope-based mechanism to control when `IJavaPeerable` instances are cleaned up: public enum JavaPeerableRegistrationScopeCleanup { RegisterWithManager, Dispose, Release, } public ref struct JavaPeerableRegistrationScope { public JavaPeerableRegistrationScope(JavaPeerableRegistrationScopeCleanup cleanup); public void Dispose(); } `JavaPeerableRegistrationScope` is a [`ref struct`][3], which means it can only be allocated on the runtime stack, ensuring that cleanup semantics are *scope* semantics. TODO: is that actually a good idea? If a `JavaPeerableRegistrationScope` is created using `JavaPeerableRegistrationScopeCleanup.RegisterWithManager`, existing behavior is followed. This is useful for nested scopes, should instances need to be registered with `JniRuntime.ValueManager`. If a `JavaPeerableRegistrationScope` is created using `JavaPeerableRegistrationScopeCleanup.Dispose` or `JavaPeerableRegistrationScopeCleanup.Release`, then: 1. `IJavaPeerable` instances created within the scope are "thread-local": they can be *used* by other threads, but `JniRuntime.JniValueManager.PeekPeer()` will only find the value on the creating thread. 2. At the end of a `using` block / when `JavaScope.Dispose()` is called, all collected instances will be `Dispose()`d (with `.Dispose`) or released (with `.Release`), and left to the GC to eventually finalize. For example: using (new JavaPeerableRegistrationScope (JavaPeerableRegistrationScopeCleanup.Dispose)) { var singleton = JavaSingleton.Singleton; // use singleton // If other threads attempt to access `JavaSingleton.Singleton`, // they'll create their own peer wrapper } // `singleton.Dispose()` is called at the end of the `using` block However, if e.g. the singleton instance is already accessed, then it won't be added to the registration scope and won't be disposed: var singleton = JavaSingleton.Singleton; // singleton is registered with the ValueManager // later on the same thread or some other threa… using (new JavaPeerableRegistrationScope (JavaPeerableRegistrationScopeCleanup.Dispose)) { var anotherSingleton = JavaSingleton.Singleton; // use anotherSingleton… } then `anotherSingleton` will *not* disposed, as it already existed. *Only newly created instances* are added to the current scope. By default, only `JavaPeerableRegistrationScopeCleanup.RegisterWithManager` is supported. To support the other cleanup modes, `JniRuntime.JniValueManager.SupportsPeerableRegistrationScopes` must return `true`, which in turn requires that: * `JniRuntime.JniValueManager.AddPeer()` calls `TryAddPeerToRegistrationScope()`, * `JniRuntime.JniValueManager.RemovePeer()` calls `TryRemovePeerFromRegistrationScopes()` * `JniRuntime.JniValueManager.PeekPeer()` calls `TryPeekPeerFromRegistrationScopes()`. See `ManagedValueManager` for an example implementation. Finally, add the following methods to `JniRuntime.JniValueManager` to help further assist peer management: partial class JniRuntime.JniValueManager { public virtual bool CanCollectPeers { get; } public virtual bool CanReleasePeers { get; } public virtual void ReleasePeers (); } TODO: docs? TODO: *nested* scopes, and "bound" vs. "unbound" instance construction around `JniValueManager.GetValue<T>()` or `.CreateValue<T>()`, and *why* they should be treated differently. TODO: Should `CreateValue<T>()` be *removed*? name implies it always "creates" a new value, but implementation will return existing instances, so `GetValue<T>()` alone may be better. One related difference is that` `CreateValue<T>()` uses `PeekBoxedObject()`, while `GetValue<T>()` doesn't. *Should* it? [0]: https://web.archive.org/web/20211106214514/https://bugzilla.xamarin.com/25/25443/bug.html#c63 [1]: https://issuetracker.google.com/issues/37034307 [2]: https://developer.android.com/reference/android/graphics/Typeface#create(java.lang.String,%20int) [3]: https://docs.microsoft.com/dotnet/csharp/language-reference/builtin-types/struct#ref-struct
1 parent 7a058c0 commit 4f0ccbb

File tree

11 files changed

+842
-22
lines changed

11 files changed

+842
-22
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
5+
namespace Java.Interop {
6+
public enum JavaPeerableRegistrationScopeCleanup {
7+
RegisterWithManager,
8+
Dispose,
9+
Release,
10+
}
11+
12+
public ref struct JavaPeerableRegistrationScope {
13+
PeerableCollection? scope;
14+
bool disposed;
15+
16+
public JavaPeerableRegistrationScope (JavaPeerableRegistrationScopeCleanup cleanup)
17+
{
18+
scope = JniEnvironment.CurrentInfo.BeginRegistrationScope (cleanup);
19+
disposed = false;
20+
}
21+
22+
public void Dispose ()
23+
{
24+
if (disposed) {
25+
return;
26+
}
27+
disposed = true;
28+
JniEnvironment.CurrentInfo.EndRegistrationScope (scope);
29+
scope = null;
30+
}
31+
}
32+
}

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

+4-16
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ sealed class JavaProxyObject : JavaObject, IEquatable<JavaProxyObject>
1313
internal const string JniTypeName = "net/dot/jni/internal/JavaProxyObject";
1414

1515
static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (JavaProxyObject));
16-
static readonly ConditionalWeakTable<object, JavaProxyObject> CachedValues = new ConditionalWeakTable<object, JavaProxyObject> ();
1716

1817
[JniAddNativeMethodRegistrationAttribute]
1918
static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args)
@@ -29,10 +28,8 @@ public override JniPeerMembers JniPeerMembers {
2928
}
3029
}
3130

32-
JavaProxyObject (object value)
31+
internal JavaProxyObject (object value)
3332
{
34-
if (value == null)
35-
throw new ArgumentNullException (nameof (value));
3633
Value = value;
3734
}
3835

@@ -57,19 +54,10 @@ public override bool Equals (object? obj)
5754
return Value.ToString ();
5855
}
5956

60-
[return: NotNullIfNotNull ("object")]
61-
public static JavaProxyObject? GetProxy (object value)
57+
protected override void Dispose (bool disposing)
6258
{
63-
if (value == null)
64-
return null;
65-
66-
lock (CachedValues) {
67-
if (CachedValues.TryGetValue (value, out var proxy))
68-
return proxy;
69-
proxy = new JavaProxyObject (value);
70-
CachedValues.Add (value, proxy);
71-
return proxy;
72-
}
59+
base.Dispose (disposing);
60+
JniEnvironment.Runtime.ValueManager.RemoveProxy (Value);
7361
}
7462

7563
// TODO: Keep in sync with the code generated by ExportedMemberBuilder

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

+121
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
using System;
44
using System.Collections.Generic;
5+
using System.Collections.ObjectModel;
56
using System.Diagnostics;
7+
using System.Diagnostics.CodeAnalysis;
68
using System.Linq;
79
using System.Runtime.CompilerServices;
810
using System.Runtime.InteropServices;
11+
using System.Text;
912
using System.Threading;
1013

1114
namespace Java.Interop {
@@ -209,6 +212,8 @@ sealed class JniEnvironmentInfo : IDisposable {
209212
bool disposed;
210213
JniRuntime? runtime;
211214

215+
List<PeerableCollection?>? scopes;
216+
212217
public int LocalReferenceCount {get; internal set;}
213218
public bool WithinNewObjectScope {get; set;}
214219
public JniRuntime Runtime {
@@ -243,6 +248,11 @@ public bool IsValid {
243248
get {return Runtime != null && environmentPointer != IntPtr.Zero;}
244249
}
245250

251+
public List<PeerableCollection?>?
252+
Scopes => scopes;
253+
public PeerableCollection? CurrentScope =>
254+
scopes == null ? null : scopes [scopes.Count-1];
255+
246256
public JniEnvironmentInfo ()
247257
{
248258
Runtime = JniRuntime.CurrentRuntime;
@@ -293,6 +303,42 @@ public void Dispose ()
293303
disposed = true;
294304
}
295305

306+
public PeerableCollection? BeginRegistrationScope (JavaPeerableRegistrationScopeCleanup cleanup)
307+
{
308+
if (cleanup != JavaPeerableRegistrationScopeCleanup.RegisterWithManager &&
309+
!Runtime.ValueManager.SupportsPeerableRegistrationScopes) {
310+
throw new NotSupportedException ("Peerable registration scopes are not supported by this runtime.");
311+
}
312+
scopes ??= new List<PeerableCollection?> ();
313+
if (cleanup == JavaPeerableRegistrationScopeCleanup.RegisterWithManager) {
314+
scopes.Add (null);
315+
return null;
316+
}
317+
var scope = new PeerableCollection (cleanup);
318+
scopes.Add (scope);
319+
return scope;
320+
}
321+
322+
public void EndRegistrationScope (PeerableCollection? scope)
323+
{
324+
Debug.Assert (scopes != null);
325+
if (scopes == null) {
326+
return;
327+
}
328+
329+
for (int i = scopes.Count; i > 0; --i) {
330+
var s = scopes [i - 1];
331+
if (s == scope) {
332+
scopes.RemoveAt (i - 1);
333+
break;
334+
}
335+
}
336+
if (scopes.Count == 0) {
337+
scopes = null;
338+
}
339+
scope?.DisposeScope ();
340+
}
341+
296342
#if FEATURE_JNIENVIRONMENT_SAFEHANDLES
297343
internal List<List<JniLocalReference>> LocalReferences = new List<List<JniLocalReference>> () {
298344
new List<JniLocalReference> (),
@@ -309,5 +355,80 @@ static unsafe JniEnvironmentInvoker CreateInvoker (IntPtr handle)
309355
}
310356
#endif // !FEATURE_JNIENVIRONMENT_JI_PINVOKES
311357
}
358+
359+
sealed class PeerableCollection : KeyedCollection<int, IJavaPeerable> {
360+
public JavaPeerableRegistrationScopeCleanup Cleanup { get; }
361+
362+
public PeerableCollection (JavaPeerableRegistrationScopeCleanup cleanup)
363+
{
364+
Cleanup = cleanup;
365+
}
366+
367+
protected override int GetKeyForItem (IJavaPeerable item) => item.JniIdentityHashCode;
368+
369+
public IJavaPeerable? GetPeerable (JniObjectReference reference, int identityHashCode)
370+
{
371+
if (!reference.IsValid) {
372+
return null;
373+
}
374+
if (TryGetValue (identityHashCode, out var p) &&
375+
JniEnvironment.Types.IsSameObject (reference, p.PeerReference)) {
376+
return p;
377+
}
378+
return null;
379+
}
380+
381+
public void DisposeScope ()
382+
{
383+
Console.Error.WriteLine ($"# jonp: DisposeScope: {Cleanup}");
384+
Debug.Assert (Cleanup != JavaPeerableRegistrationScopeCleanup.RegisterWithManager);
385+
switch (Cleanup) {
386+
case JavaPeerableRegistrationScopeCleanup.Dispose:
387+
List<Exception>? exceptions = null;
388+
foreach (var p in this) {
389+
DisposePeer (p, ref exceptions);
390+
}
391+
Clear ();
392+
if (exceptions != null) {
393+
throw new AggregateException ("Exceptions while disposing peers.", exceptions);
394+
}
395+
break;
396+
case JavaPeerableRegistrationScopeCleanup.Release:
397+
Clear ();
398+
break;
399+
case JavaPeerableRegistrationScopeCleanup.RegisterWithManager:
400+
default:
401+
throw new NotSupportedException ($"Unsupported scope cleanup value: {Cleanup}");
402+
}
403+
404+
[SuppressMessage ("Design", "CA1031:Do not catch general exception types",
405+
Justification = "Exceptions are bundled into an AggregateException and rethrown")]
406+
static void DisposePeer (IJavaPeerable peer, ref List<Exception>? exceptions)
407+
{
408+
try {
409+
Console.Error.WriteLine ($"# jonp: DisposeScope: disposing of: {peer} {peer.PeerReference}");
410+
peer.Dispose ();
411+
} catch (Exception e) {
412+
exceptions ??= new ();
413+
exceptions.Add (e);
414+
Trace.WriteLine (e);
415+
}
416+
}
417+
}
418+
419+
public override string ToString ()
420+
{
421+
var c = (Collection<IJavaPeerable>) this;
422+
var s = new StringBuilder ();
423+
s.Append ("PeerableCollection[").Append (Count).Append ("](");
424+
for (int i = 0; i < Count; ++i ) {
425+
s.AppendLine ();
426+
var e = c [i];
427+
s.Append ($" [{i}] hash={e.JniIdentityHashCode} ref={e.PeerReference} type={e.GetType ().ToString ()} value=`{e.ToString ()}`");
428+
}
429+
s.Append (")");
430+
return s.ToString ();
431+
}
432+
}
312433
}
313434

0 commit comments

Comments
 (0)