Register JNI natives via blittable JniNativeMethod#1455
Open
simonrozsival wants to merge 2 commits into
Open
Conversation
The default native-method registration funnel invoked the JNI RegisterNatives function pointer typed as `delegate* unmanaged<nint, nint, JniNativeMethodRegistration[], int, int>`, 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<JniNativeMethod>) overload, eliminating the non-blittable `delegate* unmanaged<>` call site. This matches the trimmable type-map path, which was already immune. Fixes dotnet/android#11633 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Member
Author
|
/azp run |
|
No pipelines are associated with this pull request. |
There was a problem hiding this comment.
Pull request overview
This PR updates Java.Interop’s JNI native method registration to avoid passing a non-blittable JniNativeMethodRegistration[] across a delegate* unmanaged<> boundary, instead marshalling into a blittable JniNativeMethod array and calling the existing RegisterNatives (JniObjectReference, ReadOnlySpan<JniNativeMethod>) overload. This targets a crossgen2 composite R2R + PGO miscompilation that can corrupt method names during registration.
Changes:
- Rework
JniEnvironment.Types.RegisterNatives (JniObjectReference, JniNativeMethodRegistration[], int)to marshalName/Signatureto unmanaged UTF-8 and call the blittable span-based overload. - Add an
IL3050-suppressed helper forMarshal.GetFunctionPointerForDelegateused by the registration path. - Add a fast-path return for
numMethods == 0/methods == null.
Show a summary per file
| File | Description |
|---|---|
| src/Java.Interop/Java.Interop/JniEnvironment.Types.cs | Routes native registration through blittable JniNativeMethod + ReadOnlySpan to avoid non-blittable array marshalling across unmanaged function pointers. |
Copilot's findings
- Files reviewed: 1/1 changed files
- Comments generated: 1
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<JniNativeMethod>)` 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Register JNI native methods by marshalling into the blittable
JniNativeMethodstruct and calling the existingRegisterNatives(JniObjectReference, ReadOnlySpan<JniNativeMethod>)overload, instead of invoking the JNIRegisterNativesfunction pointer with a non-blittable managed array (JniNativeMethodRegistration[]).This avoids relying on the runtime to marshal an array of a non-blittable struct across a
delegate* unmanaged<>call — a path that is currently miscompiled by crossgen2 under composite ReadyToRun + PGO and corrupts the registered method names.Refs dotnet/android#11633
Background: what breaks
The default (non-trimmable / llvm-ir typemap) registration funnels through:
JniNativeMethodRegistrationis non-blittable (string Name; string Signature; Delegate Marshaler). PassingJniNativeMethodRegistration[]through thedelegate* unmanaged<>requires the runtime to marshal the array element-by-element.ManagedPeer..cctor,AndroidTypeManager,ManagedTypeManager, and everyJniType.RegisterNativeMethodscaller funnel through this single method (_RegisterNativeshas exactly one caller), so fixing it here fixes all of them.Root cause (a crossgen2 / runtime regression)
dotnet/runtime #126911 ("Move built-in array marshalling to managed", 2026-05-01) moved array-of-struct marshalling from native C++ into the managed generic
System.StubHelpers.StructureMarshaler<T> : IArrayElementMarshaler<T, StructureMarshaler<T>>. Its element converter is an intrinsic with a blittable-only fallback body:For non-blittable
T, the VM generates a real per-field marshalling body at JIT time (StructMarshalStubs::TryGenerateStructMarshallingMethod,dllimport.cpp). crossgen2 has no equivalent, so when PGO/MIBC marks the marshaller hot and crossgen2 precompiles the shared canonical (__Canon) instantiation, it emits the literalMemmovefallback — a raw blit of the managed object references into the native struct.Disassembly of the precompiled canonical converter from a composite-R2R + MIBC image:
So JNI receives a managed
stringobject pointer where it expects a UTF-8char*, the method name is garbage, and registration fails withNoSuchMethodErrorduring startup (e.g.MauiApplication,net.dot.jni.ManagedPeer).This only manifests when crossgen2 precompiles the marshaller (MIBC marks it hot). Without a profile the JIT compiles it and registration is correct — which is why the same app works without a startup MIBC.
Why this is the right fix
RegisterNatives(ReadOnlySpan<JniNativeMethod>)path marshals names/signatures to UTF-8 ourselves (Marshal.StringToCoTaskMemUTF8) and passes a blittableJniNativeMethod*— there is noStructureMarshaler<T>involved, so it is correct regardless of the crossgen2 bug.ManagedPeer,AndroidTypeManager, andManagedTypeManagertogether.We do not want to marshal arrays of non-blittable types across
delegate* unmanaged<>/ P/Invoke boundaries given this runtime limitation. A scan of the shipped runtime path (Java.Interop + Mono.Android) showsRegisterNativeswas the only such site: it is the only invoker function pointer with an array parameter, the only non-blittable-array native call; allJValue[]call sites already pin (fixed (JValue* …)) and pass a blittable pointer.Changes
JniEnvironment.Types.RegisterNatives(JniObjectReference, JniNativeMethodRegistration[], int)to marshalName/Signatureto unmanaged UTF-8 and a function pointer, then dispatch to the blittableRegisterNatives(JniObjectReference, ReadOnlySpan<JniNativeMethod>)overload (no more_RegisterNatives/ non-blittabledelegate* unmanaged<>call).JNIEnv::RegisterNatives()— e.g.NoSuchMethodError— and guardstype.IsValid, matching the generated_RegisterNativeswrapper it replaces. This benefits both the array-based path and the trimmable type-map path (which previously could leave a pending exception in the JNIEnv).Implementation notes
finally(Marshal.ZeroFreeCoTaskMemUTF8);GC.KeepAlive (methods)keeps the marshaler delegates alive across the native call.Marshal.GetFunctionPointerForDelegate(Delegate)is wrapped in a helper with anIL3050suppression: thisJniNativeMethodRegistration[]path runs only on JIT-capable runtimes (MonoVM/CoreCLR); NativeAOT registers through the trimmable type map with statically-compiled function pointers and never reaches it.Verification
Reproduced and fixed in an isolated
net11.0console app (no Java.Interop/Android types): a non-blittablestruct[]passed through adelegate* unmanaged<>to a real native function corrupts only under composite R2R + MIBC (when the call site is hot); the blittable equivalent is correct under all configurations (JIT, plain R2R, composite R2R, composite R2R + MIBC).Tracking
__Canon, or defer it to the JIT). This PR is the Java.Interop-side fix and is correct independent of the runtime change.