Skip to content

Commit f0ae3fc

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 `JavaPeerableRegistrationScope.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. Additionally, 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 (); } Finally, many of the new members are marked with the [`[Experimental]` attribute][4], which indicates that an API is experimental and could change. In order to use the new APIs, then the `JI9999` warning must be disabled, e.g. via [`#pragma warning disable JI9999`][5]. This will allow us to break API and semantics, should that be necessary. 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 [4]: https://learn.microsoft.com/dotnet/api/system.diagnostics.codeanalysis.experimentalattribute?view=net-8.0 [5]: https://learn.microsoft.com/dotnet/csharp/language-reference/preprocessor-directives#pragma-warning
1 parent 7a058c0 commit f0ae3fc

13 files changed

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

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

+125
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,44 @@ public void Dispose ()
293303
disposed = true;
294304
}
295305

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

0 commit comments

Comments
 (0)