Skip to content

[TrimmableTypeMap] Replace startup hook with direct TypeMapLoader.Initialize() call#11197

Merged
jonathanpeppers merged 4 commits intomainfrom
dev/simonrozsival/typemap-entry-point
Apr 24, 2026
Merged

[TrimmableTypeMap] Replace startup hook with direct TypeMapLoader.Initialize() call#11197
jonathanpeppers merged 4 commits intomainfrom
dev/simonrozsival/typemap-entry-point

Conversation

@simonrozsival
Copy link
Copy Markdown
Member

@simonrozsival simonrozsival commented Apr 23, 2026

Summary

Fixes #11196

Replace the DOTNET_STARTUP_HOOKS mechanism for trimmable typemap initialization with a direct call from JNIEnvInit.Initialize() to TypeMapLoader.Initialize().

Problem

Startup hooks don't work with NativeAOT (Assembly.Load and reflection are not supported). The current approach also adds unnecessary latency via reflection-based discovery of System.StartupHookProvider.

Approach

Introduce a reference assembly (_Microsoft.Android.TypeMaps.dll) with a public TypeMapLoader.Initialize() stub that Mono.Android compiles against. At app build time, the trimmable typemap generator produces the real implementation assembly with the same identity, which replaces the ref assembly before trimming/linking.

// Before:
JNIEnvInit.Initialize()
  → RunStartupHooksIfNeeded()
    → reflection to find System.StartupHookProvider
      → StartupHook.Initialize()  [generated _Microsoft.Android.TypeMaps.dll]
        → TrimmableTypeMap.Initialize(typeMap, proxyMap)

// After:
JNIEnvInit.Initialize()
  → TypeMapLoader.Initialize()  [generated _Microsoft.Android.TypeMaps.dll]
    → TrimmableTypeMap.Initialize(typeMap, proxyMap)

No reflection, no startup hook discovery, works with NativeAOT.

Changes

  • New ref assembly project (src/Microsoft.Android.TypeMaps.Ref/) — compile-only stub with public static class TypeMapLoader, using ProduceOnlyReferenceAssembly=true (unsigned, to match the generated assembly)
  • Mono.Android — compile-only ProjectReference to ref assembly (Private=false, PrivateAssets=all); direct TypeMapLoader.Initialize() call from JNIEnvInit.Initialize() behind the RuntimeFeature.TrimmableTypeMap feature switch
  • TrimmableTypeMap.Initialize() — made public (both overloads); the class itself remains internal (accessed via [IgnoresAccessChecksTo])
  • Generator — emits public TypeMapLoader in Microsoft.Android.Runtime namespace instead of internal StartupHook
  • MSBuild targets — removed _ConfigureTrimmableTypeMapStartupHook target and StartupHookSupport=true override (startup hooks remain available for HotReload)
  • Tests — updated generator test to verify TypeMapLoader type name, namespace (Microsoft.Android.Runtime), and public visibility (414/414 pass)

Replace the DOTNET_STARTUP_HOOKS mechanism for trimmable typemap initialization
with a direct call from JNIEnvInit.Initialize() to TypeMapLoader.Initialize().

This fixes NativeAOT compatibility (startup hooks don't work with NativeAOT)
and removes the reflection-based startup hook discovery overhead.

Changes:
- Add ref assembly project (src/Microsoft.Android.TypeMaps.Ref/) that Mono.Android
  compiles against. At app build time, the generated impl assembly replaces it.
- Call TypeMapLoader.Initialize() directly from JNIEnvInit when TrimmableTypeMap
  feature is enabled
- Make TrimmableTypeMap.Initialize() public (both overloads)
- Update generator to emit public TypeMapLoader class (in Microsoft.Android.Runtime
  namespace) instead of internal StartupHook
- Remove _ConfigureTrimmableTypeMapStartupHook target and StartupHookSupport=true
  from trimmable typemap targets (startup hooks remain for HotReload)

Fixes: #11196

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival force-pushed the dev/simonrozsival/typemap-entry-point branch from 6130f19 to bb9ac8b Compare April 23, 2026 15:43
@simonrozsival simonrozsival added copilot `copilot-cli` or other AIs were used to author this trimmable-type-map labels Apr 23, 2026
@simonrozsival simonrozsival changed the title Replace startup hook with direct TypeMapLoader.Initialize() call [TrimmableTypeMap] Replace startup hook with direct TypeMapLoader.Initialize() call Apr 23, 2026
@simonrozsival simonrozsival marked this pull request as ready for review April 23, 2026 15:57
Copilot AI review requested due to automatic review settings April 23, 2026 15:57
@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 23, 2026
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

This PR removes the trimmable typemap initialization dependency on DOTNET_STARTUP_HOOKS (reflection-based and incompatible with NativeAOT) and replaces it with a direct JNIEnvInit.Initialize()TypeMapLoader.Initialize() call, with the typemap generator emitting the real TypeMapLoader implementation at app build time.

Changes:

  • Add a new reference-assembly project (_Microsoft.Android.TypeMaps) that provides a compile-time Microsoft.Android.Runtime.TypeMapLoader.Initialize() stub.
  • Update Mono.Android runtime initialization to call TypeMapLoader.Initialize() when RuntimeFeature.TrimmableTypeMap is enabled, and remove MSBuild startup-hook wiring for trimmable typemaps.
  • Update typemap generator + tests to emit/expect TypeMapLoader rather than StartupHook.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs Updates generator test expectation from StartupHook to TypeMapLoader.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets Removes StartupHookSupport and the target that composed DOTNET_STARTUP_HOOKS for typemap initialization.
src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.HotReload.targets Updates comments to no longer mention trimmable typemap as a startup-hook contributor.
src/Mono.Android/Mono.Android.csproj Adds compile-time project reference to the new _Microsoft.Android.TypeMaps ref assembly.
src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs Makes TrimmableTypeMap.Initialize overloads public and updates XML docs to reference TypeMapLoader.
src/Mono.Android/Android.Runtime/JNIEnvInit.cs Calls TypeMapLoader.Initialize() when trimmable typemap feature is enabled.
src/Microsoft.Android.TypeMaps.Ref/TypeMapLoader.cs New TypeMapLoader stub type for compile-time reference.
src/Microsoft.Android.TypeMaps.Ref/Microsoft.Android.TypeMaps.Ref.csproj New project producing the _Microsoft.Android.TypeMaps reference assembly.
src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs Generator now emits public Microsoft.Android.Runtime.TypeMapLoader.Initialize() entrypoint.

Comment thread src/Mono.Android/Mono.Android.csproj
@jonathanpeppers
Copy link
Copy Markdown
Member

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 23, 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 architectural improvement

Summary: Replaces the reflection-based DOTNET_STARTUP_HOOKS mechanism with a direct TypeMapLoader.Initialize() call for trimmable typemap initialization. This eliminates reflection, removes startup hook latency, and — most importantly — enables NativeAOT support where Assembly.Load and reflection aren't available.

What works well

  • Ref assembly pattern is correctly implemented — ProduceOnlyReferenceAssembly=true, unsigned to match the generated impl, Private=false + PrivateAssets=all to prevent shipping
  • Feature switch isolation — the TypeMapLoader.Initialize() call is in its own if (RuntimeFeature.TrimmableTypeMap) block, consistent with the trimmer pattern used elsewhere in this file
  • Startup hooks remain available for HotReloadStartupHookSupport=false only applies in Optimize=true (Release) builds; Debug builds retain the default, so HotReload is unaffected
  • Generated IL changes are minimal and correct — TypeAttributes.Public, proper namespace, matching method signature
  • Target cleanup is thorough — _ConfigureTrimmableTypeMapStartupHook target and StartupHookSupport=true are cleanly removed with no orphaned references

Suggestions (2 × 💡)

# Severity File Issue
1 💡 RootTypeMapAssemblyGeneratorTests.cs Verify TypeMapLoader's namespace and public visibility in the assertion
2 💡 RootTypeMapAssemblyGenerator.cs Comment drops TrimmableTypeMap from IgnoresAccessChecksTo list, but the class is still internal

Both are minor and non-blocking.

Generated by Android PR Reviewer for issue #11197 · ● 4.1M

simonrozsival and others added 3 commits April 23, 2026 22:18
- Add namespace and visibility assertions to TypeMapLoader test
- Fix IgnoresAccessChecksTo comment to include TrimmableTypeMap (class is still internal)
- Clarify ProduceOnlyReferenceAssembly intent in ref assembly csproj

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prevents the JIT from trying to resolve TypeMapLoader (from
_Microsoft.Android.TypeMaps.dll) when compiling JNIEnvInit.Initialize()
in non-trimmable builds where that assembly isn't present.

This is the standard .NET pattern for conditional dependencies on
assemblies that may not be available at runtime. Per docs:
"runtimes will refuse to load reference assemblies for execution"
so shipping the ref assembly as a fallback wouldn't work either.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The generated TypeMapLoader.Initialize() calls TrimmableTypeMap.Initialize()
which is now public. Making the class public means IgnoresAccessChecksTo is
no longer needed for this specific type (still needed for SingleUniverseTypeMap,
AggregateTypeMap, and __TypeMapAnchor).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival
Copy link
Copy Markdown
Member Author

CI failures are unrelated
image

@jonathanpeppers jonathanpeppers merged commit 22c42db into main Apr 24, 2026
2 of 3 checks passed
@jonathanpeppers jonathanpeppers deleted the dev/simonrozsival/typemap-entry-point branch April 24, 2026 15:30
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] Revisit usage of startup hooks to initialize the type map

3 participants