diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index fafdc00e8b9..9e67d00721c 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -79,7 +79,7 @@ public override void RaisePendingException (Exception pendingException) { var je = pendingException as JavaProxyThrowable; if (je == null) { - je = new JavaProxyThrowable (pendingException); + je = JavaProxyThrowable.Create (pendingException); } var r = new JniObjectReference (je.Handle); JniEnvironment.Exceptions.Throw (r); diff --git a/src/Mono.Android/Android.Runtime/JavaProxyThrowable.cs b/src/Mono.Android/Android.Runtime/JavaProxyThrowable.cs index 89f4d01844e..7e350980638 100644 --- a/src/Mono.Android/Android.Runtime/JavaProxyThrowable.cs +++ b/src/Mono.Android/Android.Runtime/JavaProxyThrowable.cs @@ -1,23 +1,83 @@ using System; +using System.Diagnostics; +using System.Reflection; + +using StackTraceElement = Java.Lang.StackTraceElement; namespace Android.Runtime { - class JavaProxyThrowable : Java.Lang.Error { + sealed class JavaProxyThrowable : Java.Lang.Error { public readonly Exception InnerException; - public JavaProxyThrowable (Exception innerException) - : base (GetDetailMessage (innerException)) + JavaProxyThrowable (string message, Exception innerException) + : base (message) { InnerException = innerException; } - static string GetDetailMessage (Exception innerException) + public static JavaProxyThrowable Create (Exception innerException) + { + if (innerException == null) { + throw new ArgumentNullException (nameof (innerException)); + } + + // We prepend managed exception type to message since Java will see `JavaProxyThrowable` instead. + var proxy = new JavaProxyThrowable ($"[{innerException.GetType ()}]: {innerException.Message}", innerException); + + try { + proxy.TranslateStackTrace (); + } catch (Exception ex) { + // We shouldn't throw here, just try to do the best we can do + Console.WriteLine ($"JavaProxyThrowable: translation threw an exception: {ex}"); + proxy = new JavaProxyThrowable (innerException.ToString (), innerException); + } + + return proxy; + } + + void TranslateStackTrace () { - if (innerException == null) - throw new ArgumentNullException ("innerException"); + var trace = new StackTrace (InnerException, fNeedFileInfo: true); + if (trace.FrameCount <= 0) { + return; + } + + StackTraceElement[]? javaTrace = null; + try { + javaTrace = GetStackTrace (); + } catch (Exception ex) { + // Report... + Console.WriteLine ($"JavaProxyThrowable: obtaining Java stack trace threw an exception: {ex}"); + // ..but ignore + } + + + StackFrame[] frames = trace.GetFrames (); + int nElements = frames.Length + (javaTrace?.Length ?? 0); + StackTraceElement[] elements = new StackTraceElement[nElements]; + + for (int i = 0; i < frames.Length; i++) { + StackFrame managedFrame = frames[i]; + MethodBase? managedMethod = managedFrame.GetMethod (); + + var throwableFrame = new StackTraceElement ( + declaringClass: managedMethod?.DeclaringType?.FullName, + methodName: managedMethod?.Name, + fileName: managedFrame?.GetFileName (), + lineNumber: managedFrame?.GetFileLineNumber () ?? -1 + ); + + elements[i] = throwableFrame; + } + + if (javaTrace != null) { + for (int i = frames.Length; i < nElements; i++) { + elements[i] = javaTrace[i - frames.Length]; + } + } - return innerException.ToString (); + SetStackTrace (elements); } } } diff --git a/src/Mono.Android/Java.Lang/Throwable.cs b/src/Mono.Android/Java.Lang/Throwable.cs index 5dc63ef7952..ae668e24c27 100644 --- a/src/Mono.Android/Java.Lang/Throwable.cs +++ b/src/Mono.Android/Java.Lang/Throwable.cs @@ -253,7 +253,7 @@ public static Throwable FromException (System.Exception e) if (e is Throwable) return (Throwable) e; - return new Android.Runtime.JavaProxyThrowable (e); + return Android.Runtime.JavaProxyThrowable.Create (e); } public static System.Exception ToException (Throwable e) diff --git a/tests/Mono.Android-Tests/System/ExceptionTest.cs b/tests/Mono.Android-Tests/System/ExceptionTest.cs index cc8aaab7a50..0c2c1fd847b 100644 --- a/tests/Mono.Android-Tests/System/ExceptionTest.cs +++ b/tests/Mono.Android-Tests/System/ExceptionTest.cs @@ -1,5 +1,7 @@ using System; +using System.Diagnostics; using System.Globalization; +using System.Reflection; using Android.App; using Android.Content; @@ -17,18 +19,53 @@ static Java.Lang.Throwable CreateJavaProxyThrowable (Exception e) var JavaProxyThrowable_type = typeof (Java.Lang.Object) .Assembly .GetType ("Android.Runtime.JavaProxyThrowable"); - return (Java.Lang.Throwable) Activator.CreateInstance (JavaProxyThrowable_type, e); + MethodInfo? create = JavaProxyThrowable_type.GetMethod ( + "Create", + BindingFlags.Static | BindingFlags.Public, + new Type[] { typeof (Exception) } + ); + + Assert.AreNotEqual (null, create, "Unable to find the Android.Runtime.JavaProxyThrowable.Create(Exception) method"); + return (Java.Lang.Throwable)create.Invoke (null, new object[] { e }); // Don't append Java stack trace } [Test] public void InnerExceptionIsSet () { - var ex = new InvalidOperationException ("boo!"); - using (var source = new Java.Lang.Throwable ("detailMessage", CreateJavaProxyThrowable (ex))) + Exception ex; + try { + throw new InvalidOperationException ("boo!"); + } catch (Exception e) { + ex = e; + } + + using (Java.Lang.Throwable proxy = CreateJavaProxyThrowable (ex)) + using (var source = new Java.Lang.Throwable ("detailMessage", proxy)) using (var alias = new Java.Lang.Throwable (source.Handle, JniHandleOwnership.DoNotTransfer)) { + CompareStackTraces (ex, proxy); Assert.AreEqual ("detailMessage", alias.Message); Assert.AreSame (ex, alias.InnerException); } } + + void CompareStackTraces (Exception ex, Java.Lang.Throwable throwable) + { + var managedTrace = new StackTrace (ex); + StackFrame[] managedFrames = managedTrace.GetFrames (); + Java.Lang.StackTraceElement[] javaFrames = throwable.GetStackTrace (); + + // Java + Assert.IsTrue (javaFrames.Length >= managedFrames.Length, + $"Java should have at least as many frames as .NET does; java({javaFrames.Length}) < managed({managedFrames.Length})"); + for (int i = 0; i < managedFrames.Length; i++) { + var mf = managedFrames[i]; + var jf = javaFrames[i]; + + Assert.AreEqual (mf.GetMethod ()?.Name, jf.MethodName, $"Frame {i}: method names differ"); + 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 (mf.GetFileLineNumber (), jf.LineNumber, $"Frame {i}: line numbers differ"); + } + } } }