[TrimmableTypeMap] Fix NativeAOT interface listener callbacks: encode JNI boolean as System.Boolean in n_* MemberRefs#11802
Open
simonrozsival wants to merge 2 commits into
Open
Conversation
…ck refs Fixes #11773. The trimmable typemap generator emits, for each Java peer marshal method, a UCO forwarder that calls the MCW-generated `n_*` callback via a cross-assembly MemberRef. The MemberRef signature was built by `EncodeClrTypeForCallback`, which encoded a JNI boolean (`Z`) parameter/return as `System.SByte`. The real MCW `n_*` callbacks declare their JNI boolean as `System.Boolean` (e.g. `AndroidX.Fragment.App.FragmentManager.IOnBackStackChangedListener. n_OnBackStackChangeStarted_..._Z(IntPtr, IntPtr, IntPtr, bool)`). Because `System.SByte != System.Boolean` in metadata, the emitted MemberRef could not be bound to the real method, so ILC reported: ILC: Method '..._Proxy.n_OnBackStackChangeStarted_uco_*' will always throw because: Missing method 'Void ...n_OnBackStackChangeStarted_...(..., SByte)' and replaced the forwarder body with a throw -- breaking every binding/AndroidX interface listener callback that has a boolean parameter (and any boolean- returning callback) under NativeAOT. This was misdiagnosed as a trimming/preserve-edge problem. It is not: a rooted cross-assembly call to a non-public static method -- including one declared on an interface -- is preserved by ILC. The failure is purely a signature mismatch in the generated MemberRef. Reproduced minimally by emitting an `sbyte` MemberRef against a `bool` method and re-running ilc, which produces the exact "will always throw / Missing method ... SByte" diagnostic from the issue; switching the MemberRef to `bool` resolves and runs. The fix encodes JNI boolean as `System.Boolean` for the callback MemberRef. The UCO entry point keeps `byte` (blittable, unsigned) for the `[UnmanagedCallersOnly]` ABI; only the managed `n_*` MemberRef changes. This matches the constructor path, which already references managed boolean parameters as `System.Boolean`. Passing the UCO `byte` argument to the `bool` parameter is valid IL (both are stack-int32; JNI jboolean is always 0/1), mirroring the legacy LLVM-IR/marshal-methods path which also targets the real `bool` `n_*` callbacks. Four unit tests had codified the incorrect `sbyte` expectation; they are updated to assert `System.Boolean` for the callback signature while keeping `System.Byte` for the UCO ABI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes NativeAOT failures in the TrimmableTypeMap generator where interface listener callbacks involving JNI boolean (Z) could not bind to the MCW-generated n_* methods due to a MemberRef signature mismatch (System.SByte vs System.Boolean). By encoding callback booleans as System.Boolean (while keeping the UCO ABI as byte), the generated cross-assembly MemberRefs correctly resolve and avoid the ILC “will always throw / Missing method … (…, SByte)” diagnostics.
Changes:
- Update
JniSignatureHelper.EncodeClrTypeForCallbackto encode JNIbooleanasSystem.Booleaninstead ofSystem.SByte. - Update unit tests to assert the corrected callback signature encoding while preserving
bytefor the UCO ABI. - Strengthen/clarify regression-test comments around the prior failure mode (unresolvable MemberRef → ILC diagnostic).
Show a summary per file
| File | Description |
|---|---|
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs |
Fixes callback MemberRef boolean encoding to System.Boolean, matching MCW n_* signatures. |
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs |
Updates assertions and regression tests to validate byte UCO ABI vs bool callback MemberRef encoding. |
Copilot's findings
- Files reviewed: 2/2 changed files
- Comments generated: 0
Member
|
/review |
1 similar comment
Member
Author
|
/review |
This was referenced Jul 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #11773.
Problem
Under the trimmable typemap (NativeAOT), binding/AndroidX interface listener callbacks that have a boolean parameter (e.g.
IOnBackStackChangedListener.OnBackStackChangeStarted) don't work. ILC reports:…and replaces the generated UCO forwarder body with a throw.
Root cause — a signature mismatch, not trimming
The generator emits, for each marshal method, a UCO forwarder that calls the MCW-generated
n_*callback via a cross-assemblyMemberRef. That MemberRef signature is built byJniSignatureHelper.EncodeClrTypeForCallback, which encoded a JNI boolean (Z) asSystem.SByte.But the real MCW
n_*callbacks declare their JNI boolean asSystem.Boolean. Decompiled from the shippingXamarin.AndroidX.Fragment:Since
System.SByte != System.Booleanin metadata, the emitted MemberRef can't bind to the real method → "Missing method" → "will always throw". This only affects callbacks with a boolean parameter (or boolean return), which is exactly the observed failure set.This was previously misdiagnosed as trimming
A rooted cross-assembly
callto a non-publicstaticmethod — including one declared on an interface — is preserved by ILC. Verified with a minimal 2-project NativeAOT repro: the call survives and runs. Re-runningilcwith ansbyteMemberRef against aboolmethod reproduces the exact issue diagnostic; switching toboolresolves and runs. So the earlier direct-dispatch attempt (which caused the reverted infinite-recursion regression) was treating the wrong problem.Fix
Encode JNI boolean as
System.Booleanfor the callback MemberRef. The UCO entry point keepsbyte(blittable, unsigned) for the[UnmanagedCallersOnly]ABI — only the managedn_*MemberRef changes. This matches the constructor path, which already references managed boolean parameters asSystem.Boolean. Passing the UCObyteargument into theboolparameter is valid IL (both are stack-int32; JNIjbooleanis always 0/1), and mirrors the legacy LLVM-IR/marshal-methods path, which also targets the realbooln_*callbacks.Tests
sbyteexpectation; they now assertSystem.Booleanfor the callback signature while keepingSystem.Bytefor the UCO ABI.Remaining / acceptance criteria
This resolves the ILC "will always throw" diagnostic and the binding-interface-callback breakage at the generator level. The issue's device-test criterion (
IOnBackStackChangedListenerfiring under NativeAOT, no regression for built-in listeners likeGlobalLayoutEvent_ShouldRegisterAndFire_OnActivityLaunch) should be validated by the device test legs.