Uh oh!
There was an error while loading. Please reload this page.
[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. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…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>
simonrozsival
commented
Apr 22, 2026
/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
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- 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>
simonrozsival
commented
Apr 22, 2026
| <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
<RuntimeEnvironmentVariableInclude="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
Uh oh!
There was an error while loading. Please reload this page.

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
Related issues