Skip to content

[TrimmableTypeMap] Per-assembly typemap universes with startup hook initialization#11181

Merged
jonathanpeppers merged 13 commits intomainfrom
dev/simonrozsival/per-assembly-typemap-universes
Apr 22, 2026
Merged

[TrimmableTypeMap] Per-assembly typemap universes with startup hook initialization#11181
jonathanpeppers merged 13 commits intomainfrom
dev/simonrozsival/per-assembly-typemap-universes

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

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 resolution
    • SingleUniverseTypeMap — wraps one IReadOnlyDictionary pair, handles alias holders within a single universe
    • AggregateTypeMap — wraps N SingleUniverseTypeMap instances, flattens results across universes (Debug only)
  • TrimmableTypeMap — two Initialize overloads accepting BCL types:
    • Single dict pair (Release/shared universe)
    • Two parallel arrays (Debug/per-assembly universes)
    • Re-initialization guard (InvalidOperationException)
    • Length-0 validation on array overload
  • JNIEnvInit — removed old TrimmableTypeMap.Initialize() call (now done via startup hook)

Generator (Microsoft.Android.Sdk.TrimmableTypeMap)

  • TypeMapAssemblyEmitter — emits per-assembly __TypeMapAnchor type (Debug) or references Java.Lang.Object as shared anchor (Release)
  • RootTypeMapAssemblyGenerator — complete rewrite: emits StartupHook.Initialize() IL that constructs the appropriate type map and calls TrimmableTypeMap.Initialize():
    • Option A (shared universe): calls single-dict Initialize with Java.Lang.Object anchor
    • Option B (per-assembly): builds arrays and calls array Initialize with per-assembly __TypeMapAnchor types
    • Emits [assembly: IgnoresAccessChecksTo] for Mono.Android + per-assembly DLLs

MSBuild

  • Trimmable targets — force-enables StartupHookSupport, adds DOTNET_STARTUP_HOOKS env var
  • HotReload targets — composes startup hooks instead of clobbering

Tests

  • 5 new tests for both modes (valid PE, assembly refs, IgnoresAccessChecksTo attributes)
  • All 410 generator tests pass

Key design decisions

  • useSharedTypemapUniverse flag controls the mode throughout the pipeline
  • Shared universe uses Java.Lang.Object as the anchor type (all per-assembly DLLs share the same universe)
  • Per-assembly universe uses a generated __TypeMapAnchor per DLL (isolated dictionaries)
  • Two separate Initialize overloads kept for trimming benefits
  • Root assembly is a startup hook (DOTNET_STARTUP_HOOKS) — runs before JNIEnvInit

Fixes #11180
Part of #10788

simonrozsival and others added 7 commits April 22, 2026 12:04
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>
- 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>
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

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 update TrimmableTypeMap to initialize from startup hook–provided dictionaries.
  • Update the generator to emit per-assembly anchors (Debug) or use Java.Lang.Object as a shared anchor (Release) and generate startup-hook IL to call TrimmableTypeMap.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.

Comment thread src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs Outdated
simonrozsival and others added 2 commits April 22, 2026 14:02
…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
Copy link
Copy Markdown
Member Author

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 22, 2026

Android PR Reviewer completed successfully!

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

✅ 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 ITypeMapWithAliasing abstraction cleanly isolates alias resolution from the TrimmableTypeMap singleton
  • Startup hook composition in both Trimmable and HotReload targets correctly handles the capture→remove→compose pattern
  • The InvalidOperationException re-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 IgnoresAccessChecksTo attributes

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

Comment thread src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs Outdated
Comment thread src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs Outdated
simonrozsival and others added 2 commits April 22, 2026 14:33
- 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>
@simonrozsival simonrozsival added the copilot `copilot-cli` or other AIs were used to author this label Apr 22, 2026
@jonathanpeppers jonathanpeppers added ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). and removed copilot `copilot-cli` or other AIs were used to author this labels Apr 22, 2026
@simonrozsival simonrozsival added copilot `copilot-cli` or other AIs were used to author this and removed ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). labels Apr 22, 2026
simonrozsival and others added 2 commits April 22, 2026 18:11
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
Copy link
Copy Markdown
Member Author

CI failures unrelated:
image

@simonrozsival simonrozsival added the ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). label Apr 22, 2026
Comment on lines +48 to +54
<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)' == '' " />
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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:

<RuntimeEnvironmentVariable Remove="DOTNET_STARTUP_HOOKS" />
<RuntimeEnvironmentVariable Include="DOTNET_STARTUP_HOOKS" Value="$(_AndroidHotReloadAgentAssemblyName)" />

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I opened an issue #11186 and I'll let copilot look into it overnight

@jonathanpeppers jonathanpeppers merged commit 6240086 into main Apr 22, 2026
2 of 3 checks passed
@jonathanpeppers jonathanpeppers deleted the dev/simonrozsival/per-assembly-typemap-universes branch April 22, 2026 21:13
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 ready-to-review This PR is ready to review/merge, I think any CI failures are just flaky (ignorable). trimmable-type-map

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[TrimmableTypeMap] Per-assembly typemap universes for incremental Debug builds + merged Release builds

3 participants