-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Cross-VM Exception Unwinding #108211
Comments
@jkotas: I think that would! I was not aware of that new attribute. We'll need to prototype it and see if it behaves as desired. Thanks! |
@jkotas, @steveisok: I think using System.Diagnostics;
Console.WriteLine("Hello, World!");
A();
[DebuggerDisableUserUnhandledExceptions]
void A()
{
try
{
B();
}
catch (Exception e)
{
Debugger.BreakForUserUnhandledException(e);
Console.WriteLine(e.ToString());
}
}
void B()
{
throw new InvalidOperationException();
} @steveisok, @thaystg: the problem is that namespace Scratch.AndroidApp_DisableUserUnhandledException
{
[Activity(Label = "@string/app_name", MainLauncher = true)]
public class MainActivity : Activity
{
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
// Set our view from the "main" layout resource
SetContentView(Resource.Layout.activity_main);
A();
}
[System.Diagnostics.DebuggerDisableUserUnhandledExceptions]
static void A()
{
try
{
B();
}
catch (Exception e)
{
System.Diagnostics.Debugger.BreakForUserUnhandledException(e);
Console.WriteLine(e.ToString());
}
}
static void B()
{
throw new InvalidOperationException();
}
}
} the debugger does not break within What would be involved in making |
/cc @tommcdon |
What is the debugger engine used to debug Mono on Android? It needs to be fixed there. |
@jonpryor Did you test it using VS or VSCode? Can you please test it in VSCode with the new debugger engine enabled? Because it uses the same debugger engine used to debug a coreclr app, so it may probably work? |
@thaystg: I was testing using "Visual Studio 17.12 P3" on devbox, using whatever was current for "main" circa yesterday morning. Is Visual Studio not using the correct debugger engine? |
@jonpryor Yes, this is correct, VSCode uses the same debugger engine used by coreclr debug, and VS uses the older one (debugger-libs), it will be replaced soon. |
Thanks a lot, I was also able to reproduce it in a console app running on mono runtime using the new debugger engine. |
Background
Java.Interop is used to allow in-process interactions with a Java Virtual Machine (JVM). .NET for Android builds atop Java.Interop for integration with Android, using MonoVM as the JIT. (Neither CoreCLR nor NativeAot are supported by .NET for Android at this time.)
See also:
Setup
Assume we have a call stack wherein Managed code calls into Java code which calls back into Managed code:
Java side of
JavaInvoker
:The above demonstrates a Managed > Java > Managed transition:
ManagedJavaManaged()
, which invokesJavaInvoker.CreateRunnable()
, which eventually calls the Java methodInvoker.createRunnable()
, which in turn callsconsumer.accept()
, transitioning back intoMyIntConsumer.Accept()
.When no exceptions are involved, everything is fine!
But what if an exception is thrown? What if
MyIntConsumer.Accept(int)
throws an exception?Exception Handling
Java code cannot directly invoke managed code. Instead, Java code invokes special native methods. We call these "marshal methods", which are responsible for marshaling parameters, return values, and (ideally) exceptions.
Exception Handling without a Debugger
When no Debugger is present, the marshal methods catch and marshal all exceptions:
This way, if
MyIntConsumer.Accept()
throws an exception, it will be caught withinIIntConsumerInvoker.n_Accept_I()
, marshaled to Java, and then the method will return. Execution is assumed to return to Java, Java will see the pending exception, raise it, and Java code will be able to participate in exception handling.Exception Handling with a Debugger: catch everything
Things get "more interesting" when a debugger is attached, as we want our developer customers to see a "first chance exception" which points to the correct location.
If we run the following with a debugger attached:
then when line 3 is executed, we want the debugger to "break" on line 1, with a callstack that shows (more or less):
<>compiler-generated-method
forthrow new InvalidOperationException()
MyIntConsumer.Accept(int)
IIntConsumerInvoker.n_Accept_I(IntPtr, IntPtr, int)
JniEnvironment.InstanceMethods.CallVoidMethod(…)
IRunnableInvoker.Run()
The problem is that if we use the
IIntConsumerInvoker.n_Accept_I()
marshal method described above, in which all exceptions are caught, then instead the debugger will "break" atJniEnvironment.InstanceMethods.CallVoidMethod(…)
, if anywhere at all, completely skipping the originalthrow
site and it's Java callers.This was not considered to be an acceptable developer experience.
Exception Handling with a Debugger: catch only when a debugger is attached
If we instead have a marshal method where the
catch
block uses awhen
clause:then we have a better "unhandled exception" experience: the debugger breaks on the original
throw
site.Yay!
The problem is that if the developer then continues execution (F5), then the process state may be corrupted. This is because with the
catch (Exception e) when (!Debugger.IsAttached)
construct, the catch block becomes "invisible". This means that the exception is never caught, there is never an opportunity to return to Java code, and MonoVM will unwind all the call stacks, the Java call stacks included. This can result in the JVM aborting the process.Oops.
Exception Handling Performance
There is an added complication: the use of
JniTransition
laid out above is a lie. What actually happens is that the marshal methods contain no exception handling, and the marshal method usage is "wrapped" aroundJNINativeWrapper.CreateDelegate()
:JNINativeWrapper.CreateDelegate()
in turn uses System.Reflection.Emit to do a variant on the "catch (Exception) when (!Debugger.IsAttached)
" approach. This in turn means that System.Reflection.Emit is required, and can impact app startup times by pulling in System.Reflection.Emit initialization and usage into app startup.Request
What we would like is a construct to say "ignore this
catch
block when dealing with first chance exceptions, while executing thiscatch
block when actually unwinding the stack."An idea sketched out here is to add a
StackUnwinding
type:catch(StackUnwinding)
blocks would be "invisible" to first chance exception reporting.This would allow our marshal methods to use it:
We could then update our
generator
to make use of the new type, which would also allow us to stop usingJNINativeWrapper.CreateDelegate()
entirely. This would allow for faster app startup (no System.Reflection.Emit usage) without permitting Cross-VM corruption issues.The text was updated successfully, but these errors were encountered: