Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 1013e93

Browse files
committedAug 6, 2024·
[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 1013e93

12 files changed

+887
-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

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

+180-3
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,41 @@
1414

1515
namespace Java.Interop
1616
{
17+
[Experimental ("JI9999")]
18+
[Flags]
19+
public enum JniSurfacedPeerLocations {
20+
ValueManager = 1 << 0,
21+
CurrentThread = 1 << 1,
22+
AllThreads = 1 << 2,
23+
// Everywhere = ValueManager | CurrentThread | AllThreads,
24+
// Can't add, because PublicAPI analyzers report RS0016 + RS0017
25+
}
26+
1727
public class JniSurfacedPeerInfo {
1828

1929
public int JniIdentityHashCode {get; private set;}
2030
public WeakReference<IJavaPeerable> SurfacedPeer {get; private set;}
2131

32+
[Experimental ("JI9999")]
33+
public JniSurfacedPeerLocations Location {get; internal set;}
34+
2235
public JniSurfacedPeerInfo (int jniIdentityHashCode, WeakReference<IJavaPeerable> surfacedPeer)
2336
{
2437
JniIdentityHashCode = jniIdentityHashCode;
2538
SurfacedPeer = surfacedPeer;
2639
}
40+
41+
public override string ToString ()
42+
{
43+
#pragma warning disable JI9999
44+
if (SurfacedPeer.TryGetTarget (out var target) && target != null) {
45+
return $"(JniSurfacedPeerInfo JniIdentityHashCode={JniIdentityHashCode} Location={Location} " +
46+
$"SurfacedPeer=({target.GetType ().FullName} {target.PeerReference} {target.ToString ()})" +
47+
")";
48+
}
49+
return $"(JniSurfacedPeerInfo JniIdentityHashCode={JniIdentityHashCode} Location={Location})";
50+
#pragma warning restore JI9999
51+
}
2752
}
2853

2954
partial class JniRuntime
@@ -32,7 +57,7 @@ partial class CreationOptions {
3257
public JniValueManager? ValueManager {get; set;}
3358
}
3459

35-
JniValueManager? valueManager;
60+
internal JniValueManager? valueManager;
3661
public JniValueManager ValueManager {
3762
get => valueManager ?? throw new NotSupportedException ();
3863
}
@@ -52,6 +77,8 @@ public abstract partial class JniValueManager : ISetRuntime, IDisposable {
5277
internal const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors;
5378
internal const DynamicallyAccessedMemberTypes ConstructorsAndInterfaces = Constructors | DynamicallyAccessedMemberTypes.Interfaces;
5479

80+
readonly ConditionalWeakTable<object, JavaProxyObject> ProxyValues = new ();
81+
5582
JniRuntime? runtime;
5683
bool disposed;
5784
public JniRuntime Runtime {
@@ -77,10 +104,19 @@ protected virtual void Dispose (bool disposing)
77104
disposed = true;
78105
}
79106

107+
public virtual bool CanCollectPeers => false;
108+
public virtual bool CanReleasePeers => false;
109+
public virtual bool SupportsPeerableRegistrationScopes => false;
110+
80111
public abstract void WaitForGCBridgeProcessing ();
81112

82113
public abstract void CollectPeers ();
83114

115+
public virtual void ReleasePeers ()
116+
{
117+
throw new NotImplementedException ();
118+
}
119+
84120
public abstract void AddPeer (IJavaPeerable value);
85121

86122
public abstract void RemovePeer (IJavaPeerable value);
@@ -89,6 +125,102 @@ protected virtual void Dispose (bool disposing)
89125

90126
public abstract List<JniSurfacedPeerInfo> GetSurfacedPeers ();
91127

128+
[Experimental ("JI9999")]
129+
public List<JniSurfacedPeerInfo> GetRegistrationScopePeers ()
130+
=> GetRegistrationScopePeers (JniSurfacedPeerLocations.ValueManager | JniSurfacedPeerLocations.CurrentThread | JniSurfacedPeerLocations.AllThreads);
131+
132+
[Experimental ("JI9999")]
133+
public List<JniSurfacedPeerInfo> GetRegistrationScopePeers (JniSurfacedPeerLocations locations)
134+
{
135+
List<JniSurfacedPeerInfo>? peers = null;
136+
if (locations.HasFlag (JniSurfacedPeerLocations.ValueManager)) {
137+
peers = GetSurfacedPeers ();
138+
for (int i = 0; i < peers.Count; ++i) {
139+
peers [i].Location = JniSurfacedPeerLocations.ValueManager;
140+
}
141+
}
142+
peers ??= new ();
143+
return AddRegistrationScopePeers (peers, locations);
144+
}
145+
146+
[Experimental ("JI9999")]
147+
protected List<JniSurfacedPeerInfo> AddRegistrationScopePeers (List<JniSurfacedPeerInfo> peers, JniSurfacedPeerLocations locations)
148+
{
149+
if (locations.HasFlag (JniSurfacedPeerLocations.CurrentThread)) {
150+
AddPeers (JniEnvironment.CurrentInfo.Scopes, JniSurfacedPeerLocations.CurrentThread);
151+
}
152+
if (locations.HasFlag (JniSurfacedPeerLocations.AllThreads)) {
153+
var skipCurrentThread = locations.HasFlag (JniSurfacedPeerLocations.CurrentThread);
154+
foreach (var info in JniEnvironment.Info.Values) {
155+
if (skipCurrentThread && info == JniEnvironment.CurrentInfo) {
156+
continue;
157+
}
158+
AddPeers (info.Scopes, JniSurfacedPeerLocations.AllThreads);
159+
}
160+
}
161+
162+
return peers;
163+
164+
void AddPeers (List<PeerableCollection?>? scopes, JniSurfacedPeerLocations location)
165+
{
166+
if (scopes == null)
167+
return;
168+
169+
foreach (var scope in scopes) {
170+
if (scope == null) {
171+
continue;
172+
}
173+
foreach (var p in scope) {
174+
var info = new JniSurfacedPeerInfo (p.JniIdentityHashCode, CreateWeakReference (p)) {
175+
Location = location,
176+
};
177+
peers.Add (info);
178+
}
179+
}
180+
}
181+
}
182+
183+
protected static WeakReference<IJavaPeerable> CreateWeakReference (IJavaPeerable peer) =>
184+
new WeakReference<IJavaPeerable> (peer, trackResurrection: true);
185+
186+
187+
#pragma warning disable JI9999
188+
[Experimental ("JI9999")]
189+
protected bool TryAddPeerToRegistrationScope (IJavaPeerable value)
190+
{
191+
var scope = JniEnvironment.CurrentInfo.CurrentScope;
192+
if (scope == null) {
193+
return false;
194+
}
195+
scope.Add (value);
196+
return true;
197+
}
198+
199+
[Experimental ("JI9999")]
200+
protected bool TryRemovePeerFromRegistrationScopes (IJavaPeerable value)
201+
{
202+
var scopes = JniEnvironment.CurrentInfo.Scopes;
203+
if (scopes == null) {
204+
return false;
205+
}
206+
for (int i = scopes.Count - 1; i >= 0; --i) {
207+
var scope = scopes [i];
208+
if (scope == null) {
209+
continue;
210+
}
211+
if (!scope.Contains (value.JniIdentityHashCode)) {
212+
continue;
213+
}
214+
if (scope.TryGetValue (value.JniIdentityHashCode, out var peer) &&
215+
object.ReferenceEquals (peer, value)) {
216+
scope.Remove (value);
217+
return true;
218+
}
219+
}
220+
return false;
221+
}
222+
#pragma warning restore JI9999
223+
92224
public abstract void ActivatePeer (IJavaPeerable? self, JniObjectReference reference, ConstructorInfo cinfo, object? []? argumentValues);
93225

94226
public void ConstructPeer (IJavaPeerable peer, ref JniObjectReference reference, JniObjectReferenceOptions options)
@@ -210,6 +342,28 @@ public virtual void DisposePeerUnlessReferenced (IJavaPeerable value)
210342

211343
public abstract IJavaPeerable? PeekPeer (JniObjectReference reference);
212344

345+
#pragma warning disable JI9999
346+
[Experimental ("JI9999")]
347+
protected IJavaPeerable? TryPeekPeerFromRegistrationScopes (JniObjectReference reference, int identityHashCode)
348+
{
349+
var scopes = JniEnvironment.CurrentInfo.Scopes;
350+
if (scopes == null) {
351+
return null;
352+
}
353+
for (int i = scopes.Count - 1; i >= 0; --i) {
354+
var scope = scopes [i];
355+
if (scope == null) {
356+
continue;
357+
}
358+
var peer = scope.GetPeerable (reference, identityHashCode);
359+
if (peer != null) {
360+
return peer;
361+
}
362+
}
363+
return null;
364+
}
365+
#pragma warning restore JI9999
366+
213367
public object? PeekValue (JniObjectReference reference)
214368
{
215369
if (disposed)
@@ -712,6 +866,29 @@ protected virtual JniValueMarshaler GetValueMarshalerCore (Type type)
712866
{
713867
return ProxyValueMarshaler.Instance;
714868
}
869+
870+
871+
[return: NotNullIfNotNull ("value")]
872+
internal JavaProxyObject? GetProxy (object? value)
873+
{
874+
if (value == null)
875+
return null;
876+
877+
lock (ProxyValues) {
878+
if (ProxyValues.TryGetValue (value, out var proxy))
879+
return proxy;
880+
proxy = new JavaProxyObject (value);
881+
ProxyValues.Add (value, proxy);
882+
return proxy;
883+
}
884+
}
885+
886+
internal void RemoveProxy (object value)
887+
{
888+
lock (ProxyValues) {
889+
ProxyValues.Remove (value);
890+
}
891+
}
715892
}
716893
}
717894

@@ -927,8 +1104,8 @@ public override JniValueMarshalerState CreateGenericObjectReferenceArgumentState
9271104
return new JniValueMarshalerState (s, vm);
9281105
}
9291106

930-
var p = JavaProxyObject.GetProxy (value);
931-
return new JniValueMarshalerState (p!.PeerReference.NewLocalRef ());
1107+
var p = JniRuntime.CurrentRuntime.ValueManager.GetProxy (value);
1108+
return new JniValueMarshalerState (p.PeerReference.NewLocalRef ());
9321109
}
9331110

9341111
public override void DestroyGenericArgumentState (object? value, ref JniValueMarshalerState state, ParameterAttributes synchronize)
+26
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,29 @@
11
#nullable enable
22
static Java.Interop.JavaPeerableExtensions.TryJavaCast<TResult>(this Java.Interop.IJavaPeerable? self, out TResult? result) -> bool
33
static Java.Interop.JavaPeerableExtensions.JavaAs<TResult>(this Java.Interop.IJavaPeerable? self) -> TResult?
4+
Java.Interop.JavaPeerableRegistrationScope
5+
Java.Interop.JavaPeerableRegistrationScope.JavaPeerableRegistrationScope() -> void
6+
Java.Interop.JavaPeerableRegistrationScope.JavaPeerableRegistrationScope(Java.Interop.JavaPeerableRegistrationScopeCleanup cleanup) -> void
7+
Java.Interop.JavaPeerableRegistrationScope.Dispose() -> void
8+
Java.Interop.JavaPeerableRegistrationScopeCleanup
9+
Java.Interop.JavaPeerableRegistrationScopeCleanup.Dispose = 1 -> Java.Interop.JavaPeerableRegistrationScopeCleanup
10+
Java.Interop.JavaPeerableRegistrationScopeCleanup.RegisterWithManager = 0 -> Java.Interop.JavaPeerableRegistrationScopeCleanup
11+
Java.Interop.JavaPeerableRegistrationScopeCleanup.Release = 2 -> Java.Interop.JavaPeerableRegistrationScopeCleanup
12+
Java.Interop.JniSurfacedPeerLocations
13+
Java.Interop.JniSurfacedPeerLocations.AllThreads = 4 -> Java.Interop.JniSurfacedPeerLocations
14+
Java.Interop.JniSurfacedPeerLocations.CurrentThread = 2 -> Java.Interop.JniSurfacedPeerLocations
15+
Java.Interop.JniSurfacedPeerLocations.ValueManager = 1 -> Java.Interop.JniSurfacedPeerLocations
16+
*REMOVED* Java.Interop.JniSurfacedPeerLocations.Everywhere = Java.Interop.JniSurfacedPeerLocations.AllThreads | Java.Interop.JniSurfacedPeerLocations.CurrentThread | Java.Interop.JniSurfacedPeerLocations.ValueManager -> Java.Interop.JniSurfacedPeerLocations
17+
Java.Interop.JniRuntime.JniValueManager.AddRegistrationScopePeers(System.Collections.Generic.List<Java.Interop.JniSurfacedPeerInfo!>! peers, Java.Interop.JniSurfacedPeerLocations locations) -> System.Collections.Generic.List<Java.Interop.JniSurfacedPeerInfo!>!
18+
Java.Interop.JniRuntime.JniValueManager.GetRegistrationScopePeers() -> System.Collections.Generic.List<Java.Interop.JniSurfacedPeerInfo!>!
19+
Java.Interop.JniRuntime.JniValueManager.GetRegistrationScopePeers(Java.Interop.JniSurfacedPeerLocations locations) -> System.Collections.Generic.List<Java.Interop.JniSurfacedPeerInfo!>!
20+
Java.Interop.JniRuntime.JniValueManager.TryAddPeerToRegistrationScope(Java.Interop.IJavaPeerable! value) -> bool
21+
Java.Interop.JniRuntime.JniValueManager.TryPeekPeerFromRegistrationScopes(Java.Interop.JniObjectReference reference, int identityHashCode) -> Java.Interop.IJavaPeerable?
22+
Java.Interop.JniRuntime.JniValueManager.TryRemovePeerFromRegistrationScopes(Java.Interop.IJavaPeerable! value) -> bool
23+
static Java.Interop.JniRuntime.JniValueManager.CreateWeakReference(Java.Interop.IJavaPeerable! peer) -> System.WeakReference<Java.Interop.IJavaPeerable!>!
24+
virtual Java.Interop.JniRuntime.JniValueManager.CanCollectPeers.get -> bool
25+
virtual Java.Interop.JniRuntime.JniValueManager.CanReleasePeers.get -> bool
26+
virtual Java.Interop.JniRuntime.JniValueManager.ReleasePeers() -> void
27+
virtual Java.Interop.JniRuntime.JniValueManager.SupportsPeerableRegistrationScopes.get -> bool
28+
Java.Interop.JniSurfacedPeerInfo.Location.get -> Java.Interop.JniSurfacedPeerLocations
29+
override Java.Interop.JniSurfacedPeerInfo.ToString() -> string!

‎src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs

+35-19
Original file line numberDiff line numberDiff line change
@@ -19,35 +19,31 @@ public override void WaitForGCBridgeProcessing ()
1919
{
2020
}
2121

22+
public override bool CanCollectPeers => false;
23+
public override bool CanReleasePeers => true;
24+
2225
public override void CollectPeers ()
26+
{
27+
if (RegisteredInstances == null)
28+
throw new ObjectDisposedException (nameof (ManagedValueManager));
29+
30+
throw new NotSupportedException ();
31+
}
32+
33+
public override void ReleasePeers ()
2334
{
2435
if (RegisteredInstances == null)
2536
throw new ObjectDisposedException (nameof (ManagedValueManager));
2637

2738
var peers = new List<IJavaPeerable> ();
2839

2940
lock (RegisteredInstances) {
30-
foreach (var ps in RegisteredInstances.Values) {
31-
foreach (var p in ps) {
32-
peers.Add (p);
33-
}
34-
}
3541
RegisteredInstances.Clear ();
3642
}
37-
List<Exception>? exceptions = null;
38-
foreach (var peer in peers) {
39-
try {
40-
peer.Dispose ();
41-
}
42-
catch (Exception e) {
43-
exceptions = exceptions ?? new List<Exception> ();
44-
exceptions.Add (e);
45-
}
46-
}
47-
if (exceptions != null)
48-
throw new AggregateException ("Exceptions while collecting peers.", exceptions);
4943
}
5044

45+
public override bool SupportsPeerableRegistrationScopes => true;
46+
5147
public override void AddPeer (IJavaPeerable value)
5248
{
5349
if (RegisteredInstances == null)
@@ -64,6 +60,13 @@ public override void AddPeer (IJavaPeerable value)
6460
value.SetPeerReference (r.NewGlobalRef ());
6561
JniObjectReference.Dispose (ref r, JniObjectReferenceOptions.CopyAndDispose);
6662
}
63+
64+
#pragma warning disable JI9999
65+
if (TryAddPeerToRegistrationScope (value)) {
66+
return;
67+
}
68+
#pragma warning restore JI9999
69+
6770
int key = value.JniIdentityHashCode;
6871
lock (RegisteredInstances) {
6972
List<IJavaPeerable>? peers;
@@ -120,9 +123,16 @@ void WarnNotReplacing (int key, IJavaPeerable ignoreValue, IJavaPeerable keepVal
120123

121124
if (!reference.IsValid)
122125
return null;
123-
126+
124127
int key = GetJniIdentityHashCode (reference);
125128

129+
#pragma warning disable JI9999
130+
var peer = TryPeekPeerFromRegistrationScopes (reference, key);
131+
if (peer != null) {
132+
return peer;
133+
}
134+
#pragma warning restore JI9999
135+
126136
lock (RegisteredInstances) {
127137
List<IJavaPeerable>? peers;
128138
if (!RegisteredInstances.TryGetValue (key, out peers))
@@ -147,6 +157,12 @@ public override void RemovePeer (IJavaPeerable value)
147157
if (value == null)
148158
throw new ArgumentNullException (nameof (value));
149159

160+
#pragma warning disable JI9999
161+
if (TryRemovePeerFromRegistrationScopes (value)) {
162+
return;
163+
}
164+
#pragma warning restore JI9999
165+
150166
int key = value.JniIdentityHashCode;
151167
lock (RegisteredInstances) {
152168
List<IJavaPeerable>? peers;
@@ -248,7 +264,7 @@ public override List<JniSurfacedPeerInfo> GetSurfacedPeers ()
248264
var peers = new List<JniSurfacedPeerInfo> (RegisteredInstances.Count);
249265
foreach (var e in RegisteredInstances) {
250266
foreach (var p in e.Value) {
251-
peers.Add (new JniSurfacedPeerInfo (e.Key, new WeakReference<IJavaPeerable> (p)));
267+
peers.Add (new JniSurfacedPeerInfo (e.Key, CreateWeakReference (p)));
252268
}
253269
}
254270
return peers;

‎tests/Java.Interop-Tests/Java.Interop-Tests.csproj

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
<PropertyGroup>
44
<TargetFramework>$(DotNetTargetFramework)</TargetFramework>
55
<IsPackable>false</IsPackable>
6+
<SignAssembly>true</SignAssembly>
7+
<AssemblyOriginatorKeyFile>..\..\product.snk</AssemblyOriginatorKeyFile>
68
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
79
<DefineConstants>$(DefineConstants);NO_MARSHAL_MEMBER_BUILDER_SUPPORT;NO_GC_BRIDGE_SUPPORT</DefineConstants>
810
</PropertyGroup>
@@ -50,6 +52,7 @@
5052
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\AndroidInterface.java" />
5153
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\DesugarAndroidInterface$_CC.java" />
5254
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\SelfRegistration.java" />
55+
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\Singleton.java" />
5356
<JavaInteropTestJar Include="$(MSBuildThisFileDirectory)java\net\dot\jni\test\TestType.java" />
5457
</ItemGroup>
5558

‎tests/Java.Interop-Tests/Java.Interop/JavaManagedGCBridgeTests.cs

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ public class JavaManagedGCBridgeTests : JavaVMFixture {
1515
[Test]
1616
public void CrossReferences ()
1717
{
18+
if (!JniEnvironment.Runtime.ValueManager.CanCollectPeers) {
19+
Assert.Ignore ();
20+
}
1821
using (var array = new JavaObjectArray<CrossReferenceBridge> (2)) {
1922
WeakReference<CrossReferenceBridge> root = null, child = null;
2023
var t = new Thread (() => SetupLinks (array, out root, out child));

‎tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ protected override void Dispose (bool disposing)
240240
class MyDisposableObject : JavaObject {
241241
internal const string JniTypeName = "net/dot/jni/test/MyDisposableObject";
242242

243-
bool _isDisposed;
243+
internal bool _isDisposed;
244244

245245
public MyDisposableObject ()
246246
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System;
2+
3+
using Java.Interop;
4+
5+
namespace Java.InteropTests
6+
{
7+
[JniTypeSignature (JavaSingleton.JniTypeName, GenerateJavaPeer=false)]
8+
public sealed class JavaSingleton : JavaObject
9+
{
10+
internal const string JniTypeName = "net/dot/jni/test/Singleton";
11+
12+
readonly static JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (JavaSingleton));
13+
14+
public override JniPeerMembers JniPeerMembers {
15+
get {return _members;}
16+
}
17+
18+
public bool disposed;
19+
20+
internal JavaSingleton (ref JniObjectReference reference, JniObjectReferenceOptions options)
21+
: base (ref reference, options)
22+
{
23+
}
24+
25+
protected override void Dispose (bool disposing)
26+
{
27+
disposed = disposed || disposing;
28+
base.Dispose (disposing);
29+
}
30+
31+
public static unsafe JavaSingleton Singleton {
32+
get {
33+
var o = _members.StaticMethods.InvokeObjectMethod ("getSingleton.()Lnet/dot/jni/test/Singleton;", null);
34+
return JniEnvironment.Runtime.ValueManager.GetValue<JavaSingleton> (ref o, JniObjectReferenceOptions.CopyAndDispose);
35+
}
36+
}
37+
}
38+
}

‎tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs

+424
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package net.dot.jni.test;
2+
3+
public final class Singleton {
4+
5+
static final Singleton singleton = new Singleton ();
6+
7+
private Singleton() {
8+
}
9+
10+
public static Singleton getSingleton() {
11+
return singleton;
12+
}
13+
}

0 commit comments

Comments
 (0)
Please sign in to comment.