From 82c3bad6b11018cfed1aedbd19e224591d381e5e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 12 Jun 2026 13:50:34 +0200 Subject: [PATCH 1/2] [Java.Interop] Register JNI natives via blittable JniNativeMethod The default native-method registration funnel invoked the JNI RegisterNatives function pointer typed as `delegate* unmanaged`, passing a non-blittable managed array and relying on a runtime-synthesized marshalling stub to convert it to JNINativeMethod*. crossgen2 miscompiles that stub under composite ReadyToRun + PGO (MIBC): it degrades to a raw struct blit, so the native `name`/`signature` pointers end up referencing the managed string objects instead of marshalled UTF-8 data. The registered method names are corrupted, producing NoSuchMethodError at startup (e.g. MauiApplication, net.dot.jni.ManagedPeer). Marshal JniNativeMethodRegistration[] into blittable JniNativeMethod values and dispatch to the existing RegisterNatives(JniObjectReference, ReadOnlySpan) overload, eliminating the non-blittable `delegate* unmanaged<>` call site. This matches the trimmable type-map path, which was already immune. Fixes https://github.com/dotnet/android/issues/11633 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/JniEnvironment.Types.cs | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs index d8b9f652d..8652e18cf 100644 --- a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs +++ b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Collections.Generic; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; @@ -256,7 +257,7 @@ public static void RegisterNatives (JniObjectReference type, JniNativeMethodRegi RegisterNatives (type, methods, methods == null ? 0 : methods.Length); } - public static void RegisterNatives (JniObjectReference type, JniNativeMethodRegistration [] methods, int numMethods) + public static unsafe void RegisterNatives (JniObjectReference type, JniNativeMethodRegistration [] methods, int numMethods) { if ((numMethods < 0) || (numMethods > (methods?.Length ?? 0))) { @@ -275,14 +276,45 @@ public static void RegisterNatives (JniObjectReference type, JniNativeMethodRegi } #endif // DEBUG - int r = _RegisterNatives (type, methods ?? Array.Empty(), numMethods); + if (numMethods == 0 || methods == null) { + return; + } - if (r != 0) { - throw new InvalidOperationException ( - string.Format ("Could not register native methods for class '{0}'; JNIEnv::RegisterNatives() returned {1}.", GetJniTypeNameFromClass (type), r)); + // Marshal the non-blittable JniNativeMethodRegistration[] into blittable JniNativeMethod + // values and dispatch to the blittable overload, instead of invoking the JNI + // `RegisterNatives` function pointer with a non-blittable managed-array parameter. + // The runtime marshalling stub synthesized for such a `delegate* unmanaged<>` call is + // miscompiled by crossgen2 under composite ReadyToRun + PGO: the JniNativeMethod `name` + // pointers end up referencing the managed `string` objects instead of marshalled UTF-8 + // data, which corrupts the registered method names. See https://github.com/dotnet/android/issues/11633. + var natives = new JniNativeMethod [numMethods]; + var unmanagedStrings = new IntPtr [numMethods * 2]; + try { + for (int i = 0; i < numMethods; ++i) { + var m = methods [i]; + IntPtr name = Marshal.StringToCoTaskMemUTF8 (m.Name); + IntPtr sig = Marshal.StringToCoTaskMemUTF8 (m.Signature); + unmanagedStrings [i * 2] = name; + unmanagedStrings [i * 2 + 1] = sig; + natives [i] = new JniNativeMethod ((byte*) name, (byte*) sig, GetFunctionPointerForDelegate (m.Marshaler)); + } + RegisterNatives (type, new ReadOnlySpan (natives, 0, numMethods)); + // Keep the Marshaler delegates alive at least until JNI has consumed the function pointers. + GC.KeepAlive (methods); + } finally { + for (int i = 0; i < unmanagedStrings.Length; ++i) { + Marshal.ZeroFreeCoTaskMemUTF8 (unmanagedStrings [i]); + } } } + // Native method registration via JniNativeMethodRegistration[] only runs on JIT-capable + // runtimes (MonoVM/CoreCLR). Under NativeAOT, native methods are registered through the + // trimmable type map using statically-compiled function pointers, so this path is never reached. + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Not reached under NativeAOT; only JIT-capable runtimes register via JniNativeMethodRegistration[].")] + static IntPtr GetFunctionPointerForDelegate (Delegate marshaler) => + Marshal.GetFunctionPointerForDelegate (marshaler); + /// /// Registers JNI native methods using blittable structs /// with raw function pointers and UTF-8 name/signature pointers. From b5d0af5c603fcacf8d239d1cea6f09726ae60eb0 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 12 Jun 2026 18:28:24 +0200 Subject: [PATCH 2/2] Preserve JNI exception propagation in blittable RegisterNatives Address PR review: the generated `_RegisterNatives` wrapper checked `ExceptionOccurred()` after the native call and rethrew/cleared any pending Java exception (e.g. NoSuchMethodError). Routing registration through the blittable `RegisterNatives(JniObjectReference, ReadOnlySpan)` overload dropped that check, which could leave a pending exception in the JNIEnv and cause subsequent JNI calls to fail or hide the real error. Add the pending-exception check (and a `type.IsValid` guard) to the blittable overload so both the array-based registration path and the trimmable type-map path surface JNI registration failures correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/JniEnvironment.Types.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs index 8652e18cf..bcd76a417 100644 --- a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs +++ b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs @@ -322,7 +322,11 @@ static IntPtr GetFunctionPointerForDelegate (Delegate marshaler) => /// public static unsafe void RegisterNatives (JniObjectReference type, ReadOnlySpan methods) { + if (!type.IsValid) + throw new ArgumentException ("Handle must be valid.", nameof (type)); + IntPtr env = JniEnvironment.EnvironmentPointer; + int r; fixed (JniNativeMethod* methodsPtr = methods) { #if FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS var registerNatives = (delegate* unmanaged) @@ -331,10 +335,19 @@ public static unsafe void RegisterNatives (JniObjectReference type, ReadOnlySpan var registerNatives = (delegate* unmanaged) JniEnvironment.CurrentInfo.Invoker.env.RegisterNatives; #endif - int r = registerNatives (env, type.Handle, methodsPtr, methods.Length); - if (r != 0) { - throw new InvalidOperationException ($"Could not register native methods for class '{GetJniTypeNameFromClass (type)}'; JNIEnv::RegisterNatives() returned {r}."); - } + r = registerNatives (env, type.Handle, methodsPtr, methods.Length); + } + + // Surface (and clear) any pending Java exception raised by JNI::RegisterNatives() + // — e.g. NoSuchMethodError — before falling back to the return-code check, matching + // the behavior of the generated `_RegisterNatives` wrapper. Leaving a pending + // exception in the JNIEnv would make subsequent JNI calls fail or abort. + var thrown = JniEnvironment.GetExceptionForLastThrowable (); + if (thrown != null) + ExceptionDispatchInfo.Capture (thrown).Throw (); + + if (r != 0) { + throw new InvalidOperationException ($"Could not register native methods for class '{GetJniTypeNameFromClass (type)}'; JNIEnv::RegisterNatives() returned {r}."); } }