[TrimmableTypeMap] Support legacy RegisterNativeMembers#11652
Draft
simonrozsival wants to merge 5 commits into
Draft
[TrimmableTypeMap] Support legacy RegisterNativeMembers#11652simonrozsival wants to merge 5 commits into
simonrozsival wants to merge 5 commits into
Conversation
Add an opt-in feature switch,
`Microsoft.Android.Runtime.RuntimeFeature.LegacyJniRegistration`
(MSBuild `_AndroidEnableLegacyJniRegistration`, default `false`), that
re-enables the legacy, reflection-based JNI native-method registration in
the trimmable type map path.
By default the trimmable type map registers native methods via the fast
path (JCW static initializers -> `mono.android.Runtime.registerNatives`),
and `TrimmableTypeMapTypeManager.RegisterNativeMembers` throws. That makes
two legacy mechanisms unusable with the trimmable type map:
* legacy precompiled Java Callable Wrappers (from binding jars/aars) whose
static initializers call `mono.android.Runtime.register("Type, Asm",
Class, "methods")`, and
* `Java.Interop.ManagedPeer.registerNativeMembers`.
When the switch is enabled:
* `JNIEnvInit.Initialize` wires `registerJniNativesFn` even in the
trimmable path, so the native `register(...)` call flows back into
managed code; and
* `TrimmableTypeMapTypeManager.RegisterNativeMembers` performs the
reflection-based registration instead of throwing.
The reflection parsing logic is extracted from `ManagedTypeManager` into a
shared `NativeMethodRegistrar` helper and reused by both managers (removing
a duplicate copy). With the switch off (the default), the helper and the
`register(...)` callback are unreferenced and trimmed away.
No native changes are required: `Java_mono_android_Runtime_register` is a
name-exported JNICALL that is always present and only acts when the managed
`registerJniNativesFn` pointer is set.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mable)
Refine the trimmable legacy-registration prototype to reuse the generated
fast path instead of the slow, reflection-based registration.
The legacy `RegisterNativeMembers(JniType, Type, ReadOnlySpan<char> methods)`
already carries the managed `Type`, which keys directly into the generated
`IAndroidCallableWrapper` proxy. So when `RuntimeFeature.LegacyJniRegistration`
is enabled, `TrimmableTypeMapTypeManager.RegisterNativeMembers` now calls
`TrimmableTypeMap.TryRegisterNativeMembers`, which resolves the proxy via
`GetProxyForManagedType(type)` and invokes `acw.RegisterNatives(nativeClass)`
-- the same trim-safe primitive used by `OnRegisterNatives`
(`mono.android.Runtime.registerNatives`).
`type` selects the proxy; the `methods` metadata string is redundant and is
used only for a `[Conditional("DEBUG")]` validation (JNI-name match and
non-empty methods). This keeps the legacy entry points (legacy precompiled
JCWs calling `mono.android.Runtime.register(...)`, and `Java.Interop.ManagedPeer`)
reflection-free, so there is nothing extra to trim.
Because the trimmable path no longer needs shared reflection parsing, the
previous `NativeMethodRegistrar` extraction is reverted and `ManagedTypeManager`
is restored to its original form.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract the string-based JNI native method registration logic from AndroidTypeManager.RegisterNativeMembers into a shared NativeMethodRegistration helper. The helper owns parsing the method metadata string, resolving callback delegates, handling [Export] dynamic callbacks, rooting those callbacks, and registering natives with JNI. Use the helper from AndroidTypeManager after the linker-generated fast registration map, preserving the existing fallback to Java.Interop's marshal-method registration when applicable. Also use the same helper from TrimmableTypeMapTypeManager when the new string-based registration feature switch is enabled. Rename the feature to RuntimeFeature.StringBasedJniRegistration and guard it with [FeatureGuard(typeof(RequiresUnreferencedCodeAttribute))], because this path relies on reflection over method metadata strings. The switch is true by default for non-trimmable typemaps and false by default for the trimmable typemap, where native registration normally happens through mono.android.Runtime.registerNatives(Class). Move the trimmable registerNatives JNI callback wiring out of TrimmableTypeMap and into JNIEnvInit, so responsibilities are clearer: JNIEnvInit wires JNI callbacks, TrimmableTypeMap resolves generated wrappers, and TrimmableTypeMapTypeManager handles JniTypeManager behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Member
Author
|
Updated this draft per the review feedback:
Local validation:
Known unrelated local failure observed during validation:
|
Mark NativeMethodRegistration with both RequiresUnreferencedCode and RequiresDynamicCode. This makes the intent explicit: the string-based JNI registration path parses method metadata strings and resolves callbacks via reflection/dynamic delegate creation, so it is suitable for MonoVM and CoreCLR but not NativeAOT. Remove the previous UnconditionalSuppressMessage attributes from the helper so callers must flow through the feature switch instead of hiding trim/dynamic-code usage locally. Add a RequiresDynamicCode FeatureGuard to RuntimeFeature.StringBasedJniRegistration alongside the existing RequiresUnreferencedCode guard. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make AndroidTypeManager.RegisterNativeMembers raise a clear exception when string-based JNI registration is disabled and the linker-generated fast registration map did not handle the type. Previously this path silently returned, leaving native methods unregistered and deferring the failure to a later JNI call. The exception is caught by RegisterNativeMembers and surfaced through JniEnvironment.Runtime like other registration failures. 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.
Note
Draft / exploration. No tests yet and a full
make allhas not been run. Opening for design discussion.Why
In the trimmable typemap path, native methods are registered via the fast path (a Java Callable Wrapper static initializer calls
mono.android.Runtime.registerNatives(Class)→TrimmableTypeMap.OnRegisterNatives→ the generatedIAndroidCallableWrapper.RegisterNatives, with no reflection). The legacy, reflection-based overloadJniRuntime.JniTypeManager.RegisterNativeMembers(JniType, Type, ReadOnlySpan<char> methods)is currently disabled there —TrimmableTypeMapTypeManager.RegisterNativeMembersthrowsUnreachableException.That makes two legacy mechanisms unusable with the trimmable typemap:
jar/aarlibraries) whose static initializers callmono.android.Runtime.register("Type, Asm", Class, "methods").Java.Interop.ManagedPeer(net/dot/jni/ManagedPeer.registerNativeMembers), which callsRuntime.TypeManager.RegisterNativeMembers(...)directly.This PR adds an opt-in feature switch so these can be supported when needed, while keeping the mechanism trimmed away by default ("shave it off when we don't need it").
What
New switch
Microsoft.Android.Runtime.RuntimeFeature.LegacyJniRegistration(MSBuild_AndroidEnableLegacyJniRegistration, defaultfalse, trim-substitutable):Type, which keys directly into the generated proxy.TrimmableTypeMap.TryRegisterNativeMembersresolves it viaGetProxyForManagedType(type)and callsacw.RegisterNatives(nativeClass)— the same trim-safe primitiveOnRegisterNativesuses. Themethodsstring is redundant and is used only for a[Conditional("DEBUG")]validation (JNI-name match + non-empty methods).TrimmableTypeMapTypeManager.RegisterNativeMembers: switch off → throws (guard, as today); on → fast-path reuse, falling back tobase(a safe no-op in trimmed builds) when there is no ACW proxy.JNIEnvInit.InitializewiresregisterJniNativesFnwhen!TrimmableTypeMap || LegacyJniRegistration, so legacymono.android.Runtime.register(...)calls flow back into managed code.Microsoft.Android.Sdk.RuntimeConfig.targets.No native changes:
Java_mono_android_Runtime_registeris a name-exportedJNICALLthat is always present and only acts when the managedregisterJniNativesFnpointer is set. With the switch off, the legacy entry point and theregister(...)callback are unreferenced and trimmed away.Open questions / follow-ups
type(precise per-type registration) rather than offnativeClass's JNI name likeOnRegisterNatives(which handles alias groups). Happy to switch to the JNI-name keying if preferred.jar/ ManagedPeer) still to be added.Unit tests
None yet — draft.