Skip to content

Commit

Permalink
[Java.Interop] Add JavaScope?
Browse files Browse the repository at this point in the history
Fixes: dotnet#4

Context: dotnet#426

Alternate names?

  * JavaPeerableCleanupPool
  * JavaPeerCleanup
  * JavaReferenceCleanup
  * JniPeerRegistrationScope

Issue dotnet#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 do 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 dotnet#4!

Add `JavaScope`, which introduces a scope-based mechanism to control
when `IJavaPeerable` instances are cleaned up:

	public enum JavaScopeCleanup {
	    RegisterWithManager,
	    Dispose,
	    Release,
	}

	public ref struct JavaScope {
	    public JavaScope(JavaScopeCleanup cleanup);
	    public void Dispose();
	}

`JavaScope` is a [`ref struct`][0], 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 `JavaScope` is created using
`JavaScopeCleanup.RegisterWithManager`, existing behavior is followed.
This is useful for nested scopes, should instances need to be
registered with `JniRuntime.ValueManager`.

If a `JavaScope` is created using `JavaScopeCleanup.Dispose` or
`JavaScopeCleanup.Release`, then:

 1. Object references created within the scope are "thread-local";
    they can be *used* by other threads, but
    `JniRuntime.JniValueManager.PeekPeer()` won't find existing values.

 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 JavaScope (JavaScopeCleanup.Dispose)) {
	    var singleton = JavaSingleton.Singleton;
	    // use singleton
	}
	// `singleton.Dispose()` is called at the end of the `using` block

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://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct#ref-struct
  • Loading branch information
jonpryor committed Jun 14, 2021
1 parent f4e68b5 commit a675a87
Show file tree
Hide file tree
Showing 19 changed files with 1,217 additions and 239 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
<?xml version="1.0"?>
<docs>
<member name="T:JniValueManager">
<summary>
Manages the mapping between Java instances and registered managed instances.
</summary>
<remarks>
<para>
<c>JniRuntime.JniValueManager</c> manages a mapping between Java instances,
and <see cref="T:IJavaPeerable" /> instances. A <c>IJavaPeerable</c> is
<i>registered</i> if it has been added to <c>JniRuntime.JniValueManager</c>.
Certain methods only deal with registered instances. Managed-to-Java instance
method invocation does not require the use of registered instances.
Java-to-Managed instance method invocation requires the use of registred
instances; if possible, a managed peer will be implicitly created.
See <see cref="M:GetValue" /> for details.
</para>
<list type="bullet">
<item><term>
Marshaling infrastructure:
<see cref="M:WaitForGCBridgeProcessing" />.
</term></item>
<item><term>
Lifetime management of all registered peers:
<see cref="M:CollectPeers" />,
<see cref="M:DisposePeers" />,
<see cref="M:ReleasePeers" />.
</term></item>
<item><term>
Registration management for individual peers:
<see cref="M:ActivatePeer" />,
<see cref="M:AddPeer" />,
<see cref="M:ConstructPeer" />,
<see cref="M:DisposePeer" />,
<see cref="M:RemovePeer" />.
</term></item>
<item><term>
Java-to-managed marshaling support:
<see cref="M:GetValue{T}" />,
<see cref="M:GetValue" />,
<see cref="M:GetValueMarshaler" />.
</term></item>
<item><term>
Managed-to-Java marshaling support:
<see cref="M:GetValueMarshaler" />.
</term></item>
</list>
<para>
Managed-to-Java marshaling support is handled via explicit usage,
or "generically" via <see cref="T:JniValueMarshaler" /> and
<see cref="T:JniValueMarshaler{T}" />.
</para>
<block subset="none" type="overrides">
All subclasses must be thread safe.
</block>
</remarks>
<threadsafe>This type is thread safe.</threadsafe>
</member>
<!--
Marshaling Infrastructure
-->
<member name="M:WaitForGCBridgeProcessing">
<summary>
Infrastructure. Called during Java-to-managed transitions.
</summary>
</member>
<!--
Global peer lifetime maangement
-->
<member name="P:CanCollectPeers">
<summary>
Whether or not <see cref="M:CollectPeers" /> is supported.
</summary>
<remarks>
<para>
When <c>CanCollectPeers</c> returns <c>false</c>, calls to
<see cref="M:CollectPeers" /> will throw
<see cref="T:System.NotSupportedException" />.
</para>
</remarks>
</member>
<member name="M:CollectPeers">
<summary>
Garbage collects all peer instances.
</summary>
<remarks>
<para>
When <c>CanCollectPeers</c> returns <c>false</c>, calls to
<see cref="M:CollectPeers" /> will throw
<see cref="T:System.NotSupportedException" />.
</para>
</remarks>
<exception cref="T:System.NotSupportedException">
Garbage collection of peers is not supported.
</exception>
<exception cref="T:System.ObjectDisposedException">
<see cref="M:Dispose()" /> has previously been invoked.
</exception>
</member>
<member name="M:CollectPeersCore">
<summary>
Garbage collects all peer instances.
</summary>
<remarks>
<block subset="none" type="overrides">
<para>The <c>CollectPeersCore</c> method will not be invoked
after <c>Dispose()</c> has been invoked.</para>
</block>
</remarks>
<exception cref="T:System.NotSupportedException">
Garbage collection of peers is not supported.
</exception>
</member>
<member name="M:DisposePeers">
<summary>
Dispose of all registered peer instances.
</summary>
<remarks>
<para>
Calls <see cref="M:System.IDisposable.Dispose" /> on all peer instances.
</para>
</remarks>
<exception cref="T:System.AggregateException">
Contains all exceptions thrown by registered instances when calling
<see cref="M:System.IDisposable.Dispose" />.
</exception>
<exception cref="T:System.ObjectDisposedException">
<see cref="M:Dispose" /> has previously been invoked.
</exception>
</member>
<member name="M:DisposePeersCore">
<summary>
Dispose of all registered peer instances.
</summary>
<remarks>
<block subset="none" type="overrides">
<para>
The <c>DisposePeersCore</c> method will not be invoked
after <see cref="M:Dispose" /> has been invoked.</para>
<para>Inheritors should invoke <see cref="M:System.IDisposable.Dispose" />
on all peer instances. Should any peer throw from the <c>Dispose</c>
invocation, then <c>DisposePeersCore</c> should capture all thrown
exceptions and re-raise them within a <see cref="T:System.AggregateException" />.
</para>
</block>
</remarks>
<exception cref="T:System.AggregateException">
Contains all exceptions thrown by <see cref="M:System.IDisposable.Dispose" />.
</exception>
</member>
<member name="M:ReleasePeers">
<summary>
Release all registered peer instances.
</summary>
<remarks>
<para>
The <c>JniValueManager</c> unregisters all peers.
Methods such as <see cref="M:PeekPeer" /> will not find return any peers.
</para>
<para>
Peer values may still be used, even if not referenced by a <c>JniValueManager</c>.
</para>
</remarks>
<exception cref="T:System.ObjectDisposedException">
<see cref="M:Dispose" /> has previously been invoked.
</exception>
</member>
<member name="M:ReleasePeersCore">
<summary>
Release all registered peer instances.
</summary>
<remarks>
<block subset="none" type="overrides">
<para>The <c>ReleasePeersCore</c> method will not be invoked
after <c>Dispose()</c> has been invoked.</para>
</block>
</remarks>
</member>
<!--
Lifetime management for individual peers
-->
<member name="M:AddPeer">
<summary>
Register a managed peer.
</summary>
<remarks>
<block subset="none" type="overrides">
<para>The <c>ReleasePeersCore</c> method will not be invoked
after <c>Dispose()</c> has been invoked.</para>
</block>
</remarks>
</member>
</docs>
2 changes: 2 additions & 0 deletions src/Java.Interop/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

[assembly: SuppressMessage ("Design", "CA1024:Use properties where appropriate", Justification = "<Pending>", Scope = "member", Target = "~M:Java.Interop.JniRuntime.GetRegisteredRuntimes()")]

[assembly: SuppressMessage ("Design", "CA1031:Do not catch general exception types", Justification = "Excceptions are bundled into an AggregateException and rethrown", Scope = "type", Target = "~M:Java.Interop.JavaScope.Dispose")]

[assembly: SuppressMessage ("Design", "CA1032:Implement standard exception constructors", Justification = "System.Runtime.Serialization.SerializationInfo doesn't exist in our targeted PCL profile, so we can't provide the (SerializationInfo, StreamingContext) constructor.", Scope = "type", Target = "~T:Java.Interop.JavaProxyThrowable")]
[assembly: SuppressMessage ("Design", "CA1032:Implement standard exception constructors", Justification = "System.Runtime.Serialization.SerializationInfo doesn't exist in our targeted PCL profile, so we can't provide the (SerializationInfo, StreamingContext) constructor.", Scope = "type", Target = "~T:Java.Interop.JniLocationException")]

Expand Down
20 changes: 6 additions & 14 deletions src/Java.Interop/Java.Interop/JavaProxyObject.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;

Expand Down Expand Up @@ -28,10 +29,8 @@ public override JniPeerMembers JniPeerMembers {
}
}

JavaProxyObject (object value)
internal JavaProxyObject (object value)
{
if (value == null)
throw new ArgumentNullException (nameof (value));
Value = value;
}

Expand All @@ -56,18 +55,11 @@ 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);
if (disposing) {
CachedValues.Remove (Value);
}
}

Expand Down
55 changes: 55 additions & 0 deletions src/Java.Interop/Java.Interop/JavaScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace Java.Interop {

public enum JavaScopeCleanup {
RegisterWithManager,
Dispose,
Release,
}

public ref struct JavaScope {

JavaScopeCleanup? cleanup;
PeerableCollection? scope;

public JavaScope (JavaScopeCleanup cleanup)
{
this.cleanup = cleanup;
scope = JniEnvironment.CurrentInfo.BeginScope (cleanup);
}

public void Dispose ()
{
if (cleanup == null || scope == null) {
return;
}
List<Exception>? exceptions = null;
switch (cleanup) {
case JavaScopeCleanup.Dispose:
// Need to iterate over a copy of `scope`, as `p.Dispose()` will modify `scope`
var copy = new IJavaPeerable [scope.Count];
scope.CopyTo (copy, 0);
foreach (var p in copy) {
try {
p.Dispose ();
}
catch (Exception e) {
exceptions = exceptions ?? new List<Exception>();
exceptions.Add (e);
Trace.WriteLine (e);
}
}
break;
}
JniEnvironment.CurrentInfo.EndScope (scope);
scope.Clear ();
scope = null;
if (exceptions != null) {
throw new AggregateException (exceptions);
}
}
}
}
Loading

1 comment on commit a675a87

@jonpryor
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JavaScopeCleanup.RegisterWithManager should probably be JavaScopeCleanup.LeaveWithManager, as object construction must register with the manager; what JavaScopeCleanup details is wha to do with the registrations created within the JavaScope. .RegisterWithManager doesn't really provide clarity here.

Please sign in to comment.