Skip to content

Improve JavaProxyThrowable by translating managed stack trace #8185

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ee565e4
Improve JavaProxyThrowable by translating managed stack trace
grendello Jul 12, 2023
7c0a507
Update apkdiffs
grendello Jul 12, 2023
6ee31a3
Create JavaProxyThrowable with `Create` and reflection
grendello Jul 12, 2023
7cfc3dc
Oops
grendello Jul 12, 2023
78839fb
Seal the class and add test
grendello Jul 13, 2023
b2b38b1
Merge branch 'main' into throwable-stacktrace
grendello Jul 14, 2023
ada140c
Optionally append java stack trace
grendello Jul 14, 2023
45b3e72
oops
grendello Jul 14, 2023
241ed9b
Merge branch 'main' into throwable-stacktrace
grendello Jul 17, 2023
c1cabbc
oops #2
grendello Jul 17, 2023
4c38c11
Let's see if this works
grendello Jul 17, 2023
7a8964b
Java and managed traces differ, let's see what's the difference
grendello Jul 17, 2023
1bac358
Merge branch 'main' into throwable-stacktrace
grendello Jul 18, 2023
998c012
That should do it
grendello Jul 18, 2023
a25636d
Merge branch 'main' into throwable-stacktrace
grendello Jul 18, 2023
92e5db3
Merge branch 'main' into throwable-stacktrace
grendello Jul 24, 2023
4b5b56e
Update JavaProxyThrowable.cs
grendello Jul 26, 2023
b68dcc2
Merge branch 'main' into throwable-stacktrace
grendello Jul 27, 2023
32130ab
Get the Java stack trace from `self` directly
grendello Jul 27, 2023
603f1e4
Merge branch 'throwable-stacktrace' of github.com:grendello/xamarin-a…
grendello Jul 27, 2023
cdf4714
Merge branch 'main' into throwable-stacktrace
grendello Jul 31, 2023
784014c
Address comments
grendello Jul 31, 2023
a78d466
Merge remote-tracking branch 'origin/main' into throwable-stacktrace
jonpryor Aug 18, 2023
691e075
Update ExceptionTest.cs
jonpryor Aug 22, 2023
af90f92
Fix unit test.
jonpryor Aug 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Mono.Android/Android.Runtime/AndroidRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
74 changes: 67 additions & 7 deletions src/Mono.Android/Android.Runtime/JavaProxyThrowable.cs
Original file line number Diff line number Diff line change
@@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why have Exception? innerException if you're gonna throw ArgumentNullException here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was a typo

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);
Copy link
Contributor

Choose a reason for hiding this comment

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

This is where [System.Func`1[System.Type]] is coming from!

Which still doesn't make sense, as that would mean that innerException.GetType() is Func<Type>, but innerException is an Exception, so… how is that possible? I remain flummoxed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh! It's because () is missing! This should be innerException.GetType() (with parens), not innerException.GetType! I'm kinda surprised that innerException.GetType even compiled; apparently the compiler was just implicitly converting it to a Func<Type>, which kinda makes sense?


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 ()
Copy link
Contributor

Choose a reason for hiding this comment

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

The problem with this method is that it doesn't "merge", it "replaces". #1188 should have provided examples for desired behavior.

Consider:

var e = new Java.Lang.Error ("here!");
e.PrintStackTrace(Java.Lang.JavaSystem.Out);

This produces the output:

java.lang.Error: here!
   at crc641c2f9234c37aaa3c.MainActivity.n_onCreate(Native Method)
   at crc641c2f9234c37aaa3c.MainActivity.onCreate(MainActivity.java:30)
   at android.app.Activity.performCreate(Activity.java:8341)
   at android.app.Activity.performCreate(Activity.java:8320)
   at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1417)
   at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3622)
   at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3778)
   at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
   at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:138)
   at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2303)
   at android.os.Handler.dispatchMessage(Handler.java:106)
   at android.os.Looper.loopOnce(Looper.java:201)
   at android.os.Looper.loop(Looper.java:288)
   at android.app.ActivityThread.main(ActivityThread.java:7884)
   at java.lang.reflect.Method.invoke(Native Method)
   at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

If the assumption in #1188 is correct, and we want printStackTrace() to be "more useful", replacing all of the above is not "more useful". What we'd want is to instead produce:

java.lang.Error: here!
   at android_cwd.MainActivity.OnCreate(MainActivity.cs:11)
   at crc641c2f9234c37aaa3c.MainActivity.n_onCreate(Native Method)
   at crc641c2f9234c37aaa3c.MainActivity.onCreate(MainActivity.java:30)
…

As far as I can tell, TranslateStackTrace() does not do any such "merging". It would thus instead produce:

   at android_cwd.MainActivity.OnCreate(MainActivity.cs:11)

as the complete stack trace, which I don't believe would be useful.

(Now, this PR/#1188 is about JavaProxyThrowable, not for new Error().PrintStackTrace(), but PrintStackTrace() is the main point.)

{
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 () ?? 0
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably be ?? -1, as negative numbers appear to be the "don't have one" behavior. See e.g.

W *jonp*  : # jonp: stack[3]=crc64ef325ad1a3b68b57.MainActivity.n_onCreate(MainActivity.java:-2)

No idea where -2 came from; I assume it's because the file has no line number info? (Which also makes no sense, as the declaration for n_onCreate() does have a line number…)

);

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);
}
}
}
2 changes: 1 addition & 1 deletion src/Mono.Android/Java.Lang/Throwable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
42 changes: 39 additions & 3 deletions tests/Mono.Android-Tests/System/ExceptionTest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;

using Android.App;
using Android.Content;
Expand All @@ -17,18 +19,52 @@ 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), typeof(bool) }
Copy link
Contributor

Choose a reason for hiding this comment

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

Regarding the "Unable to find ….Create(Exception) method" error, I think it's because it's looking for Create(Exception, bool), which does not exist. We need to instead look for Create(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, false }); // 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.AreEqual (managedFrames.Length, javaFrames.Length, "Java and managed stack traces have a different number of frames");
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");
}
}
}
}