Skip to content

[TrimmableTypeMap] Support legacy RegisterNativeMembers#11652

Draft
simonrozsival wants to merge 5 commits into
mainfrom
dev/simonrozsival/trimmable-registernatives-signatures
Draft

[TrimmableTypeMap] Support legacy RegisterNativeMembers#11652
simonrozsival wants to merge 5 commits into
mainfrom
dev/simonrozsival/trimmable-registernatives-signatures

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Note

Draft / exploration. No tests yet and a full make all has 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 generated IAndroidCallableWrapper.RegisterNatives, with no reflection). The legacy, reflection-based overload JniRuntime.JniTypeManager.RegisterNativeMembers(JniType, Type, ReadOnlySpan<char> methods) is currently disabled there — TrimmableTypeMapTypeManager.RegisterNativeMembers throws UnreachableException.

That makes two legacy mechanisms unusable with the trimmable typemap:

  • Legacy precompiled JCWs (from binding jar/aar libraries) whose static initializers call mono.android.Runtime.register("Type, Asm", Class, "methods").
  • Java.Interop.ManagedPeer (net/dot/jni/ManagedPeer.registerNativeMembers), which calls Runtime.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, default false, trim-substitutable):

  • Reuses the fast path instead of reflection. The legacy overload already carries the managed Type, which keys directly into the generated proxy. TrimmableTypeMap.TryRegisterNativeMembers resolves it via GetProxyForManagedType(type) and calls acw.RegisterNatives(nativeClass) — the same trim-safe primitive OnRegisterNatives uses. The methods string 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 to base (a safe no-op in trimmed builds) when there is no ACW proxy.
  • JNIEnvInit.Initialize wires registerJniNativesFn when !TrimmableTypeMap || LegacyJniRegistration, so legacy mono.android.Runtime.register(...) calls flow back into managed code.
  • MSBuild wiring in Microsoft.Android.Sdk.RuntimeConfig.targets.

No native changes: Java_mono_android_Runtime_register is a name-exported JNICALL that is always present and only acts when the managed registerJniNativesFn pointer is set. With the switch off, the legacy entry point and the register(...) callback are unreferenced and trimmed away.

Open questions / follow-ups

  • Proxy lookup is keyed off type (precise per-type registration) rather than off nativeClass's JNI name like OnRegisterNatives (which handles alias groups). Happy to switch to the JNI-name keying if preferred.
  • A legacy type with no generated proxy (binding assembly not scanned by the generator) currently falls back to a no-op; could log a diagnostic there.
  • Tests (and an end-to-end run with a legacy jar / ManagedPeer) still to be added.

Unit tests

None yet — draft.

simonrozsival and others added 2 commits June 14, 2026 19:24
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>
@simonrozsival simonrozsival changed the title [Mono.Android] Support legacy RegisterNativeMembers in trimmable typemap [TrimmableTypeMap] Support legacy RegisterNativeMembers Jun 15, 2026
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>
@simonrozsival

Copy link
Copy Markdown
Member Author

Updated this draft per the review feedback:

  • Renamed the switch to RuntimeFeature.StringBasedJniRegistration and added [FeatureGuard(typeof(RequiresUnreferencedCodeAttribute))].
  • Defaulted _AndroidEnableStringBasedJniRegistration to true for non-trimmable typemaps and false for _AndroidTypeMapImplementation=trimmable.
  • Extracted the string-based RegisterNativeMembers parser/registration implementation from AndroidTypeManager into shared NativeMethodRegistration.
  • AndroidTypeManager now uses that helper after the linker-generated fast registration map.
  • TrimmableTypeMapTypeManager uses the helper only when StringBasedJniRegistration is enabled; otherwise it throws actionable guidance with the .csproj opt-in snippet.
  • Split responsibilities more clearly:
    • JNIEnvInit wires the trimmable mono.android.Runtime.registerNatives(Class) JNI callback.
    • TrimmableTypeMap resolves proxies/wrappers and invokes generated IAndroidCallableWrapper.RegisterNatives.
    • TrimmableTypeMapTypeManager owns the JniTypeManager.RegisterNativeMembers behavior.

Local validation:

  • make prepare && make all passed.
  • Direct Mono.Android.csproj build passed.
  • Targeted host-side trimmable tests passed:
    • CoreClrTrimmableTypeMap_PackagesReadyToRunTypeMap
    • ReleaseCoreClrTrimmableTypeMap_SupportsExplicitDynamicCodeSupportOff
  • TrimmableTypeMapBuildTests excluding one unrelated cleanup-race test passed: 19 passed / 2 skipped / 0 failed.

Known unrelated local failure observed during validation:

  • Build_WithTrimmableTypeMap_ArrayRankChangeRegeneratesTypeMap fails in GenerateTrimmableTypeMap while deleting its generated temp linked-java output (Directory not empty from Directory.Delete(..., recursive: true)). This is in the test's temp output cleanup path and appears unrelated to the string-based JNI registration change.

simonrozsival and others added 2 commits June 15, 2026 13:42
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

copilot `copilot-cli` or other AIs were used to author this trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant