From 763963af648e1ee08134b1e844586f9ac5cd6b7d Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Thu, 18 Feb 2021 11:55:19 -0500 Subject: [PATCH] [Java.Runtime.Environment] Partial support for .NET Core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable C#8 [Nullable Reference Types][0] for `Java.Runtime.Environment.dll`. Add partial 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", conditional on the `!NO_GC_BRIDGE_SUPPORT` define. The `ManagedValueManager.CollectPeers()` method calls `IJavaPeerable.Dispose()` on all currently referenced peers, then stops referencing the managed peers. This causes all GREFs to be dropped, allowing Java peers to be collected, and then allows the .NET GC to collect the `IJavaPeerable` values. Any and all exceptions thrown by `IJavaPeerable.Dispose()` are caught and re-thrown by an `AggregateException`. Update `Java.Interop-Tests.csproj` to define `NO_GC_BRIDGE_SUPPORT` and `NO_MARSHAL_MEMBER_BUILDER_SUPPORT` when building for .NET Core. This excludes all currently "troublesome"/non-passing tests. 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 Passed! - Failed: 0, Passed: 617, Skipped: 1, Total: 618, Duration: 1 s Other changes: * The attempt to retain useful Java-side exceptions in 89a5a229 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 --- build-tools/automation/azure-pipelines.yaml | 1 + .../automation/templates/core-tests.yaml | 8 + .../Java.Interop/MarshalMemberBuilder.cs | 7 +- .../Java.Interop/JniEnvironment.Types.cs | 18 ++ .../Java.Interop/JreRuntime.cs | 72 ++---- .../ManagedObjectReferenceManager.cs | 228 +++++++++++++++++ .../Java.Interop/ManagedValueManager.cs | 237 ++++++++++++++++++ .../MonoRuntimeObjectReferenceManager.cs | 22 +- .../Java.Interop/MonoRuntimeValueManager.cs | 24 +- .../Java.Runtime.Environment.csproj | 2 + .../Java.Interop-Tests.csproj | 8 + .../Java.Interop/JavaObjectTest.cs | 6 + .../Java.Interop/JavaVMFixture.cs | 2 + .../JniValueMarshalerContractTests.cs | 2 +- .../Java.Interop/TestType.cs | 12 +- .../Java.Interop/MarshalMemberBuilderTest.cs | 17 +- tests/TestJVM/TestJVM.cs | 19 +- 17 files changed, 598 insertions(+), 87 deletions(-) create mode 100644 src/Java.Runtime.Environment/Java.Interop/ManagedObjectReferenceManager.cs create mode 100644 src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs diff --git a/build-tools/automation/azure-pipelines.yaml b/build-tools/automation/azure-pipelines.yaml index 4799a033e..c907106f6 100644 --- a/build-tools/automation/azure-pipelines.yaml +++ b/build-tools/automation/azure-pipelines.yaml @@ -21,6 +21,7 @@ variables: DotNetCoreVersion: 3.1.300 HostedMacImage: macOS-10.15 HostedWinVS2019: Hosted Windows 2019 with VS2019 + NetCoreTargetFrameworkPathSuffix: -netcoreapp3.1 jobs: - job: windows_build diff --git a/build-tools/automation/templates/core-tests.yaml b/build-tools/automation/templates/core-tests.yaml index fad8361fe..9c4696400 100644 --- a/build-tools/automation/templates/core-tests.yaml +++ b/build-tools/automation/templates/core-tests.yaml @@ -67,6 +67,14 @@ steps: arguments: bin/Test$(Build.Configuration)/Java.Interop-Tests.dll continueOnError: true +- task: DotNetCoreCLI@2 + displayName: 'Tests: Java.Interop' + condition: eq('${{ parameters.runNativeTests }}', 'true') + inputs: + command: test + arguments: bin/Test$(Build.Configuration)$(NetCoreTargetFrameworkPathSuffix)/Java.Interop-Tests.dll + continueOnError: true + - task: DotNetCoreCLI@2 displayName: 'Tests: Java.Interop.Dynamic' condition: eq('${{ parameters.runNativeTests }}', 'true') diff --git a/src/Java.Interop.Export/Java.Interop/MarshalMemberBuilder.cs b/src/Java.Interop.Export/Java.Interop/MarshalMemberBuilder.cs index 69919d646..7a8109cd5 100644 --- a/src/Java.Interop.Export/Java.Interop/MarshalMemberBuilder.cs +++ b/src/Java.Interop.Export/Java.Interop/MarshalMemberBuilder.cs @@ -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 NETCOREAPP + typeof (System.Runtime.CompilerServices.RuntimeHelpers) +#else // !NETCOREAPP + typeof (System.Runtime.Serialization.FormatterServices) +#endif // NETCOREAPP .GetRuntimeMethod ("GetUninitializedObject", new[]{typeof (Type)}); static MethodInfo IJavaPeerable_SetPeerReference = typeof (IJavaPeerable).GetRuntimeMethod ("SetPeerReference", new[]{typeof (JniObjectReference)}); diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs index d26ef55bb..3d4e7eb2f 100644 --- a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs +++ b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.Diagnostics; using System.Collections.Generic; using System.Text; @@ -47,6 +48,12 @@ public static unsafe JniObjectReference FindClass (string classname) return r; } + // If the Java-side exception stack trace is *lost* a'la 89a5a229, + // change `false` to `true` and rebuild+re-run. +#if false + NativeMethods.java_interop_jnienv_exception_describe (info.EnvironmentPointer); +#endif + NativeMethods.java_interop_jnienv_exception_clear (info.EnvironmentPointer); var findClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local); @@ -167,6 +174,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 && NETCOREAPP + int r = _RegisterNatives (type, methods, numMethods); if (r != 0) { diff --git a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs index c3ceb115d..57384f8d1 100644 --- a/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs +++ b/src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs @@ -31,6 +31,9 @@ public class JreRuntimeOptions : JniRuntime.CreationOptions { public Collection ClassPath {get; private set;} + public TextWriter? JniGlobalReferenceLogWriter {get; set;} + public TextWriter? JniLocalReferenceLogWriter {get; set;} + public JreRuntimeOptions () { JniVersion = JniVersion.v1_2; @@ -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) @@ -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); @@ -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 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;} - } - } } diff --git a/src/Java.Runtime.Environment/Java.Interop/ManagedObjectReferenceManager.cs b/src/Java.Runtime.Environment/Java.Interop/ManagedObjectReferenceManager.cs new file mode 100644 index 000000000..700c6f617 --- /dev/null +++ b/src/Java.Runtime.Environment/Java.Interop/ManagedObjectReferenceManager.cs @@ -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 (); + } + } +} diff --git a/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs b/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs new file mode 100644 index 000000000..1dcb12220 --- /dev/null +++ b/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs @@ -0,0 +1,237 @@ +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.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Java.Interop { + + class ManagedValueManager : JniRuntime.JniValueManager { + + Dictionary>? RegisteredInstances = new Dictionary>(); + + public override void WaitForGCBridgeProcessing () + { + } + + public override void CollectPeers () + { + if (RegisteredInstances == null) + throw new ObjectDisposedException (nameof (MonoRuntimeValueManager)); + + 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 void AddPeer (IJavaPeerable value) + { + if (RegisteredInstances == null) + throw new ObjectDisposedException (nameof (MonoRuntimeValueManager)); + + var r = value.PeerReference; + if (!r.IsValid) + throw new ObjectDisposedException (value.GetType ().FullName); + var o = PeekPeer (value.PeerReference); + if (o != null) + return; + + if (r.Type != JniObjectReferenceType.Global) { + value.SetPeerReference (r.NewGlobalRef ()); + JniObjectReference.Dispose (ref r, JniObjectReferenceOptions.CopyAndDispose); + } + int key = value.JniIdentityHashCode; + lock (RegisteredInstances) { + List peers; + if (!RegisteredInstances.TryGetValue (key, out peers)) { + peers = new List () { + value, + }; + RegisteredInstances.Add (key, peers); + return; + } + + for (int i = peers.Count - 1; i >= 0; i--) { + var p = peers [i]; + if (!JniEnvironment.Types.IsSameObject (p.PeerReference, value.PeerReference)) + continue; + if (Replaceable (p)) { + peers [i] = value; + } else { + WarnNotReplacing (key, value, p); + } + return; + } + peers.Add (value); + } + } + + static bool Replaceable (IJavaPeerable peer) + { + if (peer == null) + return true; + return (peer.JniManagedPeerState & JniManagedPeerStates.Replaceable) == JniManagedPeerStates.Replaceable; + } + + void WarnNotReplacing (int key, IJavaPeerable ignoreValue, IJavaPeerable keepValue) + { + Runtime.ObjectReferenceManager.WriteGlobalReferenceLine ( + "Warning: Not registering PeerReference={0} IdentityHashCode=0x{1} Instance={2} Instance.Type={3} Java.Type={4}; " + + "keeping previously registered PeerReference={5} Instance={6} Instance.Type={7} Java.Type={8}.", + ignoreValue.PeerReference.ToString (), + key.ToString ("x"), + RuntimeHelpers.GetHashCode (ignoreValue).ToString ("x"), + ignoreValue.GetType ().FullName, + JniEnvironment.Types.GetJniTypeNameFromInstance (ignoreValue.PeerReference), + keepValue.PeerReference.ToString (), + RuntimeHelpers.GetHashCode (keepValue).ToString ("x"), + keepValue.GetType ().FullName, + JniEnvironment.Types.GetJniTypeNameFromInstance (keepValue.PeerReference)); + } + + public override IJavaPeerable? PeekPeer (JniObjectReference reference) + { + if (RegisteredInstances == null) + throw new ObjectDisposedException (nameof (MonoRuntimeValueManager)); + + if (!reference.IsValid) + return null; + + int key = GetJniIdentityHashCode (reference); + + lock (RegisteredInstances) { + List peers; + if (!RegisteredInstances.TryGetValue (key, out peers)) + return null; + + for (int i = peers.Count - 1; i >= 0; i--) { + var p = peers [i]; + if (JniEnvironment.Types.IsSameObject (reference, p.PeerReference)) + return p; + } + if (peers.Count == 0) + RegisteredInstances.Remove (key); + } + return null; + } + + public override void RemovePeer (IJavaPeerable value) + { + if (RegisteredInstances == null) + throw new ObjectDisposedException (nameof (MonoRuntimeValueManager)); + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + int key = value.JniIdentityHashCode; + lock (RegisteredInstances) { + List peers; + if (!RegisteredInstances.TryGetValue (key, out peers)) + return; + + for (int i = peers.Count - 1; i >= 0; i--) { + var p = peers [i]; + if (object.ReferenceEquals (value, p)) { + peers.RemoveAt (i); + } + } + if (peers.Count == 0) + RegisteredInstances.Remove (key); + } + } + + public override void FinalizePeer (IJavaPeerable value) + { + var h = value.PeerReference; + var o = Runtime.ObjectReferenceManager; + // MUST NOT use SafeHandle.ReferenceType: local refs are tied to a JniEnvironment + // and the JniEnvironment's corresponding thread; it's a thread-local value. + // Accessing SafeHandle.ReferenceType won't kill anything (so far...), but + // instead it always returns JniReferenceType.Invalid. + if (!h.IsValid || h.Type == JniObjectReferenceType.Local) { + if (o.LogGlobalReferenceMessages) { + o.WriteGlobalReferenceLine ("Finalizing PeerReference={0} IdentityHashCode=0x{1} Instance=0x{2} Instance.Type={3}", + h.ToString (), + value.JniIdentityHashCode.ToString ("x"), + RuntimeHelpers.GetHashCode (value).ToString ("x"), + value.GetType ().ToString ()); + } + RemovePeer (value); + value.SetPeerReference (new JniObjectReference ()); + value.Finalized (); + return; + } + + RemovePeer (value); + if (o.LogGlobalReferenceMessages) { + o.WriteGlobalReferenceLine ("Finalizing PeerReference={0} IdentityHashCode=0x{1} Instance=0x{2} Instance.Type={3}", + h.ToString (), + value.JniIdentityHashCode.ToString ("x"), + RuntimeHelpers.GetHashCode (value).ToString ("x"), + value.GetType ().ToString ()); + } + value.SetPeerReference (new JniObjectReference ()); + JniObjectReference.Dispose (ref h); + value.Finalized (); + } + + public override void ActivatePeer (IJavaPeerable? self, JniObjectReference reference, ConstructorInfo cinfo, object?[]? argumentValues) + { + var runtime = JniEnvironment.Runtime; + + try { + var f = runtime.MarshalMemberBuilder.CreateConstructActivationPeerFunc (cinfo); + f (cinfo, reference, argumentValues); + } catch (Exception e) { + var m = string.Format ("Could not activate {{ PeerReference={0} IdentityHashCode=0x{1} Java.Type={2} }} for managed type '{3}'.", + reference, + runtime.ValueManager.GetJniIdentityHashCode (reference).ToString ("x"), + JniEnvironment.Types.GetJniTypeNameFromInstance (reference), + cinfo.DeclaringType.FullName); + Debug.WriteLine (m); + + throw new NotSupportedException (m, e); + } + } + + public override List GetSurfacedPeers () + { + if (RegisteredInstances == null) + throw new ObjectDisposedException (nameof (MonoRuntimeValueManager)); + + lock (RegisteredInstances) { + 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))); + } + } + return peers; + } + } + } +} diff --git a/src/Java.Runtime.Environment/Java.Interop/MonoRuntimeObjectReferenceManager.cs b/src/Java.Runtime.Environment/Java.Interop/MonoRuntimeObjectReferenceManager.cs index cf071e94d..68025e066 100644 --- a/src/Java.Runtime.Environment/Java.Interop/MonoRuntimeObjectReferenceManager.cs +++ b/src/Java.Runtime.Environment/Java.Interop/MonoRuntimeObjectReferenceManager.cs @@ -58,14 +58,14 @@ public override JniObjectReference CreateLocalReference (JniObjectReference refe return r; } - string GetCurrentManagedThreadName (bool create) + string? GetCurrentManagedThreadName (bool create) { if (create) return Runtime.GetCurrentManagedThreadName (); return null; } - string GetCurrentManagedThreadStack (bool create) + string? GetCurrentManagedThreadStack (bool create) { if (create) return Runtime.GetCurrentManagedThreadStackTrace (skipFrames: 2, fNeedFileInfo: true); @@ -120,7 +120,7 @@ public override bool LogGlobalReferenceMessages { get {return logGlobalRefs;} } - public override void WriteGlobalReferenceLine (string format, params object[] args) + public override void WriteGlobalReferenceLine (string format, params object?[]? args) { if (!LogGlobalReferenceMessages) return; @@ -217,13 +217,13 @@ partial class NativeMethods { internal static extern int java_interop_gc_bridge_lref_set_log_level (IntPtr bridge, int level); [DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)] - internal static extern void java_interop_gc_bridge_lref_log_message (IntPtr bridge, int level, string message); + internal static extern void java_interop_gc_bridge_lref_log_message (IntPtr bridge, int level, string? message); [DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)] - internal static extern void java_interop_gc_bridge_lref_log_new (IntPtr bridge, int lref_count, IntPtr curHandle, byte curType, IntPtr newHandle, byte newType, string thread_name, long thread_id, string from); + internal static extern void java_interop_gc_bridge_lref_log_new (IntPtr bridge, int lref_count, IntPtr curHandle, byte curType, IntPtr newHandle, byte newType, string? thread_name, long thread_id, string? from); [DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)] - internal static extern void java_interop_gc_bridge_lref_log_delete (IntPtr bridge, int lref_count, IntPtr handle, byte type, string thread_name, long thread_id, string from); + internal static extern void java_interop_gc_bridge_lref_log_delete (IntPtr bridge, int lref_count, IntPtr handle, byte type, string? thread_name, long thread_id, string? from); [DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)] internal static extern IntPtr java_interop_gc_bridge_gref_get_log_file (IntPtr bridge); @@ -232,19 +232,19 @@ partial class NativeMethods { internal static extern int java_interop_gc_bridge_gref_set_log_level (IntPtr bridge, int level); [DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)] - internal static extern void java_interop_gc_bridge_gref_log_message (IntPtr bridge, int level, string message); + internal static extern void java_interop_gc_bridge_gref_log_message (IntPtr bridge, int level, string? message); [DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)] - internal static extern int java_interop_gc_bridge_gref_log_new (IntPtr bridge, IntPtr curHandle, byte curType, IntPtr newHandle, byte newType, string thread_name, long thread_id, string from); + internal static extern int java_interop_gc_bridge_gref_log_new (IntPtr bridge, IntPtr curHandle, byte curType, IntPtr newHandle, byte newType, string? thread_name, long thread_id, string? from); [DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)] - internal static extern int java_interop_gc_bridge_gref_log_delete (IntPtr bridge, IntPtr handle, byte type, string thread_name, long thread_id, string from); + internal static extern int java_interop_gc_bridge_gref_log_delete (IntPtr bridge, IntPtr handle, byte type, string? thread_name, long thread_id, string? from); [DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)] - internal static extern int java_interop_gc_bridge_weak_gref_log_new (IntPtr bridge, IntPtr curHandle, byte curType, IntPtr newHandle, byte newType, string thread_name, long thread_id, string from); + internal static extern int java_interop_gc_bridge_weak_gref_log_new (IntPtr bridge, IntPtr curHandle, byte curType, IntPtr newHandle, byte newType, string? thread_name, long thread_id, string? from); [DllImport (JavaInteropLib, CallingConvention=CallingConvention.Cdecl)] - internal static extern int java_interop_gc_bridge_weak_gref_log_delete (IntPtr bridge, IntPtr handle, byte type, string thread_name, long thread_id, string from); + internal static extern int java_interop_gc_bridge_weak_gref_log_delete (IntPtr bridge, IntPtr handle, byte type, string? thread_name, long thread_id, string? from); } } diff --git a/src/Java.Runtime.Environment/Java.Interop/MonoRuntimeValueManager.cs b/src/Java.Runtime.Environment/Java.Interop/MonoRuntimeValueManager.cs index 169ba9c06..1f7cea114 100644 --- a/src/Java.Runtime.Environment/Java.Interop/MonoRuntimeValueManager.cs +++ b/src/Java.Runtime.Environment/Java.Interop/MonoRuntimeValueManager.cs @@ -97,11 +97,14 @@ protected override void Dispose (bool disposing) } } - Dictionary>> RegisteredInstances = new Dictionary>>(); + Dictionary>>? RegisteredInstances = new Dictionary>>(); public override List GetSurfacedPeers () { + if (RegisteredInstances == null) + throw new ObjectDisposedException (nameof (MonoRuntimeValueManager)); + lock (RegisteredInstances) { var peers = new List (RegisteredInstances.Count); foreach (var e in RegisteredInstances) { @@ -115,6 +118,9 @@ public override List GetSurfacedPeers () public override void AddPeer (IJavaPeerable value) { + if (RegisteredInstances == null) + throw new ObjectDisposedException (nameof (MonoRuntimeValueManager)); + var r = value.PeerReference; if (!r.IsValid) throw new ObjectDisposedException (value.GetType ().FullName); @@ -183,6 +189,9 @@ static bool Replaceable (IJavaPeerable peer) public override void RemovePeer (IJavaPeerable value) { + if (RegisteredInstances == null) + throw new ObjectDisposedException (nameof (MonoRuntimeValueManager)); + if (value == null) throw new ArgumentNullException (nameof (value)); @@ -209,8 +218,11 @@ public override void RemovePeer (IJavaPeerable value) } } - public override IJavaPeerable PeekPeer (JniObjectReference reference) + public override IJavaPeerable? PeekPeer (JniObjectReference reference) { + if (RegisteredInstances == null) + throw new ObjectDisposedException (nameof (MonoRuntimeValueManager)); + if (!reference.IsValid) return null; @@ -245,7 +257,7 @@ static Exception CreateJniLocationException () } } - public override void ActivatePeer (IJavaPeerable self, JniObjectReference reference, ConstructorInfo cinfo, object [] argumentValues) + public override void ActivatePeer (IJavaPeerable? self, JniObjectReference reference, ConstructorInfo cinfo, object?[]? argumentValues) { var runtime = JniEnvironment.Runtime; @@ -337,19 +349,19 @@ internal protected virtual bool TryGC (IJavaPeerable value, ref JniObjectReferen } static class JavaLangRuntime { - static JniType _typeRef; + static JniType? _typeRef; static JniType TypeRef { get {return JniType.GetCachedJniType (ref _typeRef, "java/lang/Runtime");} } - static JniMethodInfo _getRuntime; + static JniMethodInfo? _getRuntime; internal static JniObjectReference GetRuntime () { TypeRef.GetCachedStaticMethod (ref _getRuntime, "getRuntime", "()Ljava/lang/Runtime;"); return JniEnvironment.StaticMethods.CallStaticObjectMethod (TypeRef.PeerReference, _getRuntime); } - static JniMethodInfo _gc; + static JniMethodInfo? _gc; internal static void GC (JniObjectReference runtime) { TypeRef.GetCachedInstanceMethod (ref _gc, "gc", "()V"); diff --git a/src/Java.Runtime.Environment/Java.Runtime.Environment.csproj b/src/Java.Runtime.Environment/Java.Runtime.Environment.csproj index b1440bad1..5f4ce04d4 100644 --- a/src/Java.Runtime.Environment/Java.Runtime.Environment.csproj +++ b/src/Java.Runtime.Environment/Java.Runtime.Environment.csproj @@ -5,6 +5,8 @@ true ..\..\product.snk true + 8.0 + enable diff --git a/tests/Java.Interop-Tests/Java.Interop-Tests.csproj b/tests/Java.Interop-Tests/Java.Interop-Tests.csproj index 891d80447..a39a8e8f1 100644 --- a/tests/Java.Interop-Tests/Java.Interop-Tests.csproj +++ b/tests/Java.Interop-Tests/Java.Interop-Tests.csproj @@ -10,6 +10,10 @@ $(TestOutputFullPath) + + $(DefineConstants);NO_MARSHAL_MEMBER_BUILDER_SUPPORT;NO_GC_BRIDGE_SUPPORT + + @@ -33,6 +37,10 @@ + + + + diff --git a/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs b/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs index 6ce8935f0..94b635b49 100644 --- a/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs +++ b/tests/Java.Interop-Tests/Java.Interop/JavaObjectTest.cs @@ -10,6 +10,7 @@ namespace Java.InteropTests [TestFixture] public class JavaObjectTest : JavaVMFixture { +#if !NO_GC_BRIDGE_SUPPORT [Test] public void JavaReferencedInstanceSurvivesCollection () { @@ -41,6 +42,7 @@ public void JavaReferencedInstanceSurvivesCollection () array.Dispose (); } } +#endif // !NO_GC_BRIDGE_SUPPORT [Test] public void UnregisterFromRuntime () @@ -72,6 +74,7 @@ public void RegisterWithVM_PermitsAliases () } } +#if !NO_GC_BRIDGE_SUPPORT [Test] public void UnreferencedInstanceIsCollected () { @@ -92,6 +95,7 @@ public void UnreferencedInstanceIsCollected () Assert.IsNull (JniRuntime.CurrentRuntime.ValueManager.PeekValue (oldHandle)); JniObjectReference.Dispose (ref oldHandle); } +#endif // !NO_GC_BRIDGE_SUPPORT [Test] public void Dispose () @@ -104,6 +108,7 @@ public void Dispose () Assert.IsFalse (f); } +#if !NO_GC_BRIDGE_SUPPORT [Test] public void Dispose_Finalized () { @@ -122,6 +127,7 @@ public void Dispose_Finalized () Assert.IsFalse (d); Assert.IsTrue (f); } +#endif // !NO_GC_BRIDGE_SUPPORT [Test] public void ObjectDisposed () diff --git a/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs b/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs index ad51db4fd..e1a816c6b 100644 --- a/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs +++ b/tests/Java.Interop-Tests/Java.Interop/JavaVMFixture.cs @@ -15,7 +15,9 @@ static partial void CreateJavaVM () var c = new TestJVM ( jars: new[]{ "interop-test.jar" }, typeMappings: new Dictionary () { +#if !NO_MARSHAL_MEMBER_BUILDER_SUPPORT { TestType.JniTypeName, typeof (TestType) }, +#endif // !NO_MARSHAL_MEMBER_BUILDER_SUPPORT { GenericHolder.JniTypeName, typeof (GenericHolder<>) }, } ); diff --git a/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs b/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs index a7c83978e..08e878688 100644 --- a/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs +++ b/tests/Java.Interop-Tests/Java.Interop/JniValueMarshalerContractTests.cs @@ -645,7 +645,7 @@ public DemoValueTypeValueMarshaler () public override DemoValueType CreateGenericValue (ref JniObjectReference reference, JniObjectReferenceOptions options, Type targetType) { - var v = Int32Marshaler.CreateGenericValue (ref reference, options, targetType); + var v = Int32Marshaler.CreateGenericValue (ref reference, options, typeof (int)); return new DemoValueType (v); } diff --git a/tests/Java.Interop-Tests/Java.Interop/TestType.cs b/tests/Java.Interop-Tests/Java.Interop/TestType.cs index 4640d5f14..7e910a4fb 100644 --- a/tests/Java.Interop-Tests/Java.Interop/TestType.cs +++ b/tests/Java.Interop-Tests/Java.Interop/TestType.cs @@ -109,10 +109,11 @@ public bool PropogateFinallyBlockExecuted { static Delegate GetEqualsThisHandler () { - Func h = _EqualsThis; + EqualsThisMarshalMethod h = _EqualsThis; return JniEnvironment.Runtime.MarshalMemberBuilder.CreateMarshalToManagedDelegate (h); } + delegate bool EqualsThisMarshalMethod (IntPtr jnienv, IntPtr n_self, IntPtr n_value); static bool _EqualsThis (IntPtr jnienv, IntPtr n_self, IntPtr n_value) { var jvm = JniEnvironment.Runtime; @@ -131,10 +132,11 @@ static bool _EqualsThis (IntPtr jnienv, IntPtr n_self, IntPtr n_value) static Delegate GetInt32ValueHandler () { - Func h = _GetInt32Value; + GetInt32ValueMarshalMethod h = _GetInt32Value; return JniEnvironment.Runtime.MarshalMemberBuilder.CreateMarshalToManagedDelegate (h); } + delegate int GetInt32ValueMarshalMethod (IntPtr jnienv, IntPtr n_self); static int _GetInt32Value (IntPtr jnienv, IntPtr n_self) { var r_self = new JniObjectReference (n_self); @@ -148,10 +150,11 @@ static int _GetInt32Value (IntPtr jnienv, IntPtr n_self) static Delegate _GetStringValueHandler () { - Func h = GetStringValueHandler; + GetStringValueMarshalMethod h = GetStringValueHandler; return JniEnvironment.Runtime.MarshalMemberBuilder.CreateMarshalToManagedDelegate (h); } + delegate IntPtr GetStringValueMarshalMethod (IntPtr jnienv, IntPtr n_self, int value); static IntPtr GetStringValueHandler (IntPtr jnienv, IntPtr n_self, int value) { var r_self = new JniObjectReference (n_self); @@ -171,10 +174,11 @@ static IntPtr GetStringValueHandler (IntPtr jnienv, IntPtr n_self, int value) static Delegate GetMethodThrowsHandler () { - Action h = MethodThrowsHandler; + MethodThrowsMarshalMethod h = MethodThrowsHandler; return JniEnvironment.Runtime.MarshalMemberBuilder.CreateMarshalToManagedDelegate (h); } + delegate void MethodThrowsMarshalMethod (IntPtr jnienv, IntPtr n_self); static void MethodThrowsHandler (IntPtr jnienv, IntPtr n_self) { var r_self = new JniObjectReference (n_self); diff --git a/tests/Java.Interop.Export-Tests/Java.Interop/MarshalMemberBuilderTest.cs b/tests/Java.Interop.Export-Tests/Java.Interop/MarshalMemberBuilderTest.cs index 4aa607aa4..65ad25777 100644 --- a/tests/Java.Interop.Export-Tests/Java.Interop/MarshalMemberBuilderTest.cs +++ b/tests/Java.Interop.Export-Tests/Java.Interop/MarshalMemberBuilderTest.cs @@ -530,20 +530,29 @@ public void CreateConstructActivationPeerExpression () var b = CreateBuilder (); var c = typeof (MarshalMemberBuilderTest).GetConstructor (new Type [0]); var e = b.CreateConstructActivationPeerExpression (c); + + const string GetUninitializedObject_decltype = +#if NETCOREAPP + "RuntimeHelpers" +#else // !NETCOREAPP + "FormatterServices" +#endif // !NETCOREAPP + ; + CheckExpression (e, "ExportedMemberBuilderTest_ctor", typeof(Func), - @"object (ConstructorInfo constructor, JniObjectReference reference, object[] parameters) -{ + $@"object (ConstructorInfo constructor, JniObjectReference reference, object[] parameters) +{{ Type type; object self; type = constructor.DeclaringType; - self = FormatterServices.GetUninitializedObject(type); + self = {GetUninitializedObject_decltype}.GetUninitializedObject(type); (IJavaPeerable)self.SetPeerReference(reference); constructor.Invoke(self, parameters); return self; -}"); +}}"); } } } diff --git a/tests/TestJVM/TestJVM.cs b/tests/TestJVM/TestJVM.cs index fa61be91b..8c0ff44cd 100644 --- a/tests/TestJVM/TestJVM.cs +++ b/tests/TestJVM/TestJVM.cs @@ -3,7 +3,9 @@ using System.IO; using System.Diagnostics; using System.Linq; +using System.Reflection; using System.Threading; +using System.Text; using Xamarin.Android.Tools; @@ -13,12 +15,14 @@ namespace Java.InteropTests { public class TestJVM : JreRuntime { - static JreRuntimeOptions CreateBuilder (string[] jars) + static JreRuntimeOptions CreateBuilder (string[] jars, Assembly caller) { var dir = Path.GetDirectoryName (typeof (TestJVM).Assembly.Location); var builder = new JreRuntimeOptions () { JvmLibraryPath = GetJvmLibraryPath (), JniAddNativeMethodRegistrationAttributePresent = true, + JniGlobalReferenceLogWriter = GetLogOutput ("JAVA_INTEROP_GREF_LOG", "g-", caller), + JniLocalReferenceLogWriter = GetLogOutput ("JAVA_INTEROP_LREF_LOG", "l-", caller), }; if (jars != null) { foreach (var jar in jars) @@ -30,6 +34,17 @@ static JreRuntimeOptions CreateBuilder (string[] jars) return builder; } + static TextWriter GetLogOutput (string envVar, string prefix, Assembly caller) + { + var path = Environment.GetEnvironmentVariable (envVar); + if (!string.IsNullOrEmpty (path)) + return null; + path = Path.Combine ( + Path.GetDirectoryName (typeof (TestJVM).Assembly.Location), + prefix + Path.GetFileName (caller.Location) + ".txt"); + return new StreamWriter (path, append: false, encoding: new UTF8Encoding (encoderShouldEmitUTF8Identifier: false)); + } + static string GetJvmLibraryPath () { var env = Environment.GetEnvironmentVariable ("JI_JVM_PATH"); @@ -43,7 +58,7 @@ static string GetJvmLibraryPath () Dictionary typeMappings; public TestJVM (string[] jars = null, Dictionary typeMappings = null) - : base (CreateBuilder (jars)) + : base (CreateBuilder (jars, Assembly.GetCallingAssembly ())) { this.typeMappings = typeMappings; }