Skip to content

Commit

Permalink
[Java.Runtime.Environment] Support .NET Core
Browse files Browse the repository at this point in the history
Fixes: dotnet#426

Enable C#8 [Nullable Reference Types][0] for
`Java.Runtime.Environment.dll`.

Add support for a "non-bridged backend", so that a
`JniRuntime.JniValueManager` exists for .NET Core.  This new
"managed" backed is used if the Mono runtime is *not* used.

To work, `ManagedValueManager` holds *strong* references to
`IJavaPeerable` instances.  As such, tests which required the use of
GC integration are now "optional".

To make this work, update `JniRuntime.JniValueManager` to have the
following new abstract members:

	partial class JniRuntime {
	    partial class JniValueManager {
	        public abstract bool PeersRequireRelease {get;}
	        public abstract void ReleasePeers ();
	    }
	}

`PeersRequireRelease` shall be `true` when there is no GC bridge.
When this is true, `JniValueManager.CollectPeers()` is a no-op.
If an `IJavaPeerable` must have disposal logic performed, then
`.Dispose()` must be called on that instance.  There is no
finalization integration.

The new `JniValueManager.ReleasePeers()` method:

 1. Releases all GREFs for all held peers.

    This allows Java to collect the Java peers.

 2. Stops referencing all `IJavaPeerable` values.

    This allows the .NET GC to collect the `IJavaPeerable` values.

There is no notification to the `IJavaPeerable` instances that this
has happened.

Update `Java.Interop-Tests.csproj` to define
`NO_MARSHAL_MEMBER_BUILDER_SUPPORT` when building for .NET Core.

These changes allow all remaining `Java.Interop-Tests` unit tests to
execute under .NET Core:

	dotnet test -v diag '--logger:trx;verbosity=detailed' bin/TestDebug-netcoreapp3.1/Java.Interop-Tests.dll

Other changes:

  * The attempt to retain useful Java-side exceptions in 89a5a22
    proved to be incomplete.  Add a comment to invoke
    [`JNIEnv::ExceptionDescribe()`][1].  We don't always want this
    to be present, but when we do want it…

  * While `NO_MARSHAL_MEMBER_BUILDER_SUPPORT` is set -- which means
    that `Java.Interop.Export`-related tests aren't run -- there are
    some fixes for `Java.Interop.Export` & related unit tests for
    .NET Core, to avoid the use of generic delegate types and to
    avoid a `Type.GetType()` which is no longer needed.

[0]: https://docs.microsoft.com/dotnet/csharp/nullable-references
[1]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#ExceptionDescribe
  • Loading branch information
jonpryor committed Feb 18, 2021
1 parent 89a5a22 commit 98e0288
Show file tree
Hide file tree
Showing 17 changed files with 612 additions and 83 deletions.
7 changes: 6 additions & 1 deletion src/Java.Interop.Export/Java.Interop/MarshalMemberBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,12 @@ static Expression GetRuntime ()
return Expression.Property (null, typeof (JniEnvironment), "Runtime");
}

static MethodInfo FormatterServices_GetUninitializedObject = Type.GetType ("System.Runtime.Serialization.FormatterServices", throwOnError: true)
static MethodInfo FormatterServices_GetUninitializedObject =
#if NETFRAMEWORK || NET_2_0
typeof (System.Runtime.Serialization.FormatterServices)
#else // !(NETFRAMEWORK || NET_2_0)
typeof (System.Runtime.CompilerServices.RuntimeHelpers)
#endif // NETFRAMEWORK || NET_2_0
.GetRuntimeMethod ("GetUninitializedObject", new[]{typeof (Type)});
static MethodInfo IJavaPeerable_SetPeerReference = typeof (IJavaPeerable).GetRuntimeMethod ("SetPeerReference", new[]{typeof (JniObjectReference)});

Expand Down
13 changes: 13 additions & 0 deletions src/Java.Interop/Java.Interop/JniEnvironment.Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public static unsafe JniObjectReference FindClass (string classname)
return r;
}

// NativeMethods.java_interop_jnienv_exception_describe (info.EnvironmentPointer);

NativeMethods.java_interop_jnienv_exception_clear (info.EnvironmentPointer);

var findClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local);
Expand Down Expand Up @@ -167,6 +169,17 @@ public static void RegisterNatives (JniObjectReference type, JniNativeMethodRegi

public static void RegisterNatives (JniObjectReference type, JniNativeMethodRegistration [] methods, int numMethods)
{
#if DEBUG && NETCOREAPP
foreach (var m in methods) {
if (m.Marshaler.GetType ().GenericTypeArguments.Length != 0) {
var method = m.Marshaler.Method;
Debug.WriteLine ($"JNIEnv::RegisterNatives() given a generic delegate type. .NET Core doesn't like this.");
Debug.WriteLine ($" Java: {m.Name}{m.Signature}");
Debug.WriteLine ($" Marshaler Type={m.Marshaler.GetType ().FullName} Method={method.DeclaringType.FullName}.{method.Name}");
}
}
#endif // DEBUG

int r = _RegisterNatives (type, methods, numMethods);

if (r != 0) {
Expand Down
3 changes: 3 additions & 0 deletions src/Java.Interop/Java.Interop/JniRuntime.JniValueManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,12 @@ protected virtual void Dispose (bool disposing)
disposed = true;
}

public abstract bool PeersRequireRelease {get;}

public abstract void WaitForGCBridgeProcessing ();

public abstract void CollectPeers ();
public abstract void ReleasePeers ();

public abstract void AddPeer (IJavaPeerable value);

Expand Down
72 changes: 14 additions & 58 deletions src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public class JreRuntimeOptions : JniRuntime.CreationOptions {

public Collection<string> ClassPath {get; private set;}

public TextWriter? JniGlobalReferenceLogWriter {get; set;}
public TextWriter? JniLocalReferenceLogWriter {get; set;}

public JreRuntimeOptions ()
{
JniVersion = JniVersion.v1_2;
Expand All @@ -39,16 +42,6 @@ public JreRuntimeOptions ()
Path.GetDirectoryName (typeof (JreRuntimeOptions).Assembly.Location),
"java-interop.jar"),
};

bool onMono = Type.GetType ("Mono.Runtime", throwOnError: false) != null;
if (onMono) {
ValueManager = ValueManager ?? new MonoRuntimeValueManager ();
ObjectReferenceManager = ObjectReferenceManager ?? new MonoRuntimeObjectReferenceManager ();
}
else {
ValueManager = ValueManager ?? new DummyValueManager ();
ObjectReferenceManager = ObjectReferenceManager ?? new DummyObjectReferenceManager ();
}
}

public JreRuntimeOptions AddOption (string option)
Expand Down Expand Up @@ -87,12 +80,22 @@ static unsafe JreRuntimeOptions CreateJreVM (JreRuntimeOptions builder)
if (builder == null)
throw new ArgumentNullException ("builder");

bool onMono = Type.GetType ("Mono.Runtime", throwOnError: false) != null;
if (onMono) {
builder.ValueManager = builder.ValueManager ?? new MonoRuntimeValueManager ();
builder.ObjectReferenceManager = builder.ObjectReferenceManager ?? new MonoRuntimeObjectReferenceManager ();
}
else {
builder.ValueManager = builder.ValueManager ?? new ManagedValueManager ();
builder.ObjectReferenceManager = builder.ObjectReferenceManager ?? new ManagedObjectReferenceManager (builder.JniGlobalReferenceLogWriter, builder.JniLocalReferenceLogWriter);
}

if (builder.InvocationPointer != IntPtr.Zero)
return builder;

if (!string.IsNullOrEmpty (builder.JvmLibraryPath)) {
IntPtr errorPtr = IntPtr.Zero;
int r = NativeMethods.java_interop_jvm_load_with_error_message (builder.JvmLibraryPath, out errorPtr);
int r = NativeMethods.java_interop_jvm_load_with_error_message (builder.JvmLibraryPath!, out errorPtr);
if (r != 0) {
string error = Marshal.PtrToStringAnsi (errorPtr);
NativeMethods.java_interop_free (errorPtr);
Expand Down Expand Up @@ -166,52 +169,5 @@ partial class NativeMethods {
[DllImport (JavaInteropLib, CharSet=CharSet.Ansi, CallingConvention=CallingConvention.Cdecl)]
internal static extern int java_interop_jvm_create (out IntPtr javavm, out IntPtr jnienv, ref JavaVMInitArgs args);
}

class DummyValueManager : JniRuntime.JniValueManager {

public override void WaitForGCBridgeProcessing ()
{
}

public override void CollectPeers ()
{
}

public override void AddPeer (IJavaPeerable reference)
{
}

public override void RemovePeer (IJavaPeerable reference)
{
}

public override void FinalizePeer (IJavaPeerable reference)
{
}

public override List<JniSurfacedPeerInfo> GetSurfacedPeers ()
{
return null;
}

public override IJavaPeerable PeekPeer (global::Java.Interop.JniObjectReference reference)
{
return null;
}

public override void ActivatePeer (IJavaPeerable self, JniObjectReference reference, ConstructorInfo cinfo, object [] argumentValues)
{
}
}

class DummyObjectReferenceManager : JniRuntime.JniObjectReferenceManager {
public override int GlobalReferenceCount {
get {return 0;}
}

public override int WeakGlobalReferenceCount {
get {return 0;}
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

namespace Java.Interop {

class ManagedObjectReferenceManager : JniRuntime.JniObjectReferenceManager {

TextWriter? grefLog;
TextWriter? lrefLog;

int grefCount;
int wgrefCount;


public override int GlobalReferenceCount => grefCount;
public override int WeakGlobalReferenceCount => wgrefCount;

public override bool LogLocalReferenceMessages => lrefLog != null;
public override bool LogGlobalReferenceMessages => grefLog != null;

public ManagedObjectReferenceManager (TextWriter? grefLog, TextWriter? lrefLog)
{
if (grefLog != null && lrefLog != null && object.ReferenceEquals (grefLog, lrefLog)) {
this.grefLog = this.lrefLog = TextWriter.Synchronized (grefLog);
return;
}

var grefPath = Environment.GetEnvironmentVariable ("JAVA_INTEROP_GREF_LOG");
var lrefPath = Environment.GetEnvironmentVariable ("JAVA_INTEROP_LREF_LOG");

bool samePath = !string.IsNullOrEmpty (grefPath) &&
!string.IsNullOrEmpty (lrefPath) &&
grefPath == lrefPath;

if (grefLog != null) {
this.grefLog = TextWriter.Synchronized (grefLog);
}
if (lrefLog != null) {
this.lrefLog = TextWriter.Synchronized (lrefLog);
}

if (this.grefLog == null && !string.IsNullOrEmpty (grefPath)) {
this.grefLog = TextWriter.Synchronized (CreateTextWriter (grefPath));
}
if (this.lrefLog == null && samePath) {
this.lrefLog = this.grefLog;
}
if (this.lrefLog == null && !string.IsNullOrEmpty (lrefPath)) {
this.lrefLog = TextWriter.Synchronized (CreateTextWriter (lrefPath));
}
}

public override void OnSetRuntime (JniRuntime runtime)
{
base.OnSetRuntime (runtime);
}

static TextWriter? CreateTextWriter (string path)
{
return new StreamWriter (path, append: false, encoding: new UTF8Encoding (encoderShouldEmitUTF8Identifier: false));
}

public override void WriteLocalReferenceLine (string format, params object[] args)
{
if (lrefLog == null)
return;
lrefLog.WriteLine (format, args);
lrefLog.Flush ();
}

public override JniObjectReference CreateLocalReference (JniObjectReference reference, ref int localReferenceCount)
{
if (!reference.IsValid)
return reference;

var r = base.CreateLocalReference (reference, ref localReferenceCount);

CreatedReference (lrefLog, "+l+ lrefc", localReferenceCount, reference, r, Runtime);

return r;
}

public override void DeleteLocalReference (ref JniObjectReference reference, ref int localReferenceCount)
{
if (!reference.IsValid)
return;

var r = reference;

base.DeleteLocalReference (ref reference, ref localReferenceCount);

DeletedReference (lrefLog, "-l- lrefc", localReferenceCount, r, Runtime);
}

public override void CreatedLocalReference (JniObjectReference reference, ref int localReferenceCount)
{
if (!reference.IsValid)
return;
base.CreatedLocalReference (reference, ref localReferenceCount);
CreatedReference (lrefLog, "+l+ lrefc", localReferenceCount, reference, Runtime);
}

public override IntPtr ReleaseLocalReference (ref JniObjectReference reference, ref int localReferenceCount)
{
if (!reference.IsValid)
return IntPtr.Zero;
var r = reference;
var p = base.ReleaseLocalReference (ref reference, ref localReferenceCount);
DeletedReference (lrefLog, "-l- lrefc", localReferenceCount, r, Runtime);
return p;
}

public override void WriteGlobalReferenceLine (string format, params object?[]? args)
{
if (grefLog == null)
return;
grefLog.WriteLine (format, args);
grefLog.Flush ();
}

public override JniObjectReference CreateGlobalReference (JniObjectReference reference)
{
if (!reference.IsValid)
return reference;
var n = base.CreateGlobalReference (reference);
int c = Interlocked.Increment (ref grefCount);
CreatedReference (grefLog, "+g+ grefc", c, reference, n, Runtime);
return n;
}

public override void DeleteGlobalReference (ref JniObjectReference reference)
{
if (!reference.IsValid)
return;
int c = Interlocked.Decrement (ref grefCount);
DeletedReference (grefLog, "-g- grefc", c, reference, Runtime);
base.DeleteGlobalReference (ref reference);
}

public override JniObjectReference CreateWeakGlobalReference (JniObjectReference reference)
{
if (!reference.IsValid)
return reference;
var n = base.CreateWeakGlobalReference (reference);

int wc = Interlocked.Increment (ref wgrefCount);
int gc = grefCount;
if (grefLog != null) {
string message = $"+w+ grefc {gc} gwrefc {wc} obj-handle {reference.ToString ()} -> new-handle {n.ToString ()} " +
$"from thread '{Runtime.GetCurrentManagedThreadName ()}'({Environment.CurrentManagedThreadId})" +
Environment.NewLine +
Runtime.GetCurrentManagedThreadStackTrace (skipFrames: 2, fNeedFileInfo: true);
grefLog.WriteLine (message);
grefLog.Flush ();
}

return n;
}

public override void DeleteWeakGlobalReference (ref JniObjectReference reference)
{
if (!reference.IsValid)
return;

int wc = Interlocked.Decrement (ref wgrefCount);
int gc = grefCount;

if (grefLog != null) {
string message = $"-w- grefc {gc} gwrefc {wc} handle {reference.ToString ()} " +
$"from thread '{Runtime.GetCurrentManagedThreadName ()}'({Environment.CurrentManagedThreadId})" +
Environment.NewLine +
Runtime.GetCurrentManagedThreadStackTrace (skipFrames: 2, fNeedFileInfo: true);
grefLog.WriteLine (message);
grefLog.Flush ();
}

base.DeleteWeakGlobalReference (ref reference);
}

protected override void Dispose (bool disposing)
{
}

static void CreatedReference (TextWriter? writer, string kind, int count, JniObjectReference reference, JniRuntime runtime)
{
if (writer == null)
return;
string message = $"{kind} {count} handle {reference.ToString ()} " +
$"from thread '{runtime.GetCurrentManagedThreadName ()}'({Environment.CurrentManagedThreadId})" +
Environment.NewLine +
runtime.GetCurrentManagedThreadStackTrace (skipFrames: 2, fNeedFileInfo: true);
writer.WriteLine (message);
writer.Flush ();
}

static void CreatedReference (TextWriter? writer, string kind, int count, JniObjectReference reference, JniObjectReference newReference, JniRuntime runtime)
{
if (writer == null)
return;
string message = $"{kind} {count} obj-handle {reference.ToString ()} -> new-handle {newReference.ToString ()} " +
$"from thread '{runtime.GetCurrentManagedThreadName ()}'({Environment.CurrentManagedThreadId})" +
Environment.NewLine +
runtime.GetCurrentManagedThreadStackTrace (skipFrames: 2, fNeedFileInfo: true);
writer.WriteLine (message);
writer.Flush ();
}

static void DeletedReference (TextWriter? writer, string kind, int count, JniObjectReference reference, JniRuntime runtime)
{
if (writer == null)
return;
string message = $"{kind} {count} handle {reference.ToString ()} " +
$"from thread '{runtime.GetCurrentManagedThreadName ()}'({Environment.CurrentManagedThreadId})" +
Environment.NewLine +
runtime.GetCurrentManagedThreadStackTrace (skipFrames: 2, fNeedFileInfo: true);
writer.WriteLine (message);
writer.Flush ();
}
}
}
Loading

0 comments on commit 98e0288

Please sign in to comment.