Description
Context: dotnet/runtime#108211
Context: dotnet/android#9306
Context: dotnet/android#9309
Context: https://github.com/xamarin/monodroid/commit/3e9de5a51bd46263b08365ef18bed1ae472122d8
Consider this marshal method and related infrastructure::
namespace Java.Util.Functions {
public partial interface IIntConsumerInvoker : global::Java.Lang.Object, IIntConsumer {
static Delegate? cb_accept_Accept_I_V;
#pragma warning disable 0169
[global::System.Runtime.Versioning.SupportedOSPlatformAttribute ("android24.0")]
static Delegate GetAccept_IHandler ()
{
if (cb_accept_Accept_I_V == null)
cb_accept_Accept_I_V = JNINativeWrapper.CreateDelegate (new _JniMarshal_PPI_V (n_Accept_I));
return cb_accept_Accept_I_V;
}
[global::System.Runtime.Versioning.SupportedOSPlatformAttribute ("android24.0")]
static void n_Accept_I (IntPtr jnienv, IntPtr native__this, int value)
{
var __this = global::Java.Lang.Object.GetObject<Java.Util.Functions.IIntConsumer> (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!;
__this.Accept (value);
}
#pragma warning restore 0169
}
}
Why do we have JNINativeWrapper.CreateDelegate()
? (Closely related: why isn't there a try
/catch
block in n_Accept_I()
? Though part of that is "we didn't think of it.")
The answer is around the "unhandled exception" experience when a debugger is attached: the debugger breaks at the "active" throw
site. If the type will be caught by a catch
block, then you won't get an "unhandled exception" notification, which made customers sad.
Which means that for a good debugger experience, we can't have catch
blocks catching exceptions; if we did, then the exceptions would be handled!
Meanwhile, we must catch and marshal exceptions back to Java, otherwise we'll corrupt the JVM during stack unwind!
Where we wound up was a terrible middle:
- If a debugger is attached, then basically have no exception handling. (Wild oversimplification, but close enough for here.)
- If a debugger isn't attached, then all exceptions are caught.
JNINativeWrapper.CreateDelegate()
usedSystem.Reflection.Emit
to bridge these two worlds.
However, .NET 9 introduces System.Diagnostics.DebuggerDisableUserUnhandledExceptionsAttribute
:
If a .NET Debugger is attached that supports the BreakForUserUnhandledException(Exception) API, the debugger won't break on user-unhandled exceptions when the exception is caught by a method with this attribute, unless BreakForUserUnhandledException(Exception) is called.
Thus, the proposal: update the above marshal method related infrastructure to instead be:
namespace Java.Util.Functions {
public partial interface IIntConsumerInvoker : global::Java.Lang.Object, IIntConsumer {
static Delegate? cb_accept_Accept_I_V;
#pragma warning disable 0169
[global::System.Runtime.Versioning.SupportedOSPlatformAttribute ("android24.0")]
static Delegate GetAccept_IHandler ()
{
return cb_accept_Accept_I_V ?? (cb_accept_Accept_I_V = new _JniMarshal_PPI_V (n_Accept_I));
}
[global::System.Runtime.Versioning.SupportedOSPlatformAttribute ("android24.0")]
[global::System.Diagnostics.DebuggerDisableUserUnhandledExceptionsAttribute)]
static void n_Accept_I (IntPtr jnienv, IntPtr native__this, int value)
{
var __envp = new global::Java.Interop.JniTransition (jnienv);
try {
var __this = global::Java.Lang.Object.GetObject<Java.Util.Functions.IIntConsumer> (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!;
__this.Accept (value);
}
catch (Exception e) {
__envp.SetPendingException (e);
Debugger.BreakForUserUnhandledException (e);
}
finally {
__envp.Dispose ();
}
}
#pragma warning restore 0169
}
}
This entirely removes JNINativeWrapper.CreateDelegate()
and in turn System.Reflection.Emit
from the marshal method codepath, which should improve app startup.