Skip to content

[TrimmableTypeMap] Separate typemap build targets into dedicated .targets files #10779

@simonrozsival

Description

@simonrozsival

Android framework version

net11.0-android (Preview)

Affected platform version

.NET 11

Description

Summary

Extract all typemap-related targets from Xamarin.Android.Common.targets and Microsoft.Android.Sdk.ILLink.targets into a dedicated Xamarin.Android.TypeMap.Legacy.targets file, imported conditionally based on _AndroidTypeMapImplementation. This is a pure refactoring — no new code, no new tasks, no trimmable typemap logic. The only new files are .targets files that reorganize existing MSBuild XML.

After this change, adding a new typemap implementation becomes a matter of creating a new .targets file that is imported when _AndroidTypeMapImplementation == 'trimmable', without touching any of the legacy targets.

Motivation

Today, Xamarin.Android.Common.targets contains all typemap logic inline in _GenerateJavaStubs — JCW generation, ACW map, type mappings, marshal methods, manifest generation, and additional provider sources. This monolithic structure makes it extremely difficult to add a new typemap implementation without accidentally mixing artifacts.

When the trimmable typemap is added, it will generate completely different outputs (a managed TypeMap assembly + JCW Java sources) compared to the legacy path (LLVM IR → .o objects + mono.android.jar copy). If both sets of artifacts are present, the app will fail with duplicate class errors (D8/R8) or native linker errors. The separation must be airtight.

Similarly, Microsoft.Android.Sdk.ILLink.targets contains both shared linking infrastructure (_LinkAssemblies) and legacy-specific ILLink custom steps (_PrepareLinking, trimmer step configuration). The trimmable typemap on CoreCLR still uses ILLink for standard trimming but must NOT get the legacy custom steps (e.g., TypeMappingStep, PreserveRegistrations). On NativeAOT, ILLink is disabled entirely — ILC handles trimming.

Current state — Build pipeline for type mapping

The existing build pipeline has two typemap "implementations" controlled by _AndroidTypeMapImplementation:

  • llvm-ir (default for MonoVM and CoreCLR) — native binary lookup tables compiled from LLVM IR into the app's .so
  • managed (default for NativeAOT) — ILLink-substituted hash-based arrays in ManagedTypeMapping, with pre-generated marshal method lookup via ManagedMarshalMethodsLookupTable

Key properties

Property Default Purpose
_AndroidTypeMapImplementation llvm-ir (non-NativeAOT), managed (NativeAOT) Selects typemap strategy
_TypeMapKind mvid (Release), strings-asm (Debug) Sub-variant of the llvm-ir implementation
_AndroidUseMarshalMethods false (Debug), $(AndroidEnableMarshalMethods) (Release) Enables pre-generated marshal method stubs

Where typemap logic currently lives

In Xamarin.Android.Common.targets_GenerateJavaStubs target:

  1. GenerateJavaCallableWrappers — generates .java JCW source files
  2. GenerateACWMap — generates acw-map.txt
  3. GenerateJavaStubs — scans for marshal methods, builds NativeCodeGenState
  4. RewriteMarshalMethods — rewrites assemblies with marshal method stubs (Release only)
  5. GenerateTypeMappings — generates LLVM IR (.ll) typemap lookup tables
  6. GenerateMainAndroidManifest — generates AndroidManifest.xml (depends on NativeCodeGenState)
  7. GenerateAdditionalProviderSources — generates runtime provider .java files

In Xamarin.Android.Common.targets — other targets:

  • _GetMonoPlatformJarPath — locates prebuilt mono.android.jar
  • _PrepareNativeAssemblySources — prepares per-ABI native source items from _TypeMapAssemblySource
  • _AddStaticResources — copies mono.android.jar, generates TypeManager.java
  • _RemoveRegisterAttribute — strips [Register] from assemblies after linking

In Microsoft.Android.Sdk.ILLink.targets:

  • _PrepareLinking — configures ILLink custom steps, preserve lists, trimmer data
  • _FixRootAssembly — adjusts root assembly mode
  • _TouchAndroidLinkFlag — writes link flag for _RemoveRegisterAttribute
  • _LinkAssemblies — shared linking dependency chain (needed by ALL paths)

Prebuilt SDK artifacts that are typemap-specific

Artifact Where it lives Typemap coupling
mono.android.jar $(_XATargetFrameworkDirectories) Contains prebuilt JCWs for Mono.Android.dll types. Trimmable typemap generates its own JCWs — both present → duplicate class errors.
java_runtime_*.jar MSBuild targets directory Contains ManagedPeer, TypeManager — runtime infrastructure. Shared by both paths.
TypeManager.java Generated by CreateTypeManagerJava Only needed when marshal methods enabled in legacy mode.
Native .o typemap objects Compiled from LLVM IR .ll files Linked into libxamarin-app.so. Only produced by legacy path.

What this issue changes

Naming: "Legacy" for existing implementations

The file covering both llvm-ir and managed implementations should be named Xamarin.Android.TypeMap.Legacy.targets. Both implementations share the same JCW generation → stub generation → type mapping pipeline (just with different flags). "Legacy" clearly communicates "everything that isn't the new trimmable system" without being misleading about the managed NativeAOT path.

File structure

File Purpose
Xamarin.Android.TypeMap.Legacy.targets All existing typemap logic (llvm-ir + managed)
Xamarin.Android.TypeMap.Trimmable.targets Shared config + runtime dispatcher + validation
Xamarin.Android.TypeMap.Trimmable.CoreCLR.targets CoreCLR-specific stubs (empty for now)
Xamarin.Android.TypeMap.Trimmable.NativeAOT.targets NativeAOT-specific stubs (empty for now)

The trimmable path needs CoreCLR/NativeAOT-specific files because their build pipelines are fundamentally different:

  • CoreCLR: TypeMap assembly will be generated after _GenerateJavaStubs, before ILLink. ILLink trims it.
  • NativeAOT: ILLink is disabled. TypeMap assembly will be generated before ILC from post-linked assemblies. Must be added to IlcReference, UnmanagedEntryPointsAssembly, and TrimmerRootAssembly.

A single trimmable targets file would need Condition attributes on nearly every target. Two small runtime-specific files are cleaner.

Step 1: Create Xamarin.Android.TypeMap.Legacy.targets

Move all legacy-specific targets, their UsingTask declarations, and related properties.

UsingTask declarations to move from Common.targets:

  • CollectTypeMapFilesForArchive
  • CreateTypeManagerJava
  • GenerateTypeMappings
  • GetMonoPlatformJar
  • RemoveRegisterAttribute

Properties to define:

  • _RemoveRegisterFlag — flag file for incremental _RemoveRegisterAttribute
  • _BeforeGenerateAndroidManifestTasks = _GenerateLegacyJavaCallableWrappers — hooks legacy JCW generation into _GenerateJavaStubs dependency chain (see Step 3)

Targets to define:

Target Source Hook mechanism
_GenerateLegacyJavaCallableWrappers Tasks currently inline in _GenerateJavaStubs body (GenerateJavaCallableWrappers, GenerateACWMap, GenerateJavaStubs, RewriteMarshalMethods) Runs as dependency of _GenerateJavaStubs via _BeforeGenerateAndroidManifestTasks property
_GenerateLegacyAndroidManifest GenerateMainAndroidManifest + GenerateAdditionalProviderSources tasks from _GenerateJavaStubs body AfterTargets="_GenerateJavaStubs"
_SetLegacyTypemapProperties _TypeMapKind property assignment from _CreatePropertiesCache area BeforeTargets="_CreatePropertiesCache"
_GetMonoPlatformJarPath Existing target in Common.targets Overrides empty stub defined in Common.targets before imports
_PrepareNativeAssemblySources Existing target in Common.targets Overrides empty stub defined in Common.targets before imports
_AddLegacyTypeManagerResources CreateTypeManagerJava + mono.android.jar copy from _AddStaticResources AfterTargets="_AddStaticResources"
_GenerateLegacyTypeMappings GenerateTypeMappings call from _GenerateJavaStubs body (can also be kept inside _GenerateLegacyJavaCallableWrappers if ordering allows) AfterTargets="_GenerateJavaStubs" or inside _GenerateLegacyJavaCallableWrappers
_PrepareLinking Currently in ILLink.targets AfterTargets="ComputeResolvedFilesToPublishList"
_FixRootAssembly Currently in ILLink.targets AfterTargets="PrepareForILLink"
_TouchAndroidLinkFlag Currently in ILLink.targets AfterTargets="ILLink"
_RemoveRegisterAttribute Existing target in Common.targets (full version with RemoveRegisterAttribute task) Overrides simplified version in Common.targets
_CollectLegacyTypeMapFiles CollectTypeMapFilesForArchive invocation BeforeTargets="_BuildApk"

Step 2: Create trimmable target stubs

Xamarin.Android.TypeMap.Trimmable.targets (shared dispatcher):

  • Define _TypeMapAssemblyName property (used by the trimmable typemap implementation)
  • Conditionally import CoreCLR or NativeAOT sub-targets based on $(_AndroidRuntime)
  • Validation target: error if runtime is not CoreCLR or NativeAOT

Xamarin.Android.TypeMap.Trimmable.CoreCLR.targets (stub):

  • Override _PrepareNativeAssemblySources as empty (no native typemap sources)
  • TODO comments marking where the trimmable typemap will add manifest generation + TypeMap assembly generation

Xamarin.Android.TypeMap.Trimmable.NativeAOT.targets (stub):

  • TODO comments marking where the trimmable typemap will add ILC integration
  • Note: Do NOT set RunILLink=false / PublishTrimmed=false in stubs — this would break NativeAOT builds. Only add these when the replacement typemap generation is implemented.

Step 3: Modify Common.targets

  1. Add empty stub targets BEFORE the conditional imports:

    <!-- Empty stubs for targets overridden by Legacy/Trimmable .targets files.
         These MUST be defined BEFORE the imports so that the imported overrides win
         ("last definition wins" in MSBuild). -->
    <Target Name="_GetMonoPlatformJarPath" />
    <Target Name="_PrepareNativeAssemblySources" />
    
    <Import Project="Xamarin.Android.TypeMap.Trimmable.targets"
        Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' " />
    <Import Project="Xamarin.Android.TypeMap.Legacy.targets"
        Condition=" '$(_AndroidTypeMapImplementation)' != 'trimmable' " />
  2. Add _BeforeGenerateAndroidManifestTasks to _GenerateJavaStubs DependsOnTargets:

    <Target Name="_GenerateJavaStubs"
        DependsOnTargets="$(_GenerateJavaStubsDependsOnTargets);$(BeforeGenerateAndroidManifest);$(_BeforeGenerateAndroidManifestTasks)"
        ...>

    This property is set by Legacy.targets to _GenerateLegacyJavaCallableWrappers. When trimmable, it's unset and _GenerateJavaStubs runs without legacy JCW generation.

  3. Strip _GenerateJavaStubs body — remove all task invocations. Keep only:

    • _MergedManifestDocuments item setup
    • Stamp file output (<Touch>)
    • <FileWrites> for $(_ManifestOutput)
  4. Simplify _RemoveRegisterAttribute — remove RemoveRegisterAttribute task call, _AndroidLinkFlag inputs, _RemoveRegisterFlag outputs. Keep only the assembly copy logic. Legacy.targets overrides this with the full version.

  5. Remove _TypeMapKind and TypeMapKind=$(_TypeMapKind) from property cache — moved to _SetLegacyTypemapProperties in Legacy.targets.

  6. Remove legacy items from _AddStaticResourcesCreateTypeManagerJava call, mono.android.jar copy/touch, related <FileWrites>.

  7. Move legacy UsingTask declarations to Legacy.targets.

Step 4: Simplify Microsoft.Android.Sdk.ILLink.targets

Remove _PrepareLinking, _FixRootAssembly, _TouchAndroidLinkFlag (all moved to Legacy.targets). Keep only _LinkAssemblies.

Step 5: Add notes to Microsoft.Android.Sdk.NativeAOT.targets

Add comments indicating where the trimmable typemap will hook in. No functional changes needed.

Things to watch out for

Target ordering with AfterTargets

Multiple targets using AfterTargets="_GenerateJavaStubs" run in undefined order. If ordering between them matters (e.g., manifest generation depends on typemap generation), use explicit DependsOnTargets between them.

_RemoveRegisterAttribute incremental build

The legacy version has Inputs="$(_AndroidLinkFlag)" / Outputs="$(_RemoveRegisterFlag)" for incremental builds. The simplified version in Common.targets (trimmable path) doesn't need these because it doesn't produce the flag file. Verify incremental build correctness for both paths.

ILLink custom steps must NOT reach the trimmable path

The trimmable CoreCLR path still uses ILLink (for standard trimming) but must NOT get legacy custom steps like TypeMappingStep or PreserveRegistrations. Verify that _PrepareLinking only runs from Legacy.targets.

NativeAOT stubs must not break current NativeAOT builds

Stubs for trimmable NativeAOT must be truly empty. Do NOT set RunILLink=false or disable any NativeAOT build infrastructure — that comes when the replacement typemap generation is implemented.

_CollectRuntimeJarFilenames — shared, not legacy

The runtime jar selection (java_runtime_clr.jar / java_runtime_net6.jar) is shared by both paths. Don't move it into Legacy.targets. Both paths need runtime jars — the difference is in mono.android.jar (legacy-only) and TypeManager.java (legacy-only).

Test coverage

The test "build with _AndroidTypeMapImplementation=trimmable succeeds" will produce an app that crashes at runtime (no typemap, no trimmable manifest). The test should verify build completion only — not device deployment. More useful artifact verification tests:

  • trimmable build does NOT contain typemap.*.o files
  • trimmable build does NOT copy mono.android.jar
  • trimmable build does NOT invoke RemoveRegisterAttribute

Definition of Done

  • Xamarin.Android.Build.Tests full suite passes (all existing tests use legacy path by default)
  • New file: Xamarin.Android.TypeMap.Legacy.targets with all legacy typemap targets
  • New file: Xamarin.Android.TypeMap.Trimmable.targets (dispatcher with validation)
  • New file: Xamarin.Android.TypeMap.Trimmable.CoreCLR.targets (empty stubs)
  • New file: Xamarin.Android.TypeMap.Trimmable.NativeAOT.targets (empty stubs)
  • Common.targets has zero legacy typemap task invocations — only shared setup + conditional imports
  • ILLink.targets reduced to _LinkAssemblies only
  • Build with default settings produces identical output (verified by full test suite)
  • Build with _AndroidTypeMapImplementation=trimmable succeeds (stubs are no-ops)
  • Artifact test: trimmable build does NOT produce native typemap .o objects
  • Artifact test: trimmable build does NOT copy mono.android.jar
  • Artifact test: trimmable build does NOT invoke RemoveRegisterAttribute
  • No UsingTask for legacy-only tasks when trimmable is active

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions