Skip to content

Commit 356485e

Browse files
authored
[generator] Remove JNINativeWrapper.CreateDelegate from bindings (#1275)
Fixes: #1258 Context: c8f3e51 Context: 176240d Context: dotnet/runtime#108211 Context: dotnet/android#9306 Context: dotnet/android#9309 Context: xamarin/monodroid@3e9de5a Context: dotnet/android@8bc7a3e The [Java Native Interface][0] allows native code to be associated with a [Java `native` method declaration][1], either by way of [`Java_`-prefixed native functions][2], or via function pointers provided to [`JNIEnv::RegisterNatives()`][3]. Both `Java_`-prefixed native functions and function pointers must refer to C-callable functions with appropriate [native method arguments][4]. A *Marshal Method* is a: 1. Method or delegate which is C-callable, 2. Accepting the appropriate Java Native Method arguments, 3. Is responsible for marshaling parameter and return types, and 3. *Delegates* the call to an appropriate managed method override. We have multiple different Marshal Method implementations running around, including: * XamarinAndroid1 and XAJavaInterop1 Marshal Methods, in which the Marshal Method is an `n_`-prefixed method in (roughly-ish) the same scope as the method that would be delegated to. * `jnimarshalmethod-gen`: see 176240d * LLVM Marshal Methods, which use LLVM-IR to emit `Java_`-prefixed native functions; see dotnet/android@8bc7a3e8. Which brings us to the current XAJavaInterop1 Marshal Methods implementation. Consider the [`java.util.function.IntConsumer`][5] interface: // Java public /* partial */ interface IntConsumer { void accept(int value); } With `generator --codegen-target=XAJavaInterop1` -- used by .NET for Android -- `IntConsumer` is bound as `IIntConsumer`: namespace Java.Util.Functions { // Metadata.xml XPath interface reference: path="/api/package[@name='java.util.function']/interface[@name='IntConsumer']" [Register ("java/util/function/IntConsumer", "", "Java.Util.Functions.IIntConsumerInvoker", ApiSince = 24)] public partial interface IIntConsumer : IJavaObject, IJavaPeerable { [Register ("accept", "(I)V", "GetAccept_IHandler:Java.Util.Functions.IIntConsumerInvoker, Mono.Android, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", ApiSince = 24)] void Accept (int value); } [Register ("java/util/function/IntConsumer", DoNotGenerateAcw=true, ApiSince = 24)] internal partial class IIntConsumerInvoker : global::Java.Lang.Object, IIntConsumer { static Delegate? cb_accept_Accept_I_V; 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; } 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); } } } The Marshal Method is `IIntConsumerInvoker.n_Accept_I()`. We also have a *Connector Method*. A Connector Method is a `static` method matching the signature of `Func<Delegate>`. The name of the connector method is mentioned in the 3rd `connector` parameter of `RegisterAttribute` on the interface method. During [Java Type Registration][6], all Connector methods for a type are looked up and invoked, and the `Delegate` instances returned from all those connector method invocations are provided to `JNIEnv::RegisterNatives()`. There are static and runtime issues with connector method and marshal method implementations until now: 1. Java Native Methods, and thus Marshal Methods, *must* conform to the C ABI. C does not support exceptions. C# *does*. What happens when `__this.Accept(value)` throws? 2. The answer to (1) is in the connector method, via the `JNINativeWrapper.CreateDelegate()` invocation. [`JNINativeWrapper.CreateDelegate()`][7] uses System.Reflection.Emit to *wrap* the Marshal Method with a try/catch block. At runtime, the intermixing of (1) and (2) will result in registering a method similar to the following with `JNIEnv::RegisterNatives()`: static void n_Accept_I (IntPtr jnienv, IntPtr native__this, int value) { JNIEnv.WaitForBridgeProcessing (); try { var __this = ava.Lang.Object.GetObject<IIntConsumer> (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; __this.Accept (value); } catch (Exception e) when (!Debugger.IsAttached) { AndroidEnvironment.UnhandledException (e); } } which presents a further two problems: 1. System.Reflection.Emit is used, which possibly slows down type registration and won't work with NativeAOT. 2. The `catch` block only executes when you're *not* debugging! Which means that if you're debugging the app, and an exception is thrown, you are now potentially unwinding the stack frame through a JNI boundary, which can *corrupt JVM state*, possibly resulting in an [app abort or crash][8]. ([***Why?!***][9]) This has been how things work since the beginning. .NET 9 introduces some features that allow us to rethink all this: * [`DebuggerDisableUserUnhandledExceptionsAttribute`][10] * [`Debugger.BreakForUserUnhandledException(Exception)`][11] > If a .NET Debugger is attached that supports the > [BreakForUserUnhandledException(Exception)][11] API, the debugger > won't break on user-unhandled exceptions when the exception is > caught by a method with this attribute, unless > [BreakForUserUnhandledException(Exception)][11] is called. Embrace .NET 9, remove the possible need for System.Reflection.Emit, and fully prevent possible JVM corruption by updating connector methods and marshal methods to instead be: namespace Java.Util.Functions { internal partial class IIntConsumerInvoker { static Delegate? cb_accept_Accept_I_V; static Delegate GetAccept_IHandler () { return cb_accept_Accept_I_V ??= new _JniMarshal_PPI_V (n_Accept_I); } [DebuggerDisableUserUnhandledExceptions] static void n_Accept_I (IntPtr jnienv, IntPtr native__this, int value) { if (!JniEnvironment.BeginMarshalMethod (jnienv, out var __envp, out var __r)) return; try { var __this = Java.Lang.Object.GetObject<IIntConsumer> (jnienv, native__this, JniHandleOwnership.DoNotTransfer)!; __this.Accept (value); } catch (global::System.Exception __e) { __r?.OnUserUnhandledException (ref __envp, __e); } finally { JniEnvironment.EndMarshalMethod (ref __envp); } } } } This removes the call to `JNINativeWrapper.CreateDelegate()` and it's implicit use of System.Reflection.Emit, properly wraps *everything* in a `try`/`catch` block so that exceptions are properly caught and marshaled back to Java if necessary, and integrates properly with expected "first chance exception" semantics. The *downside* is that this requires the "new debugger backend" to work, which at the time of this writing is only used by VSCode. As this code will only be used for .NET 10+ (2025-Nov), this is fine. [0]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html [1]: http://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#compiling_loading_and_linking_native_methods [2]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#resolving_native_method_names [3]: http://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#RegisterNatives [4]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#native_method_arguments [5]: https://developer.android.com/reference/java/util/function/IntConsumer [6]: https://github.com/dotnet/android/wiki/Blueprint#java-type-registration [7]: https://github.com/dotnet/android/blob/65906e0b7b2f471fcfbd07e7e01b68169c25d9da/src/Mono.Android/Android.Runtime/JNINativeWrapper.cs#L29-L105 [8]: dotnet/android#8608 (comment) [9]: dotnet/android#4877 [10]: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.debuggerdisableuserunhandledexceptionsattribute?view=net-9.0 [11]: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.debugger.breakforuserunhandledexception?view=net-9.0#system-diagnostics-debugger-breakforuserunhandledexception(system-exception)
1 parent 14f94a5 commit 356485e

File tree

70 files changed

+2231
-846
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+2231
-846
lines changed

Diff for: src/Java.Interop/Java.Interop/JniEnvironment.cs

+45
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Diagnostics;
6+
using System.Diagnostics.CodeAnalysis;
67
using System.Linq;
78
using System.Runtime.CompilerServices;
89
using System.Runtime.InteropServices;
@@ -50,6 +51,50 @@ public static bool WithinNewObjectScope {
5051
internal set {CurrentInfo.WithinNewObjectScope = value;}
5152
}
5253

54+
[global::System.Diagnostics.CodeAnalysis.SuppressMessage (
55+
"Design",
56+
"CA1031:Do not catch general exception types",
57+
Justification = "Exceptions cannot cross a JNI boundary.")]
58+
public static bool BeginMarshalMethod (IntPtr jnienv, out JniTransition transition, [NotNullWhen (true)] out JniRuntime? runtime)
59+
{
60+
runtime = null;
61+
Exception? ex = null;
62+
try {
63+
runtime = Info.Value?.Runtime;
64+
}
65+
catch (Exception e) {
66+
ex = e;
67+
}
68+
if (runtime == null || ex != null) {
69+
transition = default;
70+
runtime = null;
71+
Console.Error.WriteLine ("JNI Environment Information is not available on this thread.");
72+
if (ex != null) {
73+
Console.Error.WriteLine (ex);
74+
}
75+
return false;
76+
}
77+
78+
try {
79+
runtime.OnEnterMarshalMethod ();
80+
transition = new JniTransition (jnienv);
81+
}
82+
catch (Exception e) {
83+
runtime = null;
84+
transition = default;
85+
86+
Console.Error.WriteLine ($"OnEnterMarshalMethod failed: {e}");
87+
return false;
88+
}
89+
90+
return true;
91+
}
92+
93+
public static void EndMarshalMethod (ref JniTransition transition)
94+
{
95+
transition.Dispose ();
96+
}
97+
5398
internal static void SetEnvironmentPointer (IntPtr environmentPointer)
5499
{
55100
CurrentInfo.EnvironmentPointer = environmentPointer;

Diff for: src/Java.Interop/Java.Interop/JniRuntime.cs

+13
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,19 @@ public virtual bool ExceptionShouldTransitionToJni (Exception e)
437437

438438
partial class JniRuntime {
439439

440+
public virtual void OnEnterMarshalMethod ()
441+
{
442+
ValueManager.WaitForGCBridgeProcessing ();
443+
}
444+
445+
public virtual void OnUserUnhandledException (ref JniTransition transition, Exception e)
446+
{
447+
transition.SetPendingException (e);
448+
449+
// TODO: Enable when we move to 'net9.0'
450+
//Debugger.BreakForUserUnhandledException (e);
451+
}
452+
440453
public virtual void RaisePendingException (Exception pendingException)
441454
{
442455
JniEnvironment.Exceptions.Throw (pendingException);

Diff for: src/Java.Interop/PublicAPI.Unshipped.txt

+4
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
#nullable enable
2+
static Java.Interop.JniEnvironment.BeginMarshalMethod(nint jnienv, out Java.Interop.JniTransition transition, out Java.Interop.JniRuntime? runtime) -> bool
3+
static Java.Interop.JniEnvironment.EndMarshalMethod(ref Java.Interop.JniTransition transition) -> void
4+
virtual Java.Interop.JniRuntime.OnEnterMarshalMethod() -> void
5+
virtual Java.Interop.JniRuntime.OnUserUnhandledException(ref Java.Interop.JniTransition transition, System.Exception! e) -> void
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#if !NET9_0_OR_GREATER
2+
3+
using System;
4+
5+
namespace System.Diagnostics
6+
{
7+
// This attribute was added in .NET 9, and we may not be targeting .NET 9 yet.
8+
public class DebuggerDisableUserUnhandledExceptionsAttribute : Attribute
9+
{
10+
}
11+
}
12+
13+
#endif // !NET9_0_OR_GREATER

Diff for: tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/Common/WriteDuplicateInterfaceEventArgs.txt

+28-10
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,24 @@ internal partial class AnimatorListenerInvoker : global::Java.Lang.Object, Anima
7070
#pragma warning disable 0169
7171
static Delegate GetOnAnimationEnd_IHandler ()
7272
{
73-
if (cb_OnAnimationEnd_OnAnimationEnd_I_Z == null)
74-
cb_OnAnimationEnd_OnAnimationEnd_I_Z = JNINativeWrapper.CreateDelegate (new _JniMarshal_PPI_Z (n_OnAnimationEnd_I));
75-
return cb_OnAnimationEnd_OnAnimationEnd_I_Z;
73+
return cb_OnAnimationEnd_OnAnimationEnd_I_Z ??= new _JniMarshal_PPI_Z (n_OnAnimationEnd_I);
7674
}
7775

76+
[global::System.Diagnostics.DebuggerDisableUserUnhandledExceptions]
7877
static bool n_OnAnimationEnd_I (IntPtr jnienv, IntPtr native__this, int param1)
7978
{
80-
var __this = global::Java.Lang.Object.GetObject<java.code.AnimatorListener> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
81-
return __this.OnAnimationEnd (param1);
79+
if (!global::Java.Interop.JniEnvironment.BeginMarshalMethod (jnienv, out var __envp, out var __r))
80+
return default;
81+
82+
try {
83+
var __this = global::Java.Lang.Object.GetObject<java.code.AnimatorListener> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
84+
return __this.OnAnimationEnd (param1);
85+
} catch (global::System.Exception __e) {
86+
__r.OnUserUnhandledException (ref __envp, __e);
87+
return default;
88+
} finally {
89+
global::Java.Interop.JniEnvironment.EndMarshalMethod (ref __envp);
90+
}
8291
}
8392
#pragma warning restore 0169
8493

@@ -96,15 +105,24 @@ internal partial class AnimatorListenerInvoker : global::Java.Lang.Object, Anima
96105
#pragma warning disable 0169
97106
static Delegate GetOnAnimationEnd_IIHandler ()
98107
{
99-
if (cb_OnAnimationEnd_OnAnimationEnd_II_Z == null)
100-
cb_OnAnimationEnd_OnAnimationEnd_II_Z = JNINativeWrapper.CreateDelegate (new _JniMarshal_PPII_Z (n_OnAnimationEnd_II));
101-
return cb_OnAnimationEnd_OnAnimationEnd_II_Z;
108+
return cb_OnAnimationEnd_OnAnimationEnd_II_Z ??= new _JniMarshal_PPII_Z (n_OnAnimationEnd_II);
102109
}
103110

111+
[global::System.Diagnostics.DebuggerDisableUserUnhandledExceptions]
104112
static bool n_OnAnimationEnd_II (IntPtr jnienv, IntPtr native__this, int param1, int param2)
105113
{
106-
var __this = global::Java.Lang.Object.GetObject<java.code.AnimatorListener> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
107-
return __this.OnAnimationEnd (param1, param2);
114+
if (!global::Java.Interop.JniEnvironment.BeginMarshalMethod (jnienv, out var __envp, out var __r))
115+
return default;
116+
117+
try {
118+
var __this = global::Java.Lang.Object.GetObject<java.code.AnimatorListener> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
119+
return __this.OnAnimationEnd (param1, param2);
120+
} catch (global::System.Exception __e) {
121+
__r.OnUserUnhandledException (ref __envp, __e);
122+
return default;
123+
} finally {
124+
global::Java.Interop.JniEnvironment.EndMarshalMethod (ref __envp);
125+
}
108126
}
109127
#pragma warning restore 0169
110128

Diff for: tests/generator-Tests/Unit-Tests/CodeGeneratorExpectedResults/Common/WriteInterfaceRedeclaredDefaultMethod.txt

+13-5
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,23 @@ internal partial class IMyInterface2Invoker : global::Java.Lang.Object, IMyInter
6666
#pragma warning disable 0169
6767
static Delegate GetDoSomethingHandler ()
6868
{
69-
if (cb_DoSomething_DoSomething_V == null)
70-
cb_DoSomething_DoSomething_V = JNINativeWrapper.CreateDelegate (new _JniMarshal_PP_V (n_DoSomething));
71-
return cb_DoSomething_DoSomething_V;
69+
return cb_DoSomething_DoSomething_V ??= new _JniMarshal_PP_V (n_DoSomething);
7270
}
7371

72+
[global::System.Diagnostics.DebuggerDisableUserUnhandledExceptions]
7473
static void n_DoSomething (IntPtr jnienv, IntPtr native__this)
7574
{
76-
var __this = global::Java.Lang.Object.GetObject<java.code.IMyInterface2> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
77-
__this.DoSomething ();
75+
if (!global::Java.Interop.JniEnvironment.BeginMarshalMethod (jnienv, out var __envp, out var __r))
76+
return;
77+
78+
try {
79+
var __this = global::Java.Lang.Object.GetObject<java.code.IMyInterface2> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
80+
__this.DoSomething ();
81+
} catch (global::System.Exception __e) {
82+
__r.OnUserUnhandledException (ref __envp, __e);
83+
} finally {
84+
global::Java.Interop.JniEnvironment.EndMarshalMethod (ref __envp);
85+
}
7886
}
7987
#pragma warning restore 0169
8088

0 commit comments

Comments
 (0)