[TrimmableTypeMap] Replace startup hook with direct TypeMapLoader.Initialize() call#11197
Conversation
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>
6130f19 to
bb9ac8b
Compare
There was a problem hiding this comment.
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-timeMicrosoft.Android.Runtime.TypeMapLoader.Initialize()stub. - Update
Mono.Androidruntime initialization to callTypeMapLoader.Initialize()whenRuntimeFeature.TrimmableTypeMapis enabled, and remove MSBuild startup-hook wiring for trimmable typemaps. - Update typemap generator + tests to emit/expect
TypeMapLoaderrather thanStartupHook.
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. |
|
/review |
|
✅ Android PR Reviewer completed successfully! |
There was a problem hiding this comment.
✅ 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=allto prevent shipping - Feature switch isolation — the
TypeMapLoader.Initialize()call is in its ownif (RuntimeFeature.TrimmableTypeMap)block, consistent with the trimmer pattern used elsewhere in this file - Startup hooks remain available for HotReload —
StartupHookSupport=falseonly applies inOptimize=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 —
_ConfigureTrimmableTypeMapStartupHooktarget andStartupHookSupport=trueare 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
- 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>

Summary
Fixes #11196
Replace the
DOTNET_STARTUP_HOOKSmechanism for trimmable typemap initialization with a direct call fromJNIEnvInit.Initialize()toTypeMapLoader.Initialize().Problem
Startup hooks don't work with NativeAOT (
Assembly.Loadand reflection are not supported). The current approach also adds unnecessary latency via reflection-based discovery ofSystem.StartupHookProvider.Approach
Introduce a reference assembly (
_Microsoft.Android.TypeMaps.dll) with a publicTypeMapLoader.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.No reflection, no startup hook discovery, works with NativeAOT.
Changes
src/Microsoft.Android.TypeMaps.Ref/) — compile-only stub withpublic static class TypeMapLoader, usingProduceOnlyReferenceAssembly=true(unsigned, to match the generated assembly)ProjectReferenceto ref assembly (Private=false,PrivateAssets=all); directTypeMapLoader.Initialize()call fromJNIEnvInit.Initialize()behind theRuntimeFeature.TrimmableTypeMapfeature switchTrimmableTypeMap.Initialize()— made public (both overloads); the class itself remainsinternal(accessed via[IgnoresAccessChecksTo])TypeMapLoaderinMicrosoft.Android.Runtimenamespace instead of internalStartupHook_ConfigureTrimmableTypeMapStartupHooktarget andStartupHookSupport=trueoverride (startup hooks remain available for HotReload)TypeMapLoadertype name, namespace (Microsoft.Android.Runtime), and public visibility (414/414 pass)