Skip to content

[TrimmableTypeMap] Fix NativeAOT interface listener callbacks: encode JNI boolean as System.Boolean in n_* MemberRefs#11802

Open
simonrozsival wants to merge 2 commits into
mainfrom
android-fix-11773-bool-callback-sig
Open

[TrimmableTypeMap] Fix NativeAOT interface listener callbacks: encode JNI boolean as System.Boolean in n_* MemberRefs#11802
simonrozsival wants to merge 2 commits into
mainfrom
android-fix-11773-bool-callback-sig

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

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:

ILC: Method '..._IOnBackStackChangedListener_Proxy.n_OnBackStackChangeStarted_uco_*' will always throw
     because: Missing method 'Void ...IOnBackStackChangedListener.n_OnBackStackChangeStarted_...(..., SByte)'

…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-assembly MemberRef. That MemberRef signature is built by JniSignatureHelper.EncodeClrTypeForCallback, which encoded a JNI boolean (Z) as System.SByte.

But the real MCW n_* callbacks declare their JNI boolean as System.Boolean. Decompiled from the shipping Xamarin.AndroidX.Fragment:

.method ... static void n_OnBackStackChangeStarted_Landroidx_fragment_app_Fragment_Z (
    native int jnienv, native int native__this, native int native_fragment, bool pop) cil managed

Since System.SByte != System.Boolean in 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 call to a non-public static method — including one declared on an interfaceis preserved by ILC. Verified with a minimal 2-project NativeAOT repro: the call survives and runs. Re-running ilc with an sbyte MemberRef against a bool method reproduces the exact issue diagnostic; switching to bool resolves 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.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 into the bool parameter is valid IL (both are stack-int32; JNI jboolean is always 0/1), and mirrors the legacy LLVM-IR/marshal-methods path, which also targets the real bool n_* callbacks.

-		case JniParamKind.Boolean: encoder.SByte (); break;  // MCW n_* callbacks use sbyte for JNI boolean
+		case JniParamKind.Boolean: encoder.Boolean (); break; // MCW n_* callbacks declare JNI boolean as System.Boolean

Tests

  • Updated the 4 unit tests that had codified the incorrect sbyte expectation; they now assert System.Boolean for the callback signature while keeping System.Byte for the UCO ABI.
  • All TrimmableTypeMap generator unit tests pass (564/564).

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 (IOnBackStackChangedListener firing under NativeAOT, no regression for built-in listeners like GlobalLayoutEvent_ShouldRegisterAndFire_OnActivityLaunch) should be validated by the device test legs.

…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>
Copilot AI review requested due to automatic review settings June 29, 2026 13:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.EncodeClrTypeForCallback to encode JNI boolean as System.Boolean instead of System.SByte.
  • Update unit tests to assert the corrected callback signature encoding while preserving byte for 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

@simonrozsival simonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Jun 29, 2026
@simonrozsival simonrozsival enabled auto-merge (squash) June 29, 2026 16:46
@simonrozsival simonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jun 29, 2026
@jonathanpeppers

Copy link
Copy Markdown
Member

/review

1 similar comment
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@simonrozsival simonrozsival removed the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[TrimmableTypeMap] Binding/AndroidX interface listener callbacks don't work under NativeAOT (direct-dispatch caused recursion, reverted)

3 participants