diff --git a/.gitmodules b/.gitmodules index ea5f83ceb43..c734e652680 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,7 +13,7 @@ [submodule "external/Java.Interop"] path = external/Java.Interop url = https://github.com/dotnet/java-interop - branch = main + branch = dev/jonp/jonp-overridable-Throwable-ctor-arg [submodule "external/libunwind"] path = external/libunwind url = https://github.com/libunwind/libunwind.git diff --git a/external/Java.Interop b/external/Java.Interop index 4f06201b598..52e154a9e1a 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit 4f06201b598ac3816184ea7a0b71f540f6abe9d6 +Subproject commit 52e154a9e1a34bd8f17c5e8ab5284d7f6c96c09b diff --git a/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml b/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml index fa71749e460..aa0a341e0bb 100644 --- a/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml +++ b/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml @@ -9,7 +9,6 @@ - @@ -36,15 +35,7 @@ --> - - - - - - - - diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index e6de9e23399..abf30a6ef4b 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -56,7 +56,7 @@ public override string GetCurrentManagedThreadStackTrace (int skipFrames, bool f { if (!reference.IsValid) return null; - var peeked = JNIEnvInit.AndroidValueManager?.PeekPeer (reference); + var peeked = JNIEnvInit.ValueManager?.PeekPeer (reference); var peekedExc = peeked as Exception; if (peekedExc == null) { var throwable = Java.Lang.Object.GetObject (reference.Handle, JniHandleOwnership.DoNotTransfer); @@ -64,21 +64,28 @@ public override string GetCurrentManagedThreadStackTrace (int skipFrames, bool f return throwable; } JniObjectReference.Dispose (ref reference, options); - var unwrapped = JNIEnvInit.AndroidValueManager?.UnboxException (peeked!); + var unwrapped = UnboxException (peeked!); if (unwrapped != null) { return unwrapped; } return peekedExc; } + Exception? UnboxException (IJavaPeerable value) + { + if (JNIEnvInit.ValueManager is AndroidValueManager vm) { + return vm.UnboxException (value); + } + return null; + } + public override void RaisePendingException (Exception pendingException) { - var je = pendingException as JavaProxyThrowable; + var je = pendingException as JavaException; if (je == null) { je = JavaProxyThrowable.Create (pendingException); } - var r = new JniObjectReference (je.Handle); - JniEnvironment.Exceptions.Throw (r); + JniEnvironment.Exceptions.Throw (je.PeerReference); } } @@ -158,6 +165,14 @@ public override IntPtr ReleaseLocalReference (ref JniObjectReference value, ref return r; } + public override bool LogGlobalReferenceMessages => Logger.LogGlobalRef; + public override bool LogLocalReferenceMessages => Logger.LogLocalRef; + + public override void WriteLocalReferenceLine (string format, params object?[] args) + { + RuntimeNativeMethods._monodroid_gref_log ("[LREF] " + string.Format (CultureInfo.InvariantCulture, format, args)); + } + public override void WriteGlobalReferenceLine (string format, params object?[] args) { RuntimeNativeMethods._monodroid_gref_log (string.Format (CultureInfo.InvariantCulture, format, args)); @@ -470,6 +485,7 @@ static bool CallRegisterMethodByIndex (JniNativeMethodRegistrationArguments argu } } + [Obsolete ("Use RegisterNativeMembers(JniType, Type, ReadOnlySpan) instead.")] public override void RegisterNativeMembers ( JniType nativeClass, [DynamicallyAccessedMembers (MethodsAndPrivateNested)] diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 9d6ba857379..26a4e3fd5cb 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -52,7 +52,8 @@ internal static bool IsGCUserPeer (IntPtr value) if (value == IntPtr.Zero) return false; - return IsInstanceOf (value, JNIEnvInit.grefIGCUserPeer_class); + return IsInstanceOf (value, JNIEnvInit.grefIGCUserPeer_class) || + IsInstanceOf (value, JNIEnvInit.grefGCUserPeerable_class); } internal static bool ShouldWrapJavaException (Java.Lang.Throwable? t, [CallerMemberName] string? caller = null) @@ -76,49 +77,6 @@ internal static bool ShouldWrapJavaException (Java.Lang.Throwable? t, [CallerMem return wrap; } - [DllImport ("libc")] - static extern int gettid (); - - internal static void Exit () - { - /* Manually dispose surfaced objects and close the current JniEnvironment to - * avoid ObjectDisposedException thrown on finalizer threads after shutdown - */ - foreach (var surfacedObject in Java.Interop.Runtime.GetSurfacedObjects ()) { - try { - var obj = surfacedObject.Target as IDisposable; - if (obj != null) - obj.Dispose (); - continue; - } catch (Exception e) { - RuntimeNativeMethods.monodroid_log (LogLevel.Warn, LogCategories.Default, $"Couldn't dispose object: {e}"); - } - /* If calling Dispose failed, the assumption is that user-code in - * the Dispose(bool) overload is to blame for it. In that case we - * fallback to manual deletion of the surfaced object. - */ - var jobj = surfacedObject.Target as Java.Lang.Object; - if (jobj != null) - ManualJavaObjectDispose (jobj); - } - JniEnvironment.Runtime.Dispose (); - } - - /* FIXME: This reproduces the minimal steps in Java.Lang.Object.Dispose - * that needs to be executed so that we don't leak any GREF and prevent - * code execution into an appdomain that we are disposing via a finalizer. - * Ideally it should be done via another more generic mechanism, likely - * from the Java.Interop.Runtime API. - */ - static void ManualJavaObjectDispose (Java.Lang.Object obj) - { - var peer = obj.PeerReference; - var handle = peer.Handle; - var keyHandle = ((IJavaObjectEx)obj).KeyHandle; - Java.Lang.Object.Dispose (obj, ref handle, keyHandle, (JObjectRefType)peer.Type); - GC.SuppressFinalize (obj); - } - internal static void PropagateUncaughtException (IntPtr env, IntPtr javaThreadPtr, IntPtr javaExceptionPtr) { if (!JNIEnvInit.PropagateExceptions) diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index c942c6c3f43..19ff11a0913 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -32,10 +32,11 @@ internal struct JnienvInitializeArgs { public int jniAddNativeMethodRegistrationAttributePresent; public bool jniRemappingInUse; public bool marshalMethodsEnabled; + public IntPtr grefGCUserPeerable; } #pragma warning restore 0649 - internal static AndroidValueManager? AndroidValueManager; + internal static JniRuntime.JniValueManager? ValueManager; internal static bool IsRunningOnDesktop; internal static bool jniRemappingInUse; internal static bool MarshalMethodsEnabled; @@ -43,6 +44,7 @@ internal struct JnienvInitializeArgs { internal static BoundExceptionType BoundExceptionType; internal static int gref_gc_threshold; internal static IntPtr grefIGCUserPeer_class; + internal static IntPtr grefGCUserPeerable_class; internal static IntPtr java_class_loader; internal static JniMethodInfo? mid_Class_forName; @@ -94,11 +96,12 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) BoundExceptionType = (BoundExceptionType)args->ioExceptionType; androidRuntime = new AndroidRuntime (args->env, args->javaVm, args->grefLoader, args->Loader_loadClass, args->jniAddNativeMethodRegistrationAttributePresent != 0); - AndroidValueManager = (AndroidValueManager) androidRuntime.ValueManager; + ValueManager = androidRuntime.ValueManager; IsRunningOnDesktop = args->isRunningOnDesktop == 1; grefIGCUserPeer_class = args->grefIGCUserPeer; + grefGCUserPeerable_class = args->grefGCUserPeerable; PropagateExceptions = args->brokenExceptionTransitions == 0; diff --git a/src/Mono.Android/Java.Interop/IJavaObjectEx.cs b/src/Mono.Android/Java.Interop/IJavaObjectEx.cs index 4e09670fc9c..e66229d1b9b 100644 --- a/src/Mono.Android/Java.Interop/IJavaObjectEx.cs +++ b/src/Mono.Android/Java.Interop/IJavaObjectEx.cs @@ -3,9 +3,6 @@ namespace Java.Interop { interface IJavaObjectEx { - IntPtr KeyHandle {get; set;} - bool IsProxy {get; set;} - bool NeedsActivation {get; set;} IntPtr ToLocalJniHandle (); } } diff --git a/src/Mono.Android/Java.Interop/Runtime.cs b/src/Mono.Android/Java.Interop/Runtime.cs index ceb14ca9d87..8d69aedffa4 100644 --- a/src/Mono.Android/Java.Interop/Runtime.cs +++ b/src/Mono.Android/Java.Interop/Runtime.cs @@ -11,7 +11,7 @@ public static class Runtime { [Obsolete ("Please use Java.Interop.JniEnvironment.Runtime.ValueManager.GetSurfacedPeers()")] public static List GetSurfacedObjects () { - var peers = JNIEnvInit.AndroidValueManager!.GetSurfacedPeers (); + var peers = JNIEnvInit.ValueManager!.GetSurfacedPeers (); var r = new List (peers.Count); foreach (var p in peers) { if (p.SurfacedPeer.TryGetTarget (out var target)) diff --git a/src/Mono.Android/Java.Interop/TypeManager.cs b/src/Mono.Android/Java.Interop/TypeManager.cs index 04aa1f0ed5a..8b775aea7db 100644 --- a/src/Mono.Android/Java.Interop/TypeManager.cs +++ b/src/Mono.Android/Java.Interop/TypeManager.cs @@ -133,9 +133,10 @@ static Type[] GetParameterTypes (string? signature) static void n_Activate (IntPtr jnienv, IntPtr jclass, IntPtr typename_ptr, IntPtr signature_ptr, IntPtr jobject, IntPtr parameters_ptr) { var o = Java.Lang.Object.PeekObject (jobject); - var ex = o as IJavaObjectEx; + var ex = o as IJavaPeerable; if (ex != null) { - if (!ex.NeedsActivation && !ex.IsProxy) + var state = ex.JniManagedPeerState; + if (!state.HasFlag (JniManagedPeerStates.Activatable) && !state.HasFlag (JniManagedPeerStates.Replaceable)) return; } if (!ActivationEnabled) { @@ -171,10 +172,8 @@ internal static void Activate (IntPtr jobject, ConstructorInfo cinfo, object? [] { try { var newobj = RuntimeHelpers.GetUninitializedObject (cinfo.DeclaringType!); - if (newobj is Java.Lang.Object o) { - o.handle = jobject; - } else if (newobj is Java.Lang.Throwable throwable) { - throwable.handle = jobject; + if (newobj is IJavaPeerable peer) { + peer.SetPeerReference (new JniObjectReference (jobject)); } else { throw new InvalidOperationException ($"Unsupported type: '{newobj}'"); } @@ -232,7 +231,7 @@ static Exception CreateJavaLocationException () return null; } - internal static IJavaPeerable CreateInstance (IntPtr handle, JniHandleOwnership transfer) + internal static IJavaPeerable? CreateInstance (IntPtr handle, JniHandleOwnership transfer) { return CreateInstance (handle, transfer, null); } @@ -320,7 +319,7 @@ internal static IJavaPeerable CreateInstance (IntPtr handle, JniHandleOwnership try { result = (IJavaPeerable) CreateProxy (type, handle, transfer); if (Runtime.IsGCUserPeer (result.PeerReference.Handle)) { - result.SetJniManagedPeerState (JniManagedPeerStates.Replaceable); + result.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); } } catch (MissingMethodException e) { var key_handle = JNIEnv.IdentityHash (handle); @@ -353,7 +352,6 @@ internal static object CreateProxy ( JniObjectReferenceOptions o = JniObjectReferenceOptions.Copy; var peer = (IJavaPeerable) c.Invoke (new object [] { r, o }); JNIEnv.DeleteRef (handle, transfer); - peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); return peer; } throw new MissingMethodException ( diff --git a/src/Mono.Android/Java.Lang/Object.cs b/src/Mono.Android/Java.Lang/Object.cs index 9df5b5bbee9..fe6b49a8abf 100644 --- a/src/Mono.Android/Java.Lang/Object.cs +++ b/src/Mono.Android/Java.Lang/Object.cs @@ -14,68 +14,25 @@ namespace Java.Lang { [Serializable] - public partial class Object : IDisposable, IJavaObject, IJavaObjectEx -#if JAVA_INTEROP - , IJavaPeerable -#endif // JAVA_INTEROP + public partial class Object : global::Java.Interop.JavaObject, IJavaObject, IJavaObjectEx { - [NonSerialized] IntPtr key_handle; -#pragma warning disable CS0649, CS0169, CS0414 // Suppress fields are never used warnings, these fields are used directly by monodroid-glue.cc - [NonSerialized] int refs_added; -#pragma warning restore CS0649, CS0169, CS0414 - [NonSerialized] JObjectRefType handle_type; - [NonSerialized] internal IntPtr handle; - [NonSerialized] bool needsActivation; - [NonSerialized] bool isProxy; - - IntPtr IJavaObjectEx.KeyHandle { - get {return key_handle;} - set {key_handle = value;} - } - - bool IJavaObjectEx.IsProxy { - get {return isProxy;} - set {isProxy = value;} - } - - bool IJavaObjectEx.NeedsActivation { - get {return needsActivation;} - set {needsActivation = true;} - } - IntPtr IJavaObjectEx.ToLocalJniHandle () { lock (this) { - if (handle == IntPtr.Zero) - return handle; - return JNIEnv.NewLocalRef (handle); + var peerRef = PeerReference; + if (!peerRef.IsValid) + return IntPtr.Zero; + return peerRef.NewLocalRef ().Handle; } } ~Object () { - // FIXME: need hash cleanup mechanism. - // Finalization occurs after a test of java persistence. If the - // handle still contains a java reference, we can't finalize the - // object and should "resurrect" it. - refs_added = 0; - if (Environment.HasShutdownStarted) { - return; - } - JniEnvironment.Runtime.ValueManager.FinalizePeer (this); } - public Object (IntPtr handle, JniHandleOwnership transfer) + public unsafe Object (IntPtr handle, JniHandleOwnership transfer) + : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None) { - // Check if handle was preset by our java activation mechanism - if (this.handle != IntPtr.Zero) { - needsActivation = true; - handle = this.handle; - if (handle_type != 0) - return; - transfer = JniHandleOwnership.DoNotTransfer; - } - SetHandle (handle, transfer); } @@ -92,31 +49,27 @@ internal void SetHandleOnDeserialized (StreamingContext context) JNIEnv.FinishCreateInstance (Handle, "()V"); } -#if JAVA_INTEROP [EditorBrowsable (EditorBrowsableState.Never)] - public int JniIdentityHashCode { - get {return (int) key_handle;} - } + public new int JniIdentityHashCode => base.JniIdentityHashCode; [DebuggerBrowsable (DebuggerBrowsableState.Never)] [EditorBrowsable (EditorBrowsableState.Never)] - public JniObjectReference PeerReference { - get { - return new JniObjectReference (handle, (JniObjectReferenceType) handle_type); - } - } + public new JniObjectReference PeerReference => base.PeerReference; [DebuggerBrowsable (DebuggerBrowsableState.Never)] [EditorBrowsable (EditorBrowsableState.Never)] - public virtual JniPeerMembers JniPeerMembers { + public override JniPeerMembers JniPeerMembers { get { return _members; } } -#endif // JAVA_INTEROP [EditorBrowsable (EditorBrowsableState.Never)] public IntPtr Handle { get { - return handle; + var peerRef = PeerReference; + if (!peerRef.IsValid) { + return IntPtr.Zero; + } + return peerRef.Handle; } } @@ -142,115 +95,29 @@ internal System.Type GetThresholdType () return ThresholdType; } -#if JAVA_INTEROP - JniManagedPeerStates IJavaPeerable.JniManagedPeerState { - get { - var e = (IJavaObjectEx) this; - var s = JniManagedPeerStates.None; - if (e.IsProxy) - s |= JniManagedPeerStates.Replaceable; - if (e.NeedsActivation) - s |= JniManagedPeerStates.Activatable; - return s; - } - } - - void IJavaPeerable.DisposeUnlessReferenced () - { - var p = PeekObject (handle); - if (p == null) { - Dispose (); - } - } - [EditorBrowsable (EditorBrowsableState.Never)] - public void UnregisterFromRuntime () - { - JNIEnvInit.AndroidValueManager?.RemovePeer (this, key_handle); - } - - void IJavaPeerable.Disposed () - { - Dispose (disposing: true); - } - - void IJavaPeerable.Finalized () - { - Dispose (disposing: false); - } - - void IJavaPeerable.SetJniIdentityHashCode (int value) - { - key_handle = (IntPtr) value; - } - - void IJavaPeerable.SetJniManagedPeerState (JniManagedPeerStates value) - { - var e = (IJavaObjectEx) this; - if ((value & JniManagedPeerStates.Replaceable) == JniManagedPeerStates.Replaceable) - e.IsProxy = true; - if ((value & JniManagedPeerStates.Activatable) == JniManagedPeerStates.Activatable) - e.NeedsActivation = true; - } - - void IJavaPeerable.SetPeerReference (JniObjectReference reference) - { - this.handle = reference.Handle; - this.handle_type = (JObjectRefType) reference.Type; - } -#endif // JAVA_INTEROP - + public new void UnregisterFromRuntime () => base.UnregisterFromRuntime (); - public void Dispose () + protected override void Dispose (bool disposing) { - JNIEnvInit.AndroidValueManager?.DisposePeer (this); } - protected virtual void Dispose (bool disposing) - { - } - - internal static void Dispose (IJavaPeerable instance, ref IntPtr handle, IntPtr key_handle, JObjectRefType handle_type) - { - if (handle == IntPtr.Zero) - return; - - if (Logger.LogGlobalRef) { - RuntimeNativeMethods._monodroid_gref_log ( - FormattableString.Invariant ($"Disposing handle 0x{handle:x}\n")); - } - - JNIEnvInit.AndroidValueManager?.RemovePeer (instance, key_handle); - - switch (handle_type) { - case JObjectRefType.Global: - lock (instance) { - JNIEnv.DeleteGlobalRef (handle); - handle = IntPtr.Zero; - } - break; - case JObjectRefType.WeakGlobal: - lock (instance) { - JNIEnv.DeleteWeakGlobalRef (handle); - handle = IntPtr.Zero; - } - break; - default: - throw new InvalidOperationException ("Trying to dispose handle of type '" + - handle_type + "' which is not supported."); - } - } + public new void Dispose () => base.Dispose (); [EditorBrowsable (EditorBrowsableState.Never)] protected void SetHandle (IntPtr value, JniHandleOwnership transfer) { - JNIEnvInit.AndroidValueManager?.AddPeer (this, value, transfer, out handle); - handle_type = JObjectRefType.Global; + var reference = new JniObjectReference (value); + JNIEnvInit.ValueManager?.ConstructPeer ( + this, + ref reference, + value == IntPtr.Zero ? JniObjectReferenceOptions.None : JniObjectReferenceOptions.Copy); + JNIEnv.DeleteRef (value, transfer); } internal static IJavaPeerable? PeekObject (IntPtr handle, Type? requiredType = null) { - var peeked = JNIEnvInit.AndroidValueManager?.PeekPeer (new JniObjectReference (handle)); + var peeked = JNIEnvInit.ValueManager?.PeekPeer (new JniObjectReference (handle)); if (peeked == null) return null; if (requiredType != null && !requiredType.IsAssignableFrom (peeked.GetType ())) diff --git a/src/Mono.Android/Java.Lang/Throwable.cs b/src/Mono.Android/Java.Lang/Throwable.cs index 498335e5d8e..a6dcdb4422f 100644 --- a/src/Mono.Android/Java.Lang/Throwable.cs +++ b/src/Mono.Android/Java.Lang/Throwable.cs @@ -9,191 +9,59 @@ namespace Java.Lang { - public partial class Throwable : global::System.Exception, IJavaObject, IDisposable, IJavaObjectEx -#if JAVA_INTEROP - , IJavaPeerable -#endif // JAVA_INTEROP + public partial class Throwable : JavaException, IJavaObject, IDisposable, IJavaObjectEx { - protected bool is_generated; - internal IntPtr handle; - - IntPtr key_handle; - JObjectRefType handle_type; -#pragma warning disable CS0649, CS0169, CS0414 // Suppress fields are never used warnings, these fields are used directly by monodroid-glue.cc - int refs_added; -#pragma warning restore CS0649, CS0169, CS0414 - - bool isProxy; - bool needsActivation; - string? nativeStack; - - public Throwable (IntPtr handle, JniHandleOwnership transfer) - : base (_GetMessage (handle), _GetInnerException (handle)) + public unsafe Throwable (IntPtr handle, JniHandleOwnership transfer) + : base (ref *InvalidJniObjectReference, JniObjectReferenceOptions.None, new JniObjectReference (handle)) { if (GetType () == typeof (Throwable)) is_generated = true; - // Check if handle was preset by our java activation mechanism - if (this.handle != IntPtr.Zero) { - needsActivation = true; - handle = this.handle; - if (handle_type != 0) - return; - transfer = JniHandleOwnership.DoNotTransfer; - } - SetHandle (handle, transfer); } -#if JAVA_INTEROP - static JniMethodInfo? Throwable_getMessage; -#endif - - static string? _GetMessage (IntPtr handle) - { - if (handle == IntPtr.Zero) - return null; - - IntPtr value; -#if JAVA_INTEROP - const string __id = "getMessage.()Ljava/lang/String;"; - if (Throwable_getMessage == null) { - Throwable_getMessage = _members.InstanceMethods.GetMethodInfo (__id); - } - value = JniEnvironment.InstanceMethods.CallObjectMethod (new JniObjectReference (handle), Throwable_getMessage).Handle; -#else // !JAVA_INTEROP - if (id_getMessage == IntPtr.Zero) - id_getMessage = JNIEnv.GetMethodID (class_ref, "getMessage", "()Ljava/lang/String;"); - value = JNIEnv.CallObjectMethod (handle, id_getMessage); -#endif // !JAVA_INTEROP - - return JNIEnv.GetString (value, JniHandleOwnership.TransferLocalRef); - } - -#if JAVA_INTEROP - static JniMethodInfo? Throwable_getCause; -#endif - - static global::System.Exception? _GetInnerException (IntPtr handle) - { - if (handle == IntPtr.Zero) - return null; - - IntPtr value; -#if JAVA_INTEROP - const string __id = "getCause.()Ljava/lang/Throwable;"; - if (Throwable_getCause == null) { - Throwable_getCause = _members.InstanceMethods.GetMethodInfo (__id); - } - value = JniEnvironment.InstanceMethods.CallObjectMethod (new JniObjectReference (handle), Throwable_getCause).Handle; -#else // !JAVA_INTEROP - if (id_getCause == IntPtr.Zero) - id_getCause = JNIEnv.GetMethodID (class_ref, "getCause", "()Ljava/lang/Throwable;"); - - value = JNIEnv.CallObjectMethod (handle, id_getCause); -#endif // !JAVA_INTEROP - - var cause = global::Java.Lang.Object.GetObject ( - value, - JniHandleOwnership.TransferLocalRef); - - var proxy = cause as JavaProxyThrowable; - if (proxy != null) - return proxy.InnerException; - - return cause; - } - - IntPtr IJavaObjectEx.KeyHandle { - get {return key_handle;} - set {key_handle = value;} - } - - bool IJavaObjectEx.IsProxy { - get {return isProxy;} - set {isProxy = value;} - } - - bool IJavaObjectEx.NeedsActivation { - get {return needsActivation;} - set {needsActivation = true;} - } - IntPtr IJavaObjectEx.ToLocalJniHandle () { lock (this) { - if (handle == IntPtr.Zero) - return handle; - return JNIEnv.NewLocalRef (handle); - } - } - - public override string StackTrace { - get { - return base.StackTrace + ManagedStackTraceAddendum; - } - } - - string ManagedStackTraceAddendum { - get { - var javaStack = JavaStackTrace; - if (string.IsNullOrEmpty (javaStack)) - return ""; - return Environment.NewLine + - " --- End of managed " + - GetType ().FullName + - " stack trace ---" + Environment.NewLine + - javaStack; + var peerRef = PeerReference; + if (!peerRef.IsValid) + return IntPtr.Zero; + return peerRef.NewLocalRef ().Handle; } } - string? JavaStackTrace { - get { - if (!string.IsNullOrEmpty (nativeStack)) - return nativeStack; - - if (handle == IntPtr.Zero) - return null; - - using (var nativeStackWriter = new Java.IO.StringWriter ()) - using (var nativeStackPw = new Java.IO.PrintWriter (nativeStackWriter)) { - PrintStackTrace (nativeStackPw); - nativeStack = nativeStackWriter.ToString (); - } - return nativeStack; - } - } + public override string StackTrace => base.StackTrace; public override string ToString () { - return base.ToString () + ManagedStackTraceAddendum; + return base.ToString (); } -#if JAVA_INTEROP [EditorBrowsable (EditorBrowsableState.Never)] - public int JniIdentityHashCode { - get {return (int) key_handle;} - } + public new int JniIdentityHashCode => base.JniIdentityHashCode; [DebuggerBrowsable (DebuggerBrowsableState.Never)] [EditorBrowsable (EditorBrowsableState.Never)] - public JniObjectReference PeerReference { - get { - return new JniObjectReference (handle, (JniObjectReferenceType) handle_type); - } - } + public new JniObjectReference PeerReference => base.PeerReference; [DebuggerBrowsable (DebuggerBrowsableState.Never)] [EditorBrowsable (EditorBrowsableState.Never)] - public virtual JniPeerMembers JniPeerMembers { + public override JniPeerMembers JniPeerMembers { get { return _members; } } -#endif // JAVA_INTEROP [EditorBrowsable (EditorBrowsableState.Never)] - public IntPtr Handle { get { return handle; } } + public IntPtr Handle { + get { + var peerRef = PeerReference; + if (!peerRef.IsValid) { + return IntPtr.Zero; + } + return peerRef.Handle; + } + } [DebuggerBrowsable (DebuggerBrowsableState.Never)] [EditorBrowsable (EditorBrowsableState.Never)] @@ -217,22 +85,12 @@ internal System.Type GetThresholdType () return ThresholdType; } -#if !JAVA_INTEROP - static IntPtr id_getClass; -#endif // !JAVA_INTEROP - public unsafe Java.Lang.Class? Class { [Register ("getClass", "()Ljava/lang/Class;", "GetGetClassHandler")] get { IntPtr value; -#if JAVA_INTEROP const string __id = "getClass.()Ljava/lang/Class;"; value = _members.InstanceMethods.InvokeVirtualObjectMethod (__id, this, null).Handle; -#else // !JAVA_INTEROP - if (id_getClass == IntPtr.Zero) - id_getClass = JNIEnv.GetMethodID (class_ref, "getClass", "()Ljava/lang/Class;"); - value = JNIEnv.CallObjectMethod (Handle, id_getClass); -#endif // !JAVA_INTEROP return global::Java.Lang.Object.GetObject (value, JniHandleOwnership.TransferLocalRef); } } @@ -240,8 +98,15 @@ public unsafe Java.Lang.Class? Class { [EditorBrowsable (EditorBrowsableState.Never)] protected void SetHandle (IntPtr value, JniHandleOwnership transfer) { - JNIEnvInit.AndroidValueManager?.AddPeer (this, value, transfer, out handle); - handle_type = JObjectRefType.Global; + var reference = new JniObjectReference (value); + + Construct ( + ref reference, + value == IntPtr.Zero ? JniObjectReferenceOptions.None : JniObjectReferenceOptions.Copy); + if (value != IntPtr.Zero) { + SetJavaStackTrace (new JniObjectReference (value)); + } + JNIEnv.DeleteRef (value, transfer); } public static Throwable FromException (System.Exception e) @@ -265,76 +130,13 @@ public static System.Exception ToException (Throwable e) ~Throwable () { - refs_added = 0; - if (Environment.HasShutdownStarted) { - return; - } - JniEnvironment.Runtime.ValueManager.FinalizePeer (this); - } - -#if JAVA_INTEROP - JniManagedPeerStates IJavaPeerable.JniManagedPeerState { - get { - var e = (IJavaObjectEx) this; - var s = JniManagedPeerStates.None; - if (e.IsProxy) - s |= JniManagedPeerStates.Replaceable; - if (e.NeedsActivation) - s |= JniManagedPeerStates.Activatable; - return s; - } } - void IJavaPeerable.DisposeUnlessReferenced () - { - var p = Object.PeekObject (handle); - if (p == null) { - Dispose (); - } - } + public new void UnregisterFromRuntime () => base.UnregisterFromRuntime (); - public void UnregisterFromRuntime () - { - JNIEnvInit.AndroidValueManager?.RemovePeer (this, key_handle); - } - - void IJavaPeerable.Disposed () - { - Dispose (disposing: true); - } - - void IJavaPeerable.Finalized () - { - Dispose (disposing: false); - } - - void IJavaPeerable.SetJniIdentityHashCode (int value) - { - key_handle = (IntPtr) value; - } - - void IJavaPeerable.SetJniManagedPeerState (JniManagedPeerStates value) - { - var e = (IJavaObjectEx) this; - if ((value & JniManagedPeerStates.Replaceable) == JniManagedPeerStates.Replaceable) - e.IsProxy = true; - if ((value & JniManagedPeerStates.Activatable) == JniManagedPeerStates.Activatable) - e.NeedsActivation = true; - } - - void IJavaPeerable.SetPeerReference (JniObjectReference reference) - { - this.handle = reference.Handle; - this.handle_type = (JObjectRefType) reference.Type; - } -#endif // JAVA_INTEROP - - public void Dispose () - { - JNIEnvInit.AndroidValueManager?.DisposePeer (this); - } + public new void Dispose () => base.Dispose (); - protected virtual void Dispose (bool disposing) + protected override void Dispose (bool disposing) { } } diff --git a/src/Mono.Android/PublicAPI/API-35/PublicAPI.Shipped.txt b/src/Mono.Android/PublicAPI/API-35/PublicAPI.Shipped.txt index a015c8c4a8f..d71147178cd 100644 --- a/src/Mono.Android/PublicAPI/API-35/PublicAPI.Shipped.txt +++ b/src/Mono.Android/PublicAPI/API-35/PublicAPI.Shipped.txt @@ -125925,10 +125925,10 @@ virtual Java.Lang.NoSuchMethodException.Clause.get -> Java.Lang.Throwable? virtual Java.Lang.Number.ByteValue() -> sbyte virtual Java.Lang.Number.ShortValue() -> short virtual Java.Lang.Object.Clone() -> Java.Lang.Object! -virtual Java.Lang.Object.Dispose(bool disposing) -> void +override Java.Lang.Object.Dispose(bool disposing) -> void virtual Java.Lang.Object.Equals(Java.Lang.Object? obj) -> bool virtual Java.Lang.Object.JavaFinalize() -> void -virtual Java.Lang.Object.JniPeerMembers.get -> Java.Interop.JniPeerMembers! +override Java.Lang.Object.JniPeerMembers.get -> Java.Interop.JniPeerMembers! virtual Java.Lang.Object.ThresholdClass.get -> nint virtual Java.Lang.Object.ThresholdType.get -> System.Type! virtual Java.Lang.Package.GetAnnotation(Java.Lang.Class? annotationClass) -> Java.Lang.Object? @@ -126069,11 +126069,11 @@ virtual Java.Lang.ThreadLocal.InitialValue() -> Java.Lang.Object? virtual Java.Lang.ThreadLocal.Remove() -> void virtual Java.Lang.ThreadLocal.Set(Java.Lang.Object? value) -> void virtual Java.Lang.Throwable.Cause.get -> Java.Lang.Throwable? -virtual Java.Lang.Throwable.Dispose(bool disposing) -> void +override Java.Lang.Throwable.Dispose(bool disposing) -> void virtual Java.Lang.Throwable.FillInStackTrace() -> Java.Lang.Throwable! virtual Java.Lang.Throwable.GetStackTrace() -> Java.Lang.StackTraceElement![]! virtual Java.Lang.Throwable.InitCause(Java.Lang.Throwable? cause) -> Java.Lang.Throwable! -virtual Java.Lang.Throwable.JniPeerMembers.get -> Java.Interop.JniPeerMembers! +override Java.Lang.Throwable.JniPeerMembers.get -> Java.Interop.JniPeerMembers! virtual Java.Lang.Throwable.LocalizedMessage.get -> string? virtual Java.Lang.Throwable.Message.get -> string? virtual Java.Lang.Throwable.PrintStackTrace() -> void diff --git a/src/Mono.Android/PublicAPI/API-36/PublicAPI.Shipped.txt b/src/Mono.Android/PublicAPI/API-36/PublicAPI.Shipped.txt index f53c05485ad..fd95b1c938b 100644 --- a/src/Mono.Android/PublicAPI/API-36/PublicAPI.Shipped.txt +++ b/src/Mono.Android/PublicAPI/API-36/PublicAPI.Shipped.txt @@ -125925,10 +125925,10 @@ virtual Java.Lang.NoSuchMethodException.Clause.get -> Java.Lang.Throwable? virtual Java.Lang.Number.ByteValue() -> sbyte virtual Java.Lang.Number.ShortValue() -> short virtual Java.Lang.Object.Clone() -> Java.Lang.Object! -virtual Java.Lang.Object.Dispose(bool disposing) -> void +override Java.Lang.Object.Dispose(bool disposing) -> void virtual Java.Lang.Object.Equals(Java.Lang.Object? obj) -> bool virtual Java.Lang.Object.JavaFinalize() -> void -virtual Java.Lang.Object.JniPeerMembers.get -> Java.Interop.JniPeerMembers! +override Java.Lang.Object.JniPeerMembers.get -> Java.Interop.JniPeerMembers! virtual Java.Lang.Object.ThresholdClass.get -> nint virtual Java.Lang.Object.ThresholdType.get -> System.Type! virtual Java.Lang.Package.GetAnnotation(Java.Lang.Class? annotationClass) -> Java.Lang.Object? @@ -126069,11 +126069,11 @@ virtual Java.Lang.ThreadLocal.InitialValue() -> Java.Lang.Object? virtual Java.Lang.ThreadLocal.Remove() -> void virtual Java.Lang.ThreadLocal.Set(Java.Lang.Object? value) -> void virtual Java.Lang.Throwable.Cause.get -> Java.Lang.Throwable? -virtual Java.Lang.Throwable.Dispose(bool disposing) -> void +override Java.Lang.Throwable.Dispose(bool disposing) -> void virtual Java.Lang.Throwable.FillInStackTrace() -> Java.Lang.Throwable! virtual Java.Lang.Throwable.GetStackTrace() -> Java.Lang.StackTraceElement![]! virtual Java.Lang.Throwable.InitCause(Java.Lang.Throwable? cause) -> Java.Lang.Throwable! -virtual Java.Lang.Throwable.JniPeerMembers.get -> Java.Interop.JniPeerMembers! +override Java.Lang.Throwable.JniPeerMembers.get -> Java.Interop.JniPeerMembers! virtual Java.Lang.Throwable.LocalizedMessage.get -> string? virtual Java.Lang.Throwable.Message.get -> string? virtual Java.Lang.Throwable.PrintStackTrace() -> void diff --git a/src/java-runtime/java/mono/android/Runtime.java b/src/java-runtime/java/mono/android/Runtime.java index aacc0e9fd78..8ad03b1a2c5 100644 --- a/src/java-runtime/java/mono/android/Runtime.java +++ b/src/java-runtime/java/mono/android/Runtime.java @@ -9,6 +9,7 @@ public class Runtime { static java.lang.Class java_util_TimeZone = java.util.TimeZone.class; static java.lang.Class mono_android_IGCUserPeer = mono.android.IGCUserPeer.class; static java.lang.Class mono_android_GCUserPeer = mono.android.GCUserPeer.class; + static java.lang.Class net_dot_jni_GCUserPeerable = net.dot.jni.GCUserPeerable.class; static { Thread.setDefaultUncaughtExceptionHandler (new XamarinUncaughtExceptionHandler (Thread.getDefaultUncaughtExceptionHandler ())); diff --git a/src/native/monodroid/monodroid-glue-designer.cc b/src/native/monodroid/monodroid-glue-designer.cc deleted file mode 100644 index 8e8d6ab4cba..00000000000 --- a/src/native/monodroid/monodroid-glue-designer.cc +++ /dev/null @@ -1,147 +0,0 @@ -// -// Android designer support code, not used on devices -// -#include "globals.hh" -#include "mono_android_Runtime.h" -#include "monodroid-glue-internal.hh" - -using namespace xamarin::android::internal; - -// DO NOT USE ON NORMAL X.A -// This function only works with the custom TypeManager embedded with the designer process. -force_inline static void -reinitialize_android_runtime_type_manager (JNIEnv *env) -{ - jclass typeManager = env->FindClass ("mono/android/TypeManager"); - env->UnregisterNatives (typeManager); - - jmethodID resetRegistration = env->GetStaticMethodID (typeManager, "resetRegistration", "()V"); - env->CallStaticVoidMethod (typeManager, resetRegistration); - - env->DeleteLocalRef (typeManager); -} - -inline void -MonodroidRuntime::shutdown_android_runtime (MonoDomain *domain) -{ - MonoClass *runtime = get_android_runtime_class (domain); - MonoMethod *method = mono_class_get_method_from_name (runtime, "Exit", 0); - - utils.monodroid_runtime_invoke (domain, method, nullptr, nullptr, nullptr); -} - -inline jint -MonodroidRuntime::Java_mono_android_Runtime_createNewContextWithData (JNIEnv *env, jclass klass, jobjectArray runtimeApksJava, jobjectArray assembliesJava, - jobjectArray assembliesBytes, jobjectArray assembliesPaths, jobject loader, jboolean force_preload_assemblies) -{ - log_info (LOG_DEFAULT, "CREATING NEW CONTEXT"); - reinitialize_android_runtime_type_manager (env); - MonoDomain *root_domain = mono_get_root_domain (); - mono_jit_thread_attach (root_domain); - - jstring_array_wrapper runtimeApks (env, runtimeApksJava); - jstring_array_wrapper assemblies (env, assembliesJava); - jstring_array_wrapper assembliePaths (env, assembliesPaths); - MonoDomain *domain = create_and_initialize_domain (env, klass, runtimeApks, assemblies, assembliesBytes, assembliePaths, loader, /*is_root_domain:*/ false, force_preload_assemblies, /* have_split_apks */ false); - mono_domain_set (domain, FALSE); - int domain_id = mono_domain_get_id (domain); - current_context_id = domain_id; - log_info (LOG_DEFAULT, "Created new context with id %d\n", domain_id); - return domain_id; -} - -inline void -MonodroidRuntime::Java_mono_android_Runtime_switchToContext (JNIEnv *env, jint contextID) -{ - log_info (LOG_DEFAULT, "SWITCHING CONTEXT"); - MonoDomain *domain = mono_domain_get_by_id ((int)contextID); - if (current_context_id != (int)contextID) { - mono_domain_set (domain, TRUE); - // Reinitialize TypeManager so that its JNI handle goes into the right domain - reinitialize_android_runtime_type_manager (env); - } - current_context_id = (int)contextID; -} - -inline void -MonodroidRuntime::Java_mono_android_Runtime_destroyContexts (JNIEnv *env, jintArray array) -{ - MonoDomain *root_domain = mono_get_root_domain (); - mono_jit_thread_attach (root_domain); - current_context_id = -1; - - jint *contextIDs = env->GetIntArrayElements (array, nullptr); - jsize count = env->GetArrayLength (array); - - log_info (LOG_DEFAULT, "Cleaning %d domains", count); - - for (jsize i = 0; i < count; i++) { - int domain_id = contextIDs[i]; - MonoDomain *domain = mono_domain_get_by_id (domain_id); - - if (domain == nullptr) - continue; - log_info (LOG_DEFAULT, "Shutting down domain `%d'", contextIDs[i]); - shutdown_android_runtime (domain); - osBridge.remove_monodroid_domain (domain); - designerAssemblies.clear_for_domain (domain); - } - osBridge.on_destroy_contexts (); - for (jsize i = 0; i < count; i++) { - int domain_id = contextIDs[i]; - MonoDomain *domain = mono_domain_get_by_id (domain_id); - - if (domain == nullptr) - continue; - log_info (LOG_DEFAULT, "Unloading domain `%d'", contextIDs[i]); - mono_domain_unload (domain); - } - env->ReleaseIntArrayElements (array, contextIDs, JNI_ABORT); - - reinitialize_android_runtime_type_manager (env); - - log_info (LOG_DEFAULT, "All domain cleaned up"); -} - -JNIEXPORT jint -JNICALL Java_mono_android_Runtime_createNewContextWithData (JNIEnv *env, jclass klass, jobjectArray runtimeApksJava, jobjectArray assembliesJava, jobjectArray assembliesBytes, jobjectArray assembliesPaths, jobject loader, jboolean force_preload_assemblies) -{ - return monodroidRuntime.Java_mono_android_Runtime_createNewContextWithData ( - env, - klass, - runtimeApksJava, - assembliesJava, - assembliesBytes, - assembliesPaths, - loader, - force_preload_assemblies - ); -} - -/* !DO NOT REMOVE! Used by older versions of the Android Designer (pre-16.4) */ -JNIEXPORT jint -JNICALL Java_mono_android_Runtime_createNewContext (JNIEnv *env, jclass klass, jobjectArray runtimeApksJava, jobjectArray assembliesJava, jobject loader) -{ - return monodroidRuntime.Java_mono_android_Runtime_createNewContextWithData ( - env, - klass, - runtimeApksJava, - assembliesJava, - nullptr, // assembliesBytes - nullptr, // assembliesPaths - loader, - false // force_preload_assemblies - ); -} - -JNIEXPORT void -JNICALL Java_mono_android_Runtime_switchToContext (JNIEnv *env, [[maybe_unused]] jclass klass, jint contextID) -{ - monodroidRuntime.Java_mono_android_Runtime_switchToContext (env, contextID); -} - -JNIEXPORT void -JNICALL Java_mono_android_Runtime_destroyContexts (JNIEnv *env, [[maybe_unused]] jclass klass, jintArray array) -{ - monodroidRuntime.Java_mono_android_Runtime_destroyContexts (env, array); -} diff --git a/src/native/monodroid/monodroid-glue-internal.hh b/src/native/monodroid/monodroid-glue-internal.hh index da7f9f6be2c..c32882702bb 100644 --- a/src/native/monodroid/monodroid-glue-internal.hh +++ b/src/native/monodroid/monodroid-glue-internal.hh @@ -101,6 +101,7 @@ namespace xamarin::android::internal int jniAddNativeMethodRegistrationAttributePresent; bool jniRemappingInUse; bool marshalMethodsEnabled; + jobject grefGCUserPeerable; }; using jnienv_initialize_fn = void (*) (JnienvInitializeArgs*); diff --git a/src/native/monodroid/monodroid-glue.cc b/src/native/monodroid/monodroid-glue.cc index 7a28622554f..94e4944616c 100644 --- a/src/native/monodroid/monodroid-glue.cc +++ b/src/native/monodroid/monodroid-glue.cc @@ -930,6 +930,7 @@ MonodroidRuntime::init_android_runtime (JNIEnv *env, jclass runtimeClass, jobjec init.grefLoader = env->NewGlobalRef (loader); init.grefIGCUserPeer = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, "mono_android_IGCUserPeer", true); + init.grefGCUserPeerable = RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, "net_dot_jni_GCUserPeerable", true); osBridge.initialize_on_runtime_init (env, runtimeClass); diff --git a/src/native/monodroid/osbridge.cc b/src/native/monodroid/osbridge.cc index 4d03b3600bb..919506d3fda 100644 --- a/src/native/monodroid/osbridge.cc +++ b/src/native/monodroid/osbridge.cc @@ -23,8 +23,6 @@ using namespace xamarin::android; using namespace xamarin::android::internal; const OSBridge::MonoJavaGCBridgeType OSBridge::mono_xa_gc_bridge_types[] = { - { "Java.Lang", "Object" }, - { "Java.Lang", "Throwable" }, }; const OSBridge::MonoJavaGCBridgeType OSBridge::mono_ji_gc_bridge_types[] = { diff --git a/tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs b/tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs index a1efbb26591..420544ed116 100644 --- a/tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs +++ b/tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs @@ -287,6 +287,8 @@ public void ActivatedDirectThrowableSubclassesShouldBeRegistered () { if (Build.VERSION.SdkInt <= BuildVersionCodes.GingerbreadMr1) Assert.Ignore ("Skipping test due to Bug #34141"); + + Console.Error.WriteLine ($"# jonp: BEGIN ActivatedDirectThrowableSubclassesShouldBeRegistered!!!"); using (var ThrowableActivatedFromJava_class = Java.Lang.Class.FromType (typeof (ThrowableActivatedFromJava))) { var ThrowableActivatedFromJava_init = JNIEnv.GetMethodID (ThrowableActivatedFromJava_class.Handle, "", "()V"); @@ -302,6 +304,7 @@ public void ActivatedDirectThrowableSubclassesShouldBeRegistered () Assert.IsTrue (v.Constructed); v.Dispose (); } + Console.Error.WriteLine ($"# jonp: END ActivatedDirectThrowableSubclassesShouldBeRegistered!!!"); } [Test] diff --git a/tests/Mono.Android-Tests/System/ExceptionTest.cs b/tests/Mono.Android-Tests/System/ExceptionTest.cs index a1511e393d2..017308edf7c 100644 --- a/tests/Mono.Android-Tests/System/ExceptionTest.cs +++ b/tests/Mono.Android-Tests/System/ExceptionTest.cs @@ -43,6 +43,9 @@ public void InnerExceptionIsSet () using var source = new Java.Lang.Throwable ("detailMessage", proxy); using var alias = new Java.Lang.Throwable (source.Handle, JniHandleOwnership.DoNotTransfer); + Console.Error.WriteLine ("# jonp: InnerExceptionIsSet: ex={0}", ex); + Console.Error.WriteLine ("# jonp: InnerExceptionIsSet: proxy={0}", proxy); + CompareStackTraces (ex, proxy); Assert.AreEqual ("detailMessage", alias.Message); Assert.AreSame (ex, alias.InnerException); @@ -51,7 +54,7 @@ public void InnerExceptionIsSet () [RequiresUnreferencedCode ("Tests trimming unsafe features")] void CompareStackTraces (Exception ex, Java.Lang.Throwable throwable) { - var managedTrace = new StackTrace (ex); + var managedTrace = new StackTrace (ex, fNeedFileInfo: true); StackFrame[] managedFrames = managedTrace.GetFrames (); Java.Lang.StackTraceElement[] javaFrames = throwable.GetStackTrace (); @@ -68,15 +71,21 @@ void CompareStackTraces (Exception ex, Java.Lang.Throwable throwable) managedLine = mf.HasNativeImage () ? -2 : -1; } + Console.WriteLine ("# jonp: CompareStackTraces: managedFrame[{0}]: {1}", i, mf); + Console.WriteLine ("# jonp: CompareStackTraces: javaFrame[{0}]: {1}", i, jf); if (managedLine > 0) { Assert.AreEqual (mf.GetMethod ()?.Name, jf.MethodName, $"Frame {i}: method names differ"); } else { string managedMethodName = mf.GetMethod ()?.Name ?? String.Empty; - Assert.IsTrue (jf.MethodName.StartsWith ($"{managedMethodName} + 0x"), $"Frame {i}: method name should start with: '{managedMethodName} + 0x'"); + Assert.IsTrue (jf.MethodName.StartsWith ($"{managedMethodName} + 0x"), + $"Frame {i}: method name should start with: '{managedMethodName} + 0x'; was `{jf.MethodName}`; "); } - Assert.AreEqual (mf.GetMethod ()?.DeclaringType.FullName, jf.ClassName, $"Frame {i}: class names differ"); - Assert.AreEqual (mf.GetFileName (), jf.FileName, $"Frame {i}: file names differ"); - Assert.AreEqual (managedLine, jf.LineNumber, $"Frame {i}: line numbers differ"); + Assert.AreEqual (mf.GetMethod ()?.DeclaringType.FullName, jf.ClassName, + $"Frame {i}: class names differ: `{mf.GetMethod ()?.DeclaringType.FullName}` != `{jf.ClassName}`"); + Assert.AreEqual (mf.GetFileName (), jf.FileName, + $"Frame {i}: file names differ: `{mf.GetFileName ()}` != `{jf.FileName}`"); + Assert.AreEqual (managedLine, jf.LineNumber, + $"Frame {i}: line numbers differ: {managedLine} != {jf.LineNumber}"); } } }