[TrimmableTypeMap] Support legacy RegisterNativeMembers - #11652

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

[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.

simonrozsivaland 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>
@simonrozsivalsimonrozsival changed the title [Mono.Android] Support legacy RegisterNativeMembers in trimmable typemap[TrimmableTypeMap] Support legacy RegisterNativeMembersJun 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
MemberAuthor

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.

simonrozsivaland 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>
@simonrozsivalsimonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 15, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 27, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@simonrozsival
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[TrimmableTypeMap] Support legacy RegisterNativeMembers - #11652

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

[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.

simonrozsivaland 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>
@simonrozsivalsimonrozsival changed the title [Mono.Android] Support legacy RegisterNativeMembers in trimmable typemap[TrimmableTypeMap] Support legacy RegisterNativeMembersJun 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
MemberAuthor

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.

simonrozsivaland 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>
@simonrozsivalsimonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 15, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 27, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@simonrozsival
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[TrimmableTypeMap] Support legacy RegisterNativeMembers - #11652

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

[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.

simonrozsivaland 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>
@simonrozsivalsimonrozsival changed the title [Mono.Android] Support legacy RegisterNativeMembers in trimmable typemap[TrimmableTypeMap] Support legacy RegisterNativeMembersJun 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
MemberAuthor

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.

simonrozsivaland 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>
@simonrozsivalsimonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 15, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 27, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@simonrozsival
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[TrimmableTypeMap] Support legacy RegisterNativeMembers - #11652

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

[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.

simonrozsivaland 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>
@simonrozsivalsimonrozsival changed the title [Mono.Android] Support legacy RegisterNativeMembers in trimmable typemap[TrimmableTypeMap] Support legacy RegisterNativeMembersJun 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
MemberAuthor

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.

simonrozsivaland 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>
@simonrozsivalsimonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 15, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 27, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@simonrozsival
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[TrimmableTypeMap] Support legacy RegisterNativeMembers - #11652

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

[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.

simonrozsivaland 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>
@simonrozsivalsimonrozsival changed the title [Mono.Android] Support legacy RegisterNativeMembers in trimmable typemap[TrimmableTypeMap] Support legacy RegisterNativeMembersJun 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
MemberAuthor

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.

simonrozsivaland 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>
@simonrozsivalsimonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 15, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 27, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@simonrozsival
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[TrimmableTypeMap] Support legacy RegisterNativeMembers - #11652

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

[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.

simonrozsivaland 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>
@simonrozsivalsimonrozsival changed the title [Mono.Android] Support legacy RegisterNativeMembers in trimmable typemap[TrimmableTypeMap] Support legacy RegisterNativeMembersJun 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
MemberAuthor

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.

simonrozsivaland 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>
@simonrozsivalsimonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 15, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 27, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@simonrozsival
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[TrimmableTypeMap] Support legacy RegisterNativeMembers - #11652

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

[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.

simonrozsivaland 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>
@simonrozsivalsimonrozsival changed the title [Mono.Android] Support legacy RegisterNativeMembers in trimmable typemap[TrimmableTypeMap] Support legacy RegisterNativeMembersJun 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
MemberAuthor

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.

simonrozsivaland 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>
@simonrozsivalsimonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 15, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 27, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@simonrozsival
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[TrimmableTypeMap] Support legacy RegisterNativeMembers - #11652

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

[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.

simonrozsivaland 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>
@simonrozsivalsimonrozsival changed the title [Mono.Android] Support legacy RegisterNativeMembers in trimmable typemap[TrimmableTypeMap] Support legacy RegisterNativeMembersJun 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
MemberAuthor

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.

simonrozsivaland 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>
@simonrozsivalsimonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 15, 2026
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jul 27, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@simonrozsival