Skip to content

[TrimmableTypeMap] Emit alias-base entry as unconditional for now#11220

Merged
simonrozsival merged 1 commit intodev/simonrozsival/trimmable-test-plumbingfrom
dev/simonrozsival/trimmable-getobject-mostderived
Apr 27, 2026
Merged

[TrimmableTypeMap] Emit alias-base entry as unconditional for now#11220
simonrozsival merged 1 commit intodev/simonrozsival/trimmable-test-plumbingfrom
dev/simonrozsival/trimmable-getobject-mostderived

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

Summary

Fixes Java.LangTests.ObjectTest.GetObject_ReturnsMostDerivedType under the trimmable typemap. Java.Lang.Object.GetObject<Java.Lang.Object>(jstring) was returning a Java.Lang.Object peer instead of the most-derived Java.Lang.String.

Root cause

The alias-group base TypeMap entry (e.g. java/lang/String → alias holder) in ModelBuilder.EmitPeers was always emitted as a 3-arg conditional attribute, bypassing the ForceUnconditionalEntries workaround for dotnet/runtime#127004 that the indexed entries already use.

When that workaround is in effect, the indexed alias entries are 2-arg (unconditional) and the alias holder is reachable only via TypeMapAssociation. Per #127004 the trimmer strips the association when a TypeMap entry references the same type, removing the holder and making the dictionary key java/lang/String (and java/lang/Object, java/lang/Throwable, …) disappear at runtime. The hierarchy walker then advances past the most-derived JNI class and CreatePeer falls back to the targetType-based lookup, returning a base-class peer.

Concrete symptom (verified on device with diagnostic logging):

GetObject_ReturnsMostDerivedType
hierarchy: jni='java/lang/String' targetType='Java.Lang.Object' proxyCount=0
hierarchy: jni='java/lang/Object' targetType='Java.Lang.Object' proxyCount=0
-> Expected Java.Lang.String but was Java.Lang.Object

Fix

Route the alias-base entry through the same unconditional decision used by BuildEntry: emit it as 2-arg whenever ForceUnconditionalEntries is on, the JNI name is in EssentialRuntimeTypes, or any peer in the alias group is itself unconditional.

After the fix, the same lookup reports proxyCount=2 and selects Java.Lang.String. Test exclusion removed in the same change.

Verification

Full Mono.Android.NET-Tests Release+CoreCLR+Trimmable run on device: 917 tests, 0 failures, 0 errors.

The alias-group base entry (e.g. java/lang/String -> alias holder) was
emitted as a 3-arg conditional TypeMap attribute regardless of the
ForceUnconditionalEntries workaround for dotnet/runtime#127004.

When that workaround is in effect, the indexed alias entries are 2-arg
(unconditional) and the alias holder is reachable only via
TypeMapAssociation. Per #127004 the trimmer strips the association when
a TypeMap entry references the same type, removing the holder and
making the dictionary key 'java/lang/String' (and 'java/lang/Object',
'java/lang/Throwable', ...) disappear at runtime. The hierarchy walker
then advances past the most-derived JNI class and CreatePeer falls back
to the targetType-based lookup, returning a base-class peer.

Concrete symptom (verified on device):
    GetObject_ReturnsMostDerivedType
    hierarchy: jni='java/lang/String' targetType='Java.Lang.Object' proxyCount=0
    hierarchy: jni='java/lang/Object' targetType='Java.Lang.Object' proxyCount=0
    -> Expected Java.Lang.String but was Java.Lang.Object

Route the alias-base entry through the same unconditional decision used
by BuildEntry: emit it as 2-arg whenever ForceUnconditionalEntries is on,
the JNI name is in EssentialRuntimeTypes, or any peer in the alias group
already needs to be unconditional. After the fix the same lookup
reports proxyCount=2 and selects Java.Lang.String, and the test (and the
full Mono.Android.NET-Tests trimmable suite) passes with 0 failures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 27, 2026 09:06
@simonrozsival simonrozsival changed the title Trimmable typemap: emit alias-base entry as unconditional [TrimmableTypeMap] Emit alias-base entry as unconditional for now Apr 27, 2026
@simonrozsival simonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Apr 27, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 incorrect most-derived Java peer resolution when using the trimmable typemap by ensuring the alias-group base TypeMap entry is emitted as unconditional when required, preventing the trimmer from stripping the alias holder mapping (workaround for dotnet/runtime#127004). It also re-enables the previously excluded runtime test that validates most-derived type selection.

Changes:

  • Update ModelBuilder.EmitPeers to emit the alias-base entry as 2-arg/unconditional under the same conditions as indexed entries (notably when ForceUnconditionalEntries applies).
  • Remove the trimmable-typemap exclusion for Java.LangTests.ObjectTest.GetObject_ReturnsMostDerivedType.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs Aligns alias-base entry emission with unconditional/conditional decision logic to avoid alias holder trimming and missing base JNI keys at runtime.
tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs Removes an exclusion so the most-derived-type regression test runs under the trimmable typemap.

Comment on lines +185 to 192
bool aliasBaseUnconditional = ForceUnconditionalEntries
|| EssentialRuntimeTypes.Contains (jniName)
|| peersForName.Any (IsUnconditionalEntry);
model.Entries.Add (new TypeMapAttributeData {
JniName = jniName,
ProxyTypeReference = holderRef,
TargetTypeReference = holderRef,
TargetTypeReference = aliasBaseUnconditional ? null : holderRef,
});
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

The new alias-base unconditional decision (aliasBaseUnconditional) is a key trimming workaround, but there doesn’t appear to be a unit/integration test asserting that the base alias entry (e.g., "test/Dup" → holder) is emitted as a 2-arg/unconditional TypeMap attribute (i.e., TargetTypeReference == null) when ForceUnconditionalEntries is enabled (and/or when any peer in the alias group is unconditional). Adding a regression test for this would help prevent reintroducing the original issue where only the indexed entries honored ForceUnconditionalEntries.

Copilot uses AI. Check for mistakes.
@simonrozsival simonrozsival merged commit 1c03ddc into dev/simonrozsival/trimmable-test-plumbing Apr 27, 2026
6 of 7 checks passed
@simonrozsival simonrozsival deleted the dev/simonrozsival/trimmable-getobject-mostderived branch April 27, 2026 12:49
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.

2 participants