diff --git a/src/Java.Interop/Java.Interop/JavaPeerableRegistrationScope.cs b/src/Java.Interop/Java.Interop/JavaPeerableRegistrationScope.cs new file mode 100644 index 000000000..e84ab8850 --- /dev/null +++ b/src/Java.Interop/Java.Interop/JavaPeerableRegistrationScope.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Java.Interop { + [Experimental ("JI9999")] + public enum JavaPeerableRegistrationScopeCleanup { + RegisterWithManager, + Dispose, + Release, + } + + [Experimental ("JI9999")] + public ref struct JavaPeerableRegistrationScope { + PeerableCollection? scope; + bool disposed; + + public JavaPeerableRegistrationScope (JavaPeerableRegistrationScopeCleanup cleanup) + { + scope = JniEnvironment.CurrentInfo.BeginRegistrationScope (cleanup); + disposed = false; + } + + public void Dispose () + { + if (disposed) { + return; + } + disposed = true; + JniEnvironment.CurrentInfo.EndRegistrationScope (scope); + scope = null; + } + } +} diff --git a/src/Java.Interop/Java.Interop/JavaProxyObject.cs b/src/Java.Interop/Java.Interop/JavaProxyObject.cs index 909629b18..03170eacf 100644 --- a/src/Java.Interop/Java.Interop/JavaProxyObject.cs +++ b/src/Java.Interop/Java.Interop/JavaProxyObject.cs @@ -13,7 +13,6 @@ sealed class JavaProxyObject : JavaObject, IEquatable internal const string JniTypeName = "net/dot/jni/internal/JavaProxyObject"; static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (JavaProxyObject)); - static readonly ConditionalWeakTable CachedValues = new ConditionalWeakTable (); [JniAddNativeMethodRegistrationAttribute] static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args) @@ -29,10 +28,8 @@ public override JniPeerMembers JniPeerMembers { } } - JavaProxyObject (object value) + internal JavaProxyObject (object value) { - if (value == null) - throw new ArgumentNullException (nameof (value)); Value = value; } @@ -57,19 +54,10 @@ public override bool Equals (object? obj) return Value.ToString (); } - [return: NotNullIfNotNull ("object")] - public static JavaProxyObject? GetProxy (object value) + protected override void Dispose (bool disposing) { - if (value == null) - return null; - - lock (CachedValues) { - if (CachedValues.TryGetValue (value, out var proxy)) - return proxy; - proxy = new JavaProxyObject (value); - CachedValues.Add (value, proxy); - return proxy; - } + base.Dispose (disposing); + JniEnvironment.Runtime.ValueManager.RemoveProxy (Value); } // TODO: Keep in sync with the code generated by ExportedMemberBuilder diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.cs b/src/Java.Interop/Java.Interop/JniEnvironment.cs index 2b1bd5cdd..b163c5e77 100644 --- a/src/Java.Interop/Java.Interop/JniEnvironment.cs +++ b/src/Java.Interop/Java.Interop/JniEnvironment.cs @@ -2,10 +2,13 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text; using System.Threading; namespace Java.Interop { @@ -209,6 +212,8 @@ sealed class JniEnvironmentInfo : IDisposable { bool disposed; JniRuntime? runtime; + List? scopes; + public int LocalReferenceCount {get; internal set;} public bool WithinNewObjectScope {get; set;} public JniRuntime Runtime { @@ -243,6 +248,11 @@ public bool IsValid { get {return Runtime != null && environmentPointer != IntPtr.Zero;} } + public List? + Scopes => scopes; + public PeerableCollection? CurrentScope => + scopes == null ? null : scopes [scopes.Count-1]; + public JniEnvironmentInfo () { Runtime = JniRuntime.CurrentRuntime; @@ -293,6 +303,44 @@ public void Dispose () disposed = true; } +#pragma warning disable JI9999 + public PeerableCollection? BeginRegistrationScope (JavaPeerableRegistrationScopeCleanup cleanup) + { + if (cleanup != JavaPeerableRegistrationScopeCleanup.RegisterWithManager && + !Runtime.ValueManager.SupportsPeerableRegistrationScopes) { + throw new NotSupportedException ("Peerable registration scopes are not supported by this runtime."); + } + scopes ??= new List (); + if (cleanup == JavaPeerableRegistrationScopeCleanup.RegisterWithManager) { + scopes.Add (null); + return null; + } + var scope = new PeerableCollection (cleanup); + scopes.Add (scope); + return scope; + } + + public void EndRegistrationScope (PeerableCollection? scope) + { + Debug.Assert (scopes != null); + if (scopes == null) { + return; + } + + for (int i = scopes.Count; i > 0; --i) { + var s = scopes [i - 1]; + if (s == scope) { + scopes.RemoveAt (i - 1); + break; + } + } + if (scopes.Count == 0) { + scopes = null; + } + scope?.DisposeScope (); + } +#pragma warning restore JI9999 + #if FEATURE_JNIENVIRONMENT_SAFEHANDLES internal List> LocalReferences = new List> () { new List (), @@ -309,5 +357,82 @@ static unsafe JniEnvironmentInvoker CreateInvoker (IntPtr handle) } #endif // !FEATURE_JNIENVIRONMENT_JI_PINVOKES } + +#pragma warning disable JI9999 + sealed class PeerableCollection : KeyedCollection { + public JavaPeerableRegistrationScopeCleanup Cleanup { get; } + + public PeerableCollection (JavaPeerableRegistrationScopeCleanup cleanup) + { + Cleanup = cleanup; + } + + protected override int GetKeyForItem (IJavaPeerable item) => item.JniIdentityHashCode; + + public IJavaPeerable? GetPeerable (JniObjectReference reference, int identityHashCode) + { + if (!reference.IsValid) { + return null; + } + if (TryGetValue (identityHashCode, out var p) && + JniEnvironment.Types.IsSameObject (reference, p.PeerReference)) { + return p; + } + return null; + } + + public void DisposeScope () + { + Console.Error.WriteLine ($"# jonp: DisposeScope: {Cleanup}"); + Debug.Assert (Cleanup != JavaPeerableRegistrationScopeCleanup.RegisterWithManager); + switch (Cleanup) { + case JavaPeerableRegistrationScopeCleanup.Dispose: + List? exceptions = null; + foreach (var p in this) { + DisposePeer (p, ref exceptions); + } + Clear (); + if (exceptions != null) { + throw new AggregateException ("Exceptions while disposing peers.", exceptions); + } + break; + case JavaPeerableRegistrationScopeCleanup.Release: + Clear (); + break; + case JavaPeerableRegistrationScopeCleanup.RegisterWithManager: + default: + throw new NotSupportedException ($"Unsupported scope cleanup value: {Cleanup}"); + } + + [SuppressMessage ("Design", "CA1031:Do not catch general exception types", + Justification = "Exceptions are bundled into an AggregateException and rethrown")] + static void DisposePeer (IJavaPeerable peer, ref List? exceptions) + { + try { + Console.Error.WriteLine ($"# jonp: DisposeScope: disposing of: {peer} {peer.PeerReference}"); + peer.Dispose (); + } catch (Exception e) { + exceptions ??= new (); + exceptions.Add (e); + Trace.WriteLine (e); + } + } + } + + public override string ToString () + { + var c = (Collection) this; + var s = new StringBuilder (); + s.Append ("PeerableCollection[").Append (Count).Append ("]("); + for (int i = 0; i < Count; ++i ) { + s.AppendLine (); + var e = c [i]; + s.Append ($" [{i}] hash={e.JniIdentityHashCode} ref={e.PeerReference} type={e.GetType ().ToString ()} value=`{e.ToString ()}`"); + } + s.Append (")"); + return s.ToString (); + } + } +#pragma warning restore JI9999 } diff --git a/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs b/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs index 88f973ec0..c7716899b 100644 --- a/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs +++ b/src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs @@ -14,16 +14,41 @@ namespace Java.Interop { + [Experimental ("JI9999")] + [Flags] + public enum JniSurfacedPeerLocations { + ValueManager = 1 << 0, + CurrentThread = 1 << 1, + AllThreads = 1 << 2, + // Everywhere = ValueManager | CurrentThread | AllThreads, + // Can't add, because PublicAPI analyzers report RS0016 + RS0017 + } + public class JniSurfacedPeerInfo { public int JniIdentityHashCode {get; private set;} public WeakReference SurfacedPeer {get; private set;} + [Experimental ("JI9999")] + public JniSurfacedPeerLocations Location {get; internal set;} + public JniSurfacedPeerInfo (int jniIdentityHashCode, WeakReference surfacedPeer) { JniIdentityHashCode = jniIdentityHashCode; SurfacedPeer = surfacedPeer; } + + public override string ToString () + { +#pragma warning disable JI9999 + if (SurfacedPeer.TryGetTarget (out var target) && target != null) { + return $"(JniSurfacedPeerInfo JniIdentityHashCode={JniIdentityHashCode} Location={Location} " + + $"SurfacedPeer=({target.GetType ().FullName} {target.PeerReference} {target.ToString ()})" + + ")"; + } + return $"(JniSurfacedPeerInfo JniIdentityHashCode={JniIdentityHashCode} Location={Location})"; +#pragma warning restore JI9999 + } } partial class JniRuntime @@ -32,7 +57,7 @@ partial class CreationOptions { public JniValueManager? ValueManager {get; set;} } - JniValueManager? valueManager; + internal JniValueManager? valueManager; public JniValueManager ValueManager { get => valueManager ?? throw new NotSupportedException (); } @@ -52,6 +77,8 @@ public abstract partial class JniValueManager : ISetRuntime, IDisposable { internal const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; internal const DynamicallyAccessedMemberTypes ConstructorsAndInterfaces = Constructors | DynamicallyAccessedMemberTypes.Interfaces; + readonly ConditionalWeakTable ProxyValues = new (); + JniRuntime? runtime; bool disposed; public JniRuntime Runtime { @@ -77,10 +104,19 @@ protected virtual void Dispose (bool disposing) disposed = true; } + public virtual bool CanCollectPeers => false; + public virtual bool CanReleasePeers => false; + public virtual bool SupportsPeerableRegistrationScopes => false; + public abstract void WaitForGCBridgeProcessing (); public abstract void CollectPeers (); + public virtual void ReleasePeers () + { + throw new NotImplementedException (); + } + public abstract void AddPeer (IJavaPeerable value); public abstract void RemovePeer (IJavaPeerable value); @@ -89,6 +125,102 @@ protected virtual void Dispose (bool disposing) public abstract List GetSurfacedPeers (); + [Experimental ("JI9999")] + public List GetRegistrationScopePeers () + => GetRegistrationScopePeers (JniSurfacedPeerLocations.ValueManager | JniSurfacedPeerLocations.CurrentThread | JniSurfacedPeerLocations.AllThreads); + + [Experimental ("JI9999")] + public List GetRegistrationScopePeers (JniSurfacedPeerLocations locations) + { + List? peers = null; + if (locations.HasFlag (JniSurfacedPeerLocations.ValueManager)) { + peers = GetSurfacedPeers (); + for (int i = 0; i < peers.Count; ++i) { + peers [i].Location = JniSurfacedPeerLocations.ValueManager; + } + } + peers ??= new (); + return AddRegistrationScopePeers (peers, locations); + } + + [Experimental ("JI9999")] + protected List AddRegistrationScopePeers (List peers, JniSurfacedPeerLocations locations) + { + if (locations.HasFlag (JniSurfacedPeerLocations.CurrentThread)) { + AddPeers (JniEnvironment.CurrentInfo.Scopes, JniSurfacedPeerLocations.CurrentThread); + } + if (locations.HasFlag (JniSurfacedPeerLocations.AllThreads)) { + var skipCurrentThread = locations.HasFlag (JniSurfacedPeerLocations.CurrentThread); + foreach (var info in JniEnvironment.Info.Values) { + if (skipCurrentThread && info == JniEnvironment.CurrentInfo) { + continue; + } + AddPeers (info.Scopes, JniSurfacedPeerLocations.AllThreads); + } + } + + return peers; + + void AddPeers (List? scopes, JniSurfacedPeerLocations location) + { + if (scopes == null) + return; + + foreach (var scope in scopes) { + if (scope == null) { + continue; + } + foreach (var p in scope) { + var info = new JniSurfacedPeerInfo (p.JniIdentityHashCode, CreateWeakReference (p)) { + Location = location, + }; + peers.Add (info); + } + } + } + } + + protected static WeakReference CreateWeakReference (IJavaPeerable peer) => + new WeakReference (peer, trackResurrection: true); + + +#pragma warning disable JI9999 + [Experimental ("JI9999")] + protected bool TryAddPeerToRegistrationScope (IJavaPeerable value) + { + var scope = JniEnvironment.CurrentInfo.CurrentScope; + if (scope == null) { + return false; + } + scope.Add (value); + return true; + } + + [Experimental ("JI9999")] + protected bool TryRemovePeerFromRegistrationScopes (IJavaPeerable value) + { + var scopes = JniEnvironment.CurrentInfo.Scopes; + if (scopes == null) { + return false; + } + for (int i = scopes.Count - 1; i >= 0; --i) { + var scope = scopes [i]; + if (scope == null) { + continue; + } + if (!scope.Contains (value.JniIdentityHashCode)) { + continue; + } + if (scope.TryGetValue (value.JniIdentityHashCode, out var peer) && + object.ReferenceEquals (peer, value)) { + scope.Remove (value); + return true; + } + } + return false; + } +#pragma warning restore JI9999 + public abstract void ActivatePeer (IJavaPeerable? self, JniObjectReference reference, ConstructorInfo cinfo, object? []? argumentValues); public void ConstructPeer (IJavaPeerable peer, ref JniObjectReference reference, JniObjectReferenceOptions options) @@ -210,6 +342,28 @@ public virtual void DisposePeerUnlessReferenced (IJavaPeerable value) public abstract IJavaPeerable? PeekPeer (JniObjectReference reference); +#pragma warning disable JI9999 + [Experimental ("JI9999")] + protected IJavaPeerable? TryPeekPeerFromRegistrationScopes (JniObjectReference reference, int identityHashCode) + { + var scopes = JniEnvironment.CurrentInfo.Scopes; + if (scopes == null) { + return null; + } + for (int i = scopes.Count - 1; i >= 0; --i) { + var scope = scopes [i]; + if (scope == null) { + continue; + } + var peer = scope.GetPeerable (reference, identityHashCode); + if (peer != null) { + return peer; + } + } + return null; + } +#pragma warning restore JI9999 + public object? PeekValue (JniObjectReference reference) { if (disposed) @@ -712,6 +866,29 @@ protected virtual JniValueMarshaler GetValueMarshalerCore (Type type) { return ProxyValueMarshaler.Instance; } + + + [return: NotNullIfNotNull ("value")] + internal JavaProxyObject? GetProxy (object? value) + { + if (value == null) + return null; + + lock (ProxyValues) { + if (ProxyValues.TryGetValue (value, out var proxy)) + return proxy; + proxy = new JavaProxyObject (value); + ProxyValues.Add (value, proxy); + return proxy; + } + } + + internal void RemoveProxy (object value) + { + lock (ProxyValues) { + ProxyValues.Remove (value); + } + } } } @@ -927,8 +1104,8 @@ public override JniValueMarshalerState CreateGenericObjectReferenceArgumentState return new JniValueMarshalerState (s, vm); } - var p = JavaProxyObject.GetProxy (value); - return new JniValueMarshalerState (p!.PeerReference.NewLocalRef ()); + var p = JniRuntime.CurrentRuntime.ValueManager.GetProxy (value); + return new JniValueMarshalerState (p.PeerReference.NewLocalRef ()); } public override void DestroyGenericArgumentState (object? value, ref JniValueMarshalerState state, ParameterAttributes synchronize) diff --git a/src/Java.Interop/PublicAPI.Unshipped.txt b/src/Java.Interop/PublicAPI.Unshipped.txt index 3d4565551..b6c1d79c0 100644 --- a/src/Java.Interop/PublicAPI.Unshipped.txt +++ b/src/Java.Interop/PublicAPI.Unshipped.txt @@ -1,3 +1,29 @@ #nullable enable static Java.Interop.JavaPeerableExtensions.TryJavaCast(this Java.Interop.IJavaPeerable? self, out TResult? result) -> bool static Java.Interop.JavaPeerableExtensions.JavaAs(this Java.Interop.IJavaPeerable? self) -> TResult? +Java.Interop.JavaPeerableRegistrationScope +Java.Interop.JavaPeerableRegistrationScope.JavaPeerableRegistrationScope() -> void +Java.Interop.JavaPeerableRegistrationScope.JavaPeerableRegistrationScope(Java.Interop.JavaPeerableRegistrationScopeCleanup cleanup) -> void +Java.Interop.JavaPeerableRegistrationScope.Dispose() -> void +Java.Interop.JavaPeerableRegistrationScopeCleanup +Java.Interop.JavaPeerableRegistrationScopeCleanup.Dispose = 1 -> Java.Interop.JavaPeerableRegistrationScopeCleanup +Java.Interop.JavaPeerableRegistrationScopeCleanup.RegisterWithManager = 0 -> Java.Interop.JavaPeerableRegistrationScopeCleanup +Java.Interop.JavaPeerableRegistrationScopeCleanup.Release = 2 -> Java.Interop.JavaPeerableRegistrationScopeCleanup +Java.Interop.JniSurfacedPeerLocations +Java.Interop.JniSurfacedPeerLocations.AllThreads = 4 -> Java.Interop.JniSurfacedPeerLocations +Java.Interop.JniSurfacedPeerLocations.CurrentThread = 2 -> Java.Interop.JniSurfacedPeerLocations +Java.Interop.JniSurfacedPeerLocations.ValueManager = 1 -> Java.Interop.JniSurfacedPeerLocations +*REMOVED* Java.Interop.JniSurfacedPeerLocations.Everywhere = Java.Interop.JniSurfacedPeerLocations.AllThreads | Java.Interop.JniSurfacedPeerLocations.CurrentThread | Java.Interop.JniSurfacedPeerLocations.ValueManager -> Java.Interop.JniSurfacedPeerLocations +Java.Interop.JniRuntime.JniValueManager.AddRegistrationScopePeers(System.Collections.Generic.List! peers, Java.Interop.JniSurfacedPeerLocations locations) -> System.Collections.Generic.List! +Java.Interop.JniRuntime.JniValueManager.GetRegistrationScopePeers() -> System.Collections.Generic.List! +Java.Interop.JniRuntime.JniValueManager.GetRegistrationScopePeers(Java.Interop.JniSurfacedPeerLocations locations) -> System.Collections.Generic.List! +Java.Interop.JniRuntime.JniValueManager.TryAddPeerToRegistrationScope(Java.Interop.IJavaPeerable! value) -> bool +Java.Interop.JniRuntime.JniValueManager.TryPeekPeerFromRegistrationScopes(Java.Interop.JniObjectReference reference, int identityHashCode) -> Java.Interop.IJavaPeerable? +Java.Interop.JniRuntime.JniValueManager.TryRemovePeerFromRegistrationScopes(Java.Interop.IJavaPeerable! value) -> bool +static Java.Interop.JniRuntime.JniValueManager.CreateWeakReference(Java.Interop.IJavaPeerable! peer) -> System.WeakReference! +virtual Java.Interop.JniRuntime.JniValueManager.CanCollectPeers.get -> bool +virtual Java.Interop.JniRuntime.JniValueManager.CanReleasePeers.get -> bool +virtual Java.Interop.JniRuntime.JniValueManager.ReleasePeers() -> void +virtual Java.Interop.JniRuntime.JniValueManager.SupportsPeerableRegistrationScopes.get -> bool +Java.Interop.JniSurfacedPeerInfo.Location.get -> Java.Interop.JniSurfacedPeerLocations +override Java.Interop.JniSurfacedPeerInfo.ToString() -> string! diff --git a/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs b/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs index 4016909b2..bd3fd9a5d 100644 --- a/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs +++ b/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs @@ -19,7 +19,18 @@ public override void WaitForGCBridgeProcessing () { } + public override bool CanCollectPeers => false; + public override bool CanReleasePeers => true; + public override void CollectPeers () + { + if (RegisteredInstances == null) + throw new ObjectDisposedException (nameof (ManagedValueManager)); + + throw new NotSupportedException (); + } + + public override void ReleasePeers () { if (RegisteredInstances == null) throw new ObjectDisposedException (nameof (ManagedValueManager)); @@ -27,27 +38,12 @@ public override void CollectPeers () var peers = new List (); lock (RegisteredInstances) { - foreach (var ps in RegisteredInstances.Values) { - foreach (var p in ps) { - peers.Add (p); - } - } RegisteredInstances.Clear (); } - List? exceptions = null; - foreach (var peer in peers) { - try { - peer.Dispose (); - } - catch (Exception e) { - exceptions = exceptions ?? new List (); - exceptions.Add (e); - } - } - if (exceptions != null) - throw new AggregateException ("Exceptions while collecting peers.", exceptions); } + public override bool SupportsPeerableRegistrationScopes => true; + public override void AddPeer (IJavaPeerable value) { if (RegisteredInstances == null) @@ -64,6 +60,13 @@ public override void AddPeer (IJavaPeerable value) value.SetPeerReference (r.NewGlobalRef ()); JniObjectReference.Dispose (ref r, JniObjectReferenceOptions.CopyAndDispose); } + +#pragma warning disable JI9999 + if (TryAddPeerToRegistrationScope (value)) { + return; + } +#pragma warning restore JI9999 + int key = value.JniIdentityHashCode; lock (RegisteredInstances) { List? peers; @@ -120,9 +123,16 @@ void WarnNotReplacing (int key, IJavaPeerable ignoreValue, IJavaPeerable keepVal if (!reference.IsValid) return null; - + int key = GetJniIdentityHashCode (reference); +#pragma warning disable JI9999 + var peer = TryPeekPeerFromRegistrationScopes (reference, key); + if (peer != null) { + return peer; + } +#pragma warning restore JI9999 + lock (RegisteredInstances) { List? peers; if (!RegisteredInstances.TryGetValue (key, out peers)) @@ -147,6 +157,12 @@ public override void RemovePeer (IJavaPeerable value) if (value == null) throw new ArgumentNullException (nameof (value)); +#pragma warning disable JI9999 + if (TryRemovePeerFromRegistrationScopes (value)) { + return; + } +#pragma warning restore JI9999 + int key = value.JniIdentityHashCode; lock (RegisteredInstances) { List? peers; @@ -248,7 +264,7 @@ public override List GetSurfacedPeers () var peers = new List (RegisteredInstances.Count); foreach (var e in RegisteredInstances) { foreach (var p in e.Value) { - peers.Add (new JniSurfacedPeerInfo (e.Key, new WeakReference (p))); + peers.Add (new JniSurfacedPeerInfo (e.Key, CreateWeakReference (p))); } } return peers; diff --git a/tests/Java.Interop-Tests/Java.Interop-Tests.csproj b/tests/Java.Interop-Tests/Java.Interop-Tests.csproj index 0677be887..fea3cbd90 100644 --- a/tests/Java.Interop-Tests/Java.Interop-Tests.csproj +++ b/tests/Java.Interop-Tests/Java.Interop-Tests.csproj @@ -3,6 +3,8 @@ $(DotNetTargetFramework) false + true + ..\..\product.snk true $(DefineConstants);NO_MARSHAL_MEMBER_BUILDER_SUPPORT;NO_GC_BRIDGE_SUPPORT @@ -50,6 +52,7 @@ + diff --git a/tests/Java.Interop-Tests/Java.Interop/JavaManagedGCBridgeTests.cs b/tests/Java.Interop-Tests/Java.Interop/JavaManagedGCBridgeTests.cs index 78150c279..9555839ab 100644 --- a/tests/Java.Interop-Tests/Java.Interop/JavaManagedGCBridgeTests.cs +++ b/tests/Java.Interop-Tests/Java.Interop/JavaManagedGCBridgeTests.cs @@ -15,6 +15,9 @@ public class JavaManagedGCBridgeTests : JavaVMFixture { [Test] public void CrossReferences () { + if (!JniEnvironment.Runtime.ValueManager.CanCollectPeers) { + Assert.Ignore (); + } using (var array = new JavaObjectArray (2)) { WeakReference root = null, child = null; var t = new Thread (() => SetupLinks (array, out root, out child)); diff --git a/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs b/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs index d90d5846f..613430592 100644 --- a/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs +++ b/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs @@ -240,7 +240,7 @@ protected override void Dispose (bool disposing) class MyDisposableObject : JavaObject { internal const string JniTypeName = "net/dot/jni/test/MyDisposableObject"; - bool _isDisposed; + internal bool _isDisposed; public MyDisposableObject () { diff --git a/tests/Java.Interop-Tests/Java.Interop/JavaSingleton.cs b/tests/Java.Interop-Tests/Java.Interop/JavaSingleton.cs new file mode 100644 index 000000000..c3d810018 --- /dev/null +++ b/tests/Java.Interop-Tests/Java.Interop/JavaSingleton.cs @@ -0,0 +1,38 @@ +using System; + +using Java.Interop; + +namespace Java.InteropTests +{ + [JniTypeSignature (JavaSingleton.JniTypeName, GenerateJavaPeer=false)] + public sealed class JavaSingleton : JavaObject + { + internal const string JniTypeName = "net/dot/jni/test/Singleton"; + + readonly static JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (JavaSingleton)); + + public override JniPeerMembers JniPeerMembers { + get {return _members;} + } + + public bool disposed; + + internal JavaSingleton (ref JniObjectReference reference, JniObjectReferenceOptions options) + : base (ref reference, options) + { + } + + protected override void Dispose (bool disposing) + { + disposed = disposed || disposing; + base.Dispose (disposing); + } + + public static unsafe JavaSingleton Singleton { + get { + var o = _members.StaticMethods.InvokeObjectMethod ("getSingleton.()Lnet/dot/jni/test/Singleton;", null); + return JniEnvironment.Runtime.ValueManager.GetValue (ref o, JniObjectReferenceOptions.CopyAndDispose); + } + } + } +} diff --git a/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs b/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs new file mode 100644 index 000000000..9854558a6 --- /dev/null +++ b/tests/Java.Interop-Tests/Java.Interop/JniRuntimeJniValueManagerContract.cs @@ -0,0 +1,424 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; + +using Java.Interop; + +using NUnit.Framework; + +#pragma warning disable JI9999 // Experimental + +namespace Java.InteropTests { + + // Modifies JniRuntime.valueManager instance field; can't be done in parallel + [NonParallelizable] + public abstract class JniRuntimeJniValueManagerContract : JavaVMFixture { + + protected abstract Type ValueManagerType { + get; + } + + protected virtual JniRuntime.JniValueManager CreateValueManager () + { + var manager = Activator.CreateInstance (ValueManagerType) as JniRuntime.JniValueManager; + return manager ?? throw new InvalidOperationException ($"Could not create instance of `{ValueManagerType}`!"); + } + +#pragma warning disable CS8618 + JniRuntime.JniValueManager systemManager; + JniRuntime.JniValueManager valueManager; +#pragma warning restore CS8618 + + [SetUp] + public void CreateVM () + { + systemManager = JniRuntime.CurrentRuntime.valueManager!; + valueManager = CreateValueManager (); + valueManager.OnSetRuntime (JniRuntime.CurrentRuntime); + JniRuntime.CurrentRuntime.valueManager = valueManager; + } + + [TearDown] + public void DestroyVM () + { + JniRuntime.CurrentRuntime.valueManager = systemManager; + systemManager = null!; + valueManager?.Dispose (); + valueManager = null!; + } + + [Test] + public void AddPeer () + { + } + + int GetRegistrationScopePeerCount () + { + return valueManager.GetRegistrationScopePeers ().Count; + } + + [Test] + public void AddPeer_NoDuplicates () + { + int startPeerCount = GetRegistrationScopePeerCount (); + using (var v = new MyDisposableObject ()) { + // MyDisposableObject ctor implicitly calls AddPeer(); + Assert.AreEqual (startPeerCount + 1, GetRegistrationScopePeerCount (), DumpPeers ()); + valueManager.AddPeer (v); + Assert.AreEqual (startPeerCount + 1, GetRegistrationScopePeerCount (), DumpPeers ()); + } + } + + [Test] + public void ConstructPeer_ImplicitViaBindingConstructor_PeerIsInSurfacedPeers () + { + int startPeerCount = GetRegistrationScopePeerCount (); + + var g = new GetThis (); + var surfaced = valueManager.GetRegistrationScopePeers (); + Assert.AreEqual (startPeerCount + 1, surfaced.Count); + + var found = false; + foreach (var pr in surfaced) { + if (!pr.SurfacedPeer.TryGetTarget (out var p)) + continue; + if (object.ReferenceEquals (g, p)) { + found = true; + } + } + Assert.IsTrue (found); + + var localRef = g.PeerReference.NewLocalRef (); + g.Dispose (); + Assert.AreEqual (startPeerCount, GetRegistrationScopePeerCount ()); + Assert.IsNull (valueManager.PeekPeer (localRef)); + JniObjectReference.Dispose (ref localRef); + } + + [Test] + public void ConstructPeer_ImplicitViaBindingMethod_PeerIsInSurfacedPeers () + { + int startPeerCount = GetRegistrationScopePeerCount (); + + var g = new GetThis (); + var surfaced = valueManager.GetRegistrationScopePeers (); + Assert.AreEqual (startPeerCount + 1, surfaced.Count); + + var found = false; + foreach (var pr in surfaced) { + if (!pr.SurfacedPeer.TryGetTarget (out var p)) + continue; + if (object.ReferenceEquals (g, p)) { + found = true; + } + } + Assert.IsTrue (found); + + var localRef = g.PeerReference.NewLocalRef (); + g.Dispose (); + Assert.AreEqual (startPeerCount, GetRegistrationScopePeerCount ()); + Assert.IsNull (valueManager.PeekPeer (localRef)); + JniObjectReference.Dispose (ref localRef); + } + + + [Test] + public void CollectPeers () + { + // TODO + } + + [Test] + public void CreateValue () + { + using (var o = new JavaObject ()) { + var r = o.PeerReference; + var x = (IJavaPeerable) valueManager.CreateValue (ref r, JniObjectReferenceOptions.Copy)!; + Assert.AreNotSame (o, x); + x.Dispose (); + + x = valueManager.CreateValue (ref r, JniObjectReferenceOptions.Copy); + Assert.AreNotSame (o, x); + x!.Dispose (); + } + } + + [Test] + public void GetValue_ReturnsAlias () + { + var local = new JavaObject (); + local.UnregisterFromRuntime (); + Assert.IsNull (valueManager.PeekValue (local.PeerReference)); + // GetObject must always return a value (unless handle is null, etc.). + // However, since we called local.UnregisterFromRuntime(), + // JniRuntime.PeekObject() is null (asserted above), but GetObject() must + // **still** return _something_. + // In this case, it returns an _alias_. + // TODO: "most derived type" alias generation. (Not relevant here, but...) + var p = local.PeerReference; + var alias = JniRuntime.CurrentRuntime.ValueManager.GetValue (ref p, JniObjectReferenceOptions.Copy); + Assert.AreNotSame (local, alias); + alias!.Dispose (); + local.Dispose (); + } + + [Test] + public void GetValue_ReturnsNullWithNullHandle () + { + var r = new JniObjectReference (); + var o = valueManager.GetValue (ref r, JniObjectReferenceOptions.Copy); + Assert.IsNull (o); + } + + [Test] + public void GetValue_ReturnsNullWithInvalidSafeHandle () + { + var invalid = new JniObjectReference (); + Assert.IsNull (valueManager.GetValue (ref invalid, JniObjectReferenceOptions.CopyAndDispose)); + } + + [Test] + public unsafe void GetValue_FindBestMatchType () + { +#if !NO_MARSHAL_MEMBER_BUILDER_SUPPORT + using (var t = new JniType (TestType.JniTypeName)) { + var c = t.GetConstructor ("()V"); + var o = t.NewObject (c, null); + using (var w = valueManager.GetValue (ref o, JniObjectReferenceOptions.CopyAndDispose)) { + Assert.AreEqual (typeof (TestType), w!.GetType ()); + Assert.IsTrue (((TestType) w).ExecutedActivationConstructor); + } + } +#endif // !NO_MARSHAL_MEMBER_BUILDER_SUPPORT + } + + [Test] + public void PeekPeer () + { + Assert.IsNull (valueManager.PeekPeer (new JniObjectReference ())); + + using (var v = new MyDisposableObject ()) { + Assert.IsNotNull (valueManager.PeekPeer (v.PeerReference)); + Assert.AreSame (v, valueManager.PeekPeer (v.PeerReference)); + } + } + + [Test] + public void PeekValue () + { + JniObjectReference lref; + using (var o = new JavaObject ()) { + lref = o.PeerReference.NewLocalRef (); + Assert.AreSame (o, valueManager.PeekValue (lref)); + } + // At this point, the Java-side object is kept alive by `lref`, + // but the wrapper instance has been disposed, and thus should + // be unregistered, and thus unfindable. + Assert.IsNull (valueManager.PeekValue (lref)); + JniObjectReference.Dispose (ref lref); + } + + [Test] + public void PeekValue_BoxedObjects () + { + var marshaler = valueManager.GetValueMarshaler (); + var ad = AppDomain.CurrentDomain; + + var proxy = marshaler.CreateGenericArgumentState (ad); + Assert.AreSame (ad, valueManager.PeekValue (proxy.ReferenceValue)); + marshaler.DestroyGenericArgumentState (ad, ref proxy); + + var ex = new InvalidOperationException ("boo!"); + proxy = marshaler.CreateGenericArgumentState (ex); + Assert.AreSame (ex, valueManager.PeekValue (proxy.ReferenceValue)); + marshaler.DestroyGenericArgumentState (ex, ref proxy); + } + + void AllNestedRegistrationScopeTests () + { + AddPeer (); + AddPeer_NoDuplicates (); + ConstructPeer_ImplicitViaBindingConstructor_PeerIsInSurfacedPeers (); + CreateValue (); + GetValue_FindBestMatchType (); + GetValue_ReturnsAlias (); + GetValue_ReturnsNullWithInvalidSafeHandle (); + GetValue_ReturnsNullWithNullHandle (); + PeekPeer (); + PeekValue (); + PeekValue_BoxedObjects (); + } + + [Test] + public void WithRegistrationScope_Dispose () + { + if (!valueManager.SupportsPeerableRegistrationScopes) { + Assert.Ignore (); + } + Assert.AreEqual (0, GetRegistrationScopePeerCount ()); + MyDisposableObject v; + using (new JavaPeerableRegistrationScope (JavaPeerableRegistrationScopeCleanup.Dispose)) { + v = new MyDisposableObject (); + Assert.IsFalse (v._isDisposed); + Assert.AreEqual (1, GetRegistrationScopePeerCount ()); + + var t = new Thread (() => { + // .Dispose and .Release result in thread-local registrations. + // Other threads won't be able to lookup these values via ValueManager + var peeked = valueManager.PeekPeer (v.PeerReference); + Assert.IsNull (peeked); + }); + t.Start (); + t.Join (); + } + Assert.AreEqual (0, GetRegistrationScopePeerCount ()); + Assert.IsTrue (v._isDisposed); + } + + [Test] + public void WithRegistrationScope_Dispose_All () + { + if (!valueManager.SupportsPeerableRegistrationScopes) { + Assert.Ignore (); + } + using (new JavaPeerableRegistrationScope (JavaPeerableRegistrationScopeCleanup.Dispose)) { + AllNestedRegistrationScopeTests (); + } + } + + [Test] + public void WithRegistrationScope_Dispose_Nested () + { + if (!valueManager.SupportsPeerableRegistrationScopes) { + Assert.Ignore (); + } + Assert.AreEqual (0, GetRegistrationScopePeerCount ()); + var u1 = (MyDisposableObject?) null; + using (var c1 = new JavaPeerableRegistrationScope (JavaPeerableRegistrationScopeCleanup.Dispose)) { + var b1 = JavaSingleton.Singleton; + var b2 = (JavaSingleton?) null; + var array = new JavaObjectArray (1); + u1 = new MyDisposableObject (); + array [0] = u1; + Assert.AreEqual (3, GetRegistrationScopePeerCount ()); + using (var c2 = new JavaPeerableRegistrationScope (JavaPeerableRegistrationScopeCleanup.Dispose)) { + // Creating a new scope means only contains newly created registrations; + // previously created registrations come from earlier scops. + b2 = JavaSingleton.Singleton; + Assert.AreSame (b1, b2); + Assert.AreEqual (3, GetRegistrationScopePeerCount ()); + + // Creating a new scope means that previously created "unbound" instances MUST be + // returned by JniValueManager.GetValue(), as they don't have an activation ctor + var u2 = array [0]; + Assert.AreSame (u1, u2); + Assert.AreEqual (3, GetRegistrationScopePeerCount (), DumpPeers ()); + } + Assert.IsFalse (b1.disposed); + Assert.AreEqual (3, GetRegistrationScopePeerCount (), DumpPeers ()); + } + Assert.AreEqual (0, GetRegistrationScopePeerCount ()); + Assert.IsTrue (u1._isDisposed); + } + + string DumpPeers () + { + return DumpPeers (valueManager.GetRegistrationScopePeers ()); + } + + static string DumpPeers (IEnumerable peers) + { + return string.Join ("," + Environment.NewLine, peers); + } + + [Test] + public void WithRegistrationScope_Release () + { + if (!valueManager.SupportsPeerableRegistrationScopes) { + Assert.Ignore (); + } + Assert.AreEqual (0, GetRegistrationScopePeerCount ()); + MyDisposableObject v; + using (new JavaPeerableRegistrationScope (JavaPeerableRegistrationScopeCleanup.Release)) { + v = new MyDisposableObject (); + Assert.IsFalse (v._isDisposed); + Assert.AreEqual (1, GetRegistrationScopePeerCount ()); + + var t = new Thread (() => { + // .Dispose and .Release result in thread-local registrations. + // Other threads won't be able to lookup these values via ValueManager + var peeked = valueManager.PeekPeer (v.PeerReference); + Assert.IsNull (peeked); + }); + t.Start (); + t.Join (); + } + Assert.AreEqual (0, GetRegistrationScopePeerCount ()); + Assert.IsFalse (v._isDisposed); + } + + [Test] + public void WithRegistrationScope_Release_All () + { + if (!valueManager.SupportsPeerableRegistrationScopes) { + Assert.Ignore (); + } + using (new JavaPeerableRegistrationScope (JavaPeerableRegistrationScopeCleanup.Release)) { + AllNestedRegistrationScopeTests (); + } + } + + [Test] + public void WithRegistrationScope_RegisterWithManager () + { + Assert.AreEqual (0, GetRegistrationScopePeerCount ()); + MyDisposableObject v; + using (new JavaPeerableRegistrationScope (JavaPeerableRegistrationScopeCleanup.RegisterWithManager)) { + v = new MyDisposableObject (); + Assert.IsFalse (v._isDisposed); + Assert.AreEqual (1, GetRegistrationScopePeerCount ()); + } + Assert.AreEqual (1, GetRegistrationScopePeerCount ()); + Assert.IsFalse (v._isDisposed); + } + + [Test] + public void WithRegistrationScope_RegisterWithManager_All () + { + using (new JavaPeerableRegistrationScope (JavaPeerableRegistrationScopeCleanup.RegisterWithManager)) { + AllNestedRegistrationScopeTests (); + } + } + + // also test: + // Singleton scenario + // Types w/o "activation" constructors -- need to support checking parent scopes + // nesting of scopes + // Adding an instance already added in a previous scope? + } + + public abstract class JniRuntimeJniValueManagerContract : JniRuntimeJniValueManagerContract { + + protected override Type ValueManagerType => typeof (T); + } + +#if !NETCOREAPP + [TestFixture] + public class JniRuntimeJniValueManagerContract_Mono : JniRuntimeJniValueManagerContract { + static Type MonoRuntimeValueManagerType = Type.GetType ("Java.Interop.MonoRuntimeValueManager, Java.Runtime.Environment", throwOnError:true)!; + + protected override Type ValueManagerType => MonoRuntimeValueManagerType; + } +#endif // !NETCOREAPP + + [TestFixture] + public class JniRuntimeJniValueManagerContract_NoGCIntegration : JniRuntimeJniValueManagerContract { + static Type ManagedValueManagerType = Type.GetType ("Java.Interop.ManagedValueManager, Java.Runtime.Environment", throwOnError:true)!; + + protected override Type ValueManagerType => ManagedValueManagerType; + } +} diff --git a/tests/Java.Interop-Tests/java/net/dot/jni/test/Singleton.java b/tests/Java.Interop-Tests/java/net/dot/jni/test/Singleton.java new file mode 100644 index 000000000..6c04455ed --- /dev/null +++ b/tests/Java.Interop-Tests/java/net/dot/jni/test/Singleton.java @@ -0,0 +1,13 @@ +package net.dot.jni.test; + +public final class Singleton { + + static final Singleton singleton = new Singleton (); + + private Singleton() { + } + + public static Singleton getSingleton() { + return singleton; + } +} diff --git a/tests/TestJVM/TestJVM.csproj b/tests/TestJVM/TestJVM.csproj index 5660c4e4a..2ab635532 100644 --- a/tests/TestJVM/TestJVM.csproj +++ b/tests/TestJVM/TestJVM.csproj @@ -4,6 +4,8 @@ $(DotNetTargetFramework) enable false + true + ..\..\product.snk