[TrimmableTypeMap] Per-assembly typemap universes with startup hook initialization#11181
Conversation
Implements #11180: Each assembly gets its own typemap universe via a generated __TypeMapAnchor type (instead of sharing Java.Lang.Object), creating isolated TypeMapLazyDictionary instances per assembly. Runtime layer: - Add ITypeMapWithAliasing interface with GetTypes() and TryGetProxyType() - Add SingleUniverseTypeMap (handles alias resolution within one universe) - Add AggregateTypeMap (flattens across N universes, Debug-only) - Simplify TrimmableTypeMap to consume ITypeMapWithAliasing - Remove TrimmableTypeMap.Initialize() call from JNIEnvInit Generator layer: - Emit __TypeMapAnchor in each per-assembly typemap DLL - Use TypeMap<__TypeMapAnchor> instead of TypeMap<Java.Lang.Object> - Add isRelease parameter to control universe merging - Root assembly emits StartupHook class with Initialize() IL method - Debug: constructs N SingleUniverseTypeMap + AggregateTypeMap - Release: single SingleUniverseTypeMap - Root assembly emits IgnoresAccessChecksTo for internal type access MSBuild layer: - Enable StartupHookSupport for trimmable typemap builds - Configure DOTNET_STARTUP_HOOKS to point to root typemap assembly - Fix HotReload targets to compose startup hooks instead of replacing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…, startup hook fixes, tests - Rename isRelease -> mergeAssemblyTypeMaps throughout generator and MSBuild task - Rename EmitReleaseInitialize -> EmitInitializeWithSingleTypeMap - Rename EmitDebugInitialize -> EmitInitializeWithAggregateTypeMap - Change TrimmableTypeMap.Initialize to two overloads with BCL types only: - Initialize(IReadOnlyDictionary<string, Type>, IReadOnlyDictionary<Type, Type>) - Initialize(IReadOnlyDictionary<string, Type>[], IReadOnlyDictionary<Type, Type>[]) - Generated assembly no longer references SingleUniverseTypeMap/AggregateTypeMap - Add InitializeCore that throws InvalidOperationException on re-initialization - Force-enable StartupHookSupport unconditionally in targets - Remove MonoVM STARTUP_HOOKS line (trimmable typemaps don't work with Mono) - Add tests for both mergeAssemblyTypeMaps modes (true/false) - Add tests for IgnoresAccessChecksTo attribute in both modes - Fix locals encoding for aggregate mode (pass type refs to encodeLocals lambda) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
In merged mode (Release), all per-assembly typemap DLLs must use the same anchor type so TypeMapping.GetOrCreateExternalTypeMapping<T>() finds all entries across all assemblies. Previously each DLL emitted its own __TypeMapAnchor, causing the root assembly's GetOrCreate call (using its own anchor) to find nothing. Fix: when mergeAssemblyTypeMaps=true, per-assembly DLLs reference Java.Lang.Object as the anchor type instead of emitting __TypeMapAnchor. The root assembly also references Java.Lang.Object in merged mode. In aggregate mode (Debug), each DLL keeps its own __TypeMapAnchor for isolated per-assembly universes. The root references each per-assembly anchor individually. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Merge two TrimmableTypeMap.Initialize overloads into one that takes arrays and optimizes for single-element case (uses SingleUniverseTypeMap) - Add length-zero validation - Unify IL emission: both modes now use same EmitInitializeBody (always builds arrays), removing EmitInitializeWithSingleTypeMap - Remove AddInitializeSingleRef (only AddInitializeRef remains) - Update doc comments: Option A (shared universe) / Option B (per-assembly) - Fix doc to show Java.Lang.Object as anchor for shared universe mode Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…omments" This reverts commit a25b04a.
- Doc comments: Option A (shared universe) / Option B (per-assembly) - Show Java.Lang.Object as anchor for shared universe mode - Add length-zero check in array Initialize overload Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Implements per-assembly typemap “universes” (Debug) vs a shared merged universe (Release) for the TrimmableTypeMap pipeline, and moves runtime initialization into a generated startup hook assembly (_Microsoft.Android.TypeMaps).
Changes:
- Add alias-aware typemap abstraction in
Mono.Android(ITypeMapWithAliasing,SingleUniverseTypeMap,AggregateTypeMap) and updateTrimmableTypeMapto initialize from startup hook–provided dictionaries. - Update the generator to emit per-assembly anchors (Debug) or use
Java.Lang.Objectas a shared anchor (Release) and generate startup-hook IL to callTrimmableTypeMap.Initialize(...). - Enable startup hooks via MSBuild (
StartupHookSupport,DOTNET_STARTUP_HOOKS) and update Hot Reload targets to compose startup hook values; expand generator tests for both modes.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs | Updates generator invocation to pass the new useSharedTypemapUniverse option. |
| tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs | Adds coverage for both merge modes, assembly refs, and IgnoresAccessChecksTo emission. |
| src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs | Selects shared universe for Release (useSharedTypemapUniverse: !Debug). |
| src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets | Enables startup hooks and sets DOTNET_STARTUP_HOOKS for typemap initialization. |
| src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.HotReload.targets | Preserves existing DOTNET_STARTUP_HOOKS values by composing with Hot Reload agent hook. |
| src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs | Reworks initialization to accept dictionaries/universes and delegates alias logic to ITypeMapWithAliasing. |
| src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs | Introduces alias-aware typemap interfaces/implementations for single and aggregate universes. |
| src/Mono.Android/Android.Runtime/JNIEnvInit.cs | Removes the old direct TrimmableTypeMap.Initialize() call (startup hook now owns initialization). |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs | Threads useSharedTypemapUniverse through generator execution and assembly generation. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs | Adds useSharedTypemapUniverse argument and forwards to emitter. |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs | Emits per-assembly __TypeMapAnchor (Debug) or uses Java.Lang.Object as anchor (Release). |
| src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs | Rewrites root assembly generation to emit startup-hook IL and mode-specific initialization. |
…d using - Make Initialize() method public (startup hooks require public static) - Keep StartupHook class internal (allowed by runtime) - Use ':' literal instead of Path.PathSeparator for DOTNET_STARTUP_HOOKS composition (target is Android/Linux, not the build host) - Remove unused 'using System.Runtime.InteropServices' from ITypeMapWithAliasing.cs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Don't clobber existing DOTNET_STARTUP_HOOKS entries (e.g., from VS tooling). Use the same compose pattern as HotReload targets with ':' separator. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/review |
|
✅ Android PR Reviewer completed successfully! |
There was a problem hiding this comment.
✅ LGTM — Clean design with minor suggestions
Well-structured implementation of per-assembly typemap universes. The architecture separates concerns nicely: ITypeMapWithAliasing abstracts Debug vs Release, the generator emits correct IL for both startup hook paths, and the MSBuild targets properly compose DOTNET_STARTUP_HOOKS.
Positive callouts:
- The
ITypeMapWithAliasingabstraction cleanly isolates alias resolution from theTrimmableTypeMapsingleton - Startup hook composition in both Trimmable and HotReload targets correctly handles the capture→remove→compose pattern
- The
InvalidOperationExceptionre-initialization guard is a good fail-fast improvement over the old silent return - Generated IL signatures and metadata encoding are consistent across both modes
- Tests cover both merge modes for valid PE, assembly refs, and
IgnoresAccessChecksToattributes
Summary: 4 💡 suggestions (code organization, DCL simplification, runtime unit tests, MSBuild dedup). No blocking issues found. CI public checks pass.
Generated by Android PR Reviewer for issue #11181 · ● 6.8M
- Remove outer null check in InitializeCore (single-threaded init via startup hook, lock alone is sufficient) - Split ITypeMapWithAliasing.cs into three files: ITypeMapWithAliasing.cs, SingleUniverseTypeMap.cs, AggregateTypeMap.cs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mono.Android uses explicit Compile includes, not globbing. Add the three new files split from ITypeMapWithAliasing.cs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
MSBuild does not allow built-in metadata references like %(Identity) in Condition attributes of static ItemGroup elements. This caused all trimmable typemap builds to fail during evaluation with: error MSB4190: The reference to the built-in metadata "Identity" at position 2 is not allowed in this condition. Simplify to just add the startup hook directly. Other targets (HotReload) compose on top inside proper Target elements where %(Identity) is allowed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous commit simplified too aggressively — just adding a static RuntimeEnvironmentVariable item would overwrite any existing DOTNET_STARTUP_HOOKS entries (e.g. from VS tooling). Move the compose-with-existing logic into a proper Target where %(Identity) metadata batching is allowed. The Target runs before _AndroidConfigureHotReloadEnvironment so HotReload can compose on top. Verified locally: all 3 TrimmableTypeMapBuildTests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| <RuntimeEnvironmentVariable Remove="DOTNET_STARTUP_HOOKS" /> | ||
| <RuntimeEnvironmentVariable Include="DOTNET_STARTUP_HOOKS" | ||
| Value="$(_TypeMapAssemblyName):@(_ExistingStartupHooks->'%(Value)', ':')" | ||
| Condition=" '@(_ExistingStartupHooks)' != '' " /> | ||
| <RuntimeEnvironmentVariable Include="DOTNET_STARTUP_HOOKS" | ||
| Value="$(_TypeMapAssemblyName)" | ||
| Condition=" '@(_ExistingStartupHooks)' == '' " /> |
There was a problem hiding this comment.
dotnet watch is probably the first feature that relies on $DOTNET_STARTUP_HOOKS on Android.
But you are supposed to be able to append : and list multiple.
This just clobbers the existing value:
I suppose someone could also have an AndroidEnvironment file with the contents:
DOTNET_STARTUP_HOOKS=MyHook.dll
Should we somehow merge the values for $DOTNET_STARTUP_HOOKS to account for both @(RuntimeEnvironmentVariable) and @(AndroidEnvironment)? We could fix in a new PR, the existing HotReload.targets is also wrong.
There was a problem hiding this comment.
I thought about that and that's why there's this
<RuntimeEnvironmentVariable Include="DOTNET_STARTUP_HOOKS"
Value="$(_TypeMapAssemblyName):@(_ExistingStartupHooks->'%(Value)', ':')"
Condition=" '@(_ExistingStartupHooks)' != '' " />which should preserve the previous value(s) of the existing startup hooks.
There was a problem hiding this comment.
I suppose someone could also have an AndroidEnvironment file with the contents:
right, that would be an issue. I suggest we fix that in a separate PR.
There was a problem hiding this comment.
I opened an issue #11186 and I'll let copilot look into it overnight

Summary
Implements per-assembly typemap universes for incremental Debug builds and merged Release builds, as described in #11180.
Changes
Runtime (
Mono.Android)ITypeMapWithAliasing(new) — interface abstracting type map lookups with alias resolutionSingleUniverseTypeMap— wraps oneIReadOnlyDictionarypair, handles alias holders within a single universeAggregateTypeMap— wraps NSingleUniverseTypeMapinstances, flattens results across universes (Debug only)TrimmableTypeMap— twoInitializeoverloads accepting BCL types:InvalidOperationException)JNIEnvInit— removed oldTrimmableTypeMap.Initialize()call (now done via startup hook)Generator (
Microsoft.Android.Sdk.TrimmableTypeMap)TypeMapAssemblyEmitter— emits per-assembly__TypeMapAnchortype (Debug) or referencesJava.Lang.Objectas shared anchor (Release)RootTypeMapAssemblyGenerator— complete rewrite: emitsStartupHook.Initialize()IL that constructs the appropriate type map and callsTrimmableTypeMap.Initialize():InitializewithJava.Lang.ObjectanchorInitializewith per-assembly__TypeMapAnchortypes[assembly: IgnoresAccessChecksTo]for Mono.Android + per-assembly DLLsMSBuild
StartupHookSupport, addsDOTNET_STARTUP_HOOKSenv varTests
IgnoresAccessChecksToattributes)Key design decisions
useSharedTypemapUniverseflag controls the mode throughout the pipelineJava.Lang.Objectas the anchor type (all per-assembly DLLs share the same universe)__TypeMapAnchorper DLL (isolated dictionaries)Initializeoverloads kept for trimming benefitsDOTNET_STARTUP_HOOKS) — runs beforeJNIEnvInitFixes #11180
Part of #10788