-
Notifications
You must be signed in to change notification settings - Fork 566
Description
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.somanaged(default for NativeAOT) — ILLink-substituted hash-based arrays inManagedTypeMapping, with pre-generated marshal method lookup viaManagedMarshalMethodsLookupTable
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:
GenerateJavaCallableWrappers— generates.javaJCW source filesGenerateACWMap— generatesacw-map.txtGenerateJavaStubs— scans for marshal methods, buildsNativeCodeGenStateRewriteMarshalMethods— rewrites assemblies with marshal method stubs (Release only)GenerateTypeMappings— generates LLVM IR (.ll) typemap lookup tablesGenerateMainAndroidManifest— generatesAndroidManifest.xml(depends onNativeCodeGenState)GenerateAdditionalProviderSources— generates runtime provider.javafiles
In Xamarin.Android.Common.targets — other targets:
_GetMonoPlatformJarPath— locates prebuiltmono.android.jar_PrepareNativeAssemblySources— prepares per-ABI native source items from_TypeMapAssemblySource_AddStaticResources— copiesmono.android.jar, generatesTypeManager.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, andTrimmerRootAssembly.
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:
CollectTypeMapFilesForArchiveCreateTypeManagerJavaGenerateTypeMappingsGetMonoPlatformJarRemoveRegisterAttribute
Properties to define:
_RemoveRegisterFlag— flag file for incremental_RemoveRegisterAttribute_BeforeGenerateAndroidManifestTasks=_GenerateLegacyJavaCallableWrappers— hooks legacy JCW generation into_GenerateJavaStubsdependency 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
_TypeMapAssemblyNameproperty (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
_PrepareNativeAssemblySourcesas 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=falsein stubs — this would break NativeAOT builds. Only add these when the replacement typemap generation is implemented.
Step 3: Modify Common.targets
-
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' " />
-
Add
_BeforeGenerateAndroidManifestTasksto_GenerateJavaStubsDependsOnTargets:<Target Name="_GenerateJavaStubs" DependsOnTargets="$(_GenerateJavaStubsDependsOnTargets);$(BeforeGenerateAndroidManifest);$(_BeforeGenerateAndroidManifestTasks)" ...>
This property is set by Legacy.targets to
_GenerateLegacyJavaCallableWrappers. When trimmable, it's unset and_GenerateJavaStubsruns without legacy JCW generation. -
Strip
_GenerateJavaStubsbody — remove all task invocations. Keep only:_MergedManifestDocumentsitem setup- Stamp file output (
<Touch>) <FileWrites>for$(_ManifestOutput)
-
Simplify
_RemoveRegisterAttribute— removeRemoveRegisterAttributetask call,_AndroidLinkFlaginputs,_RemoveRegisterFlagoutputs. Keep only the assembly copy logic. Legacy.targets overrides this with the full version. -
Remove
_TypeMapKindandTypeMapKind=$(_TypeMapKind)from property cache — moved to_SetLegacyTypemapPropertiesin Legacy.targets. -
Remove legacy items from
_AddStaticResources—CreateTypeManagerJavacall,mono.android.jarcopy/touch, related<FileWrites>. -
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:
trimmablebuild does NOT containtypemap.*.ofilestrimmablebuild does NOT copymono.android.jartrimmablebuild does NOT invokeRemoveRegisterAttribute
Definition of Done
-
Xamarin.Android.Build.Testsfull suite passes (all existing tests use legacy path by default) - New file:
Xamarin.Android.TypeMap.Legacy.targetswith 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.targetshas zero legacy typemap task invocations — only shared setup + conditional imports -
ILLink.targetsreduced to_LinkAssembliesonly - Build with default settings produces identical output (verified by full test suite)
- Build with
_AndroidTypeMapImplementation=trimmablesucceeds (stubs are no-ops) - Artifact test:
trimmablebuild does NOT produce native typemap.oobjects - Artifact test:
trimmablebuild does NOT copymono.android.jar - Artifact test:
trimmablebuild does NOT invokeRemoveRegisterAttribute - No UsingTask for legacy-only tasks when trimmable is active