Skip to content

[dotnet] Default to the trimmable-static registrar for NativeAOT on .NET 11 - #26346

Merged
rolfbjarne merged 9 commits into
mainfrom
dev/rolf/trimmable-static-default
Aug 4, 2026
Merged

rolfbjarne merged 9 commits into
mainfrom
dev/rolf/trimmable-static-default

Conversation

@rolfbjarne

@rolfbjarne rolfbjarne commented Jul 29, 2026 •

Copy link
Copy Markdown
Member

Stacked on top of #26345 β€” review that one first.

On .NET 11+, default Registrar to trimmable-static (instead of managed-static) for NativeAOT, and enable the PrepareAssemblies / PostProcessAssemblies flow by default for NativeAOT.

All three are set at evaluation time (in Xamarin.Shared.Sdk.props) rather than in the SelectRegistrar target, because the static _SkipILLink / _PostprocessAssembliesAfterIlc gates read them at evaluation time. PostProcessAssemblies normally defaults to $(PrepareAssemblies) in Xamarin.Shared.targets, but that default runs later than the gate reads it, so the same default is applied here too.

This is a no-op on .NET 10 β€” every condition requires TargetFrameworkVersion >= 11.0. Verified: all AppSizeTest NativeAOT and NativeAOT_TrimmableStatic expected sizes are unchanged on main (within Β±5 bytes), so no expected files needed updating.

Why NativeAOT only?

⚠️ This deliberately does not change the default for CoreCLR. The measurements don't support it β€” iOS, Release, ios-arm64, measured on the net11.0 branch:

Configuration App bundle size
CoreCLR + ReadyToRun, managed-static (current default) 11,069,079 B
CoreCLR + ReadyToRun, trimmable-static 31,622,327 B
CoreCLR + ReadyToRun, managed-static, MtouchLink=Full 11,263,711 B
CoreCLR + ReadyToRun, trimmable-static + PrepareAssemblies + MtouchLink=Full 11,304,991 B

With the default trim mode (SDK-only) trimmable-static is ~3x larger, and even in the best case (full linking + PrepareAssemblies, which is not the default) it's still 41 KB larger than managed-static in the same trim mode. macOS CoreCLR-Interpreter shows the same pattern (246.9 MB β†’ 255.9 MB).

The reason is that the size win of trimmable-static comes from the post-ILC registrar reorder, which lets us drop the native code for the UnmanagedCallersOnly trampolines that ILC trimmed away. There is no equivalent on CoreCLR/Mono, so the whole registered NSObject surface is kept.

Changing the CoreCLR default should be revisited once the registered surface can actually be trimmed there.

πŸ€– Pull request created by Copilot

rolfbjarne and others added 5 commits July 29, 2026 15:39
…-static type maps

The types named by our type-map entries only show up in the custom attribute
blobs, as assembly-qualified names. Nothing else in the generated type map
assembly refers to them from IL, so Cecil doesn't emit a TypeRef row for them.

That's valid metadata: ECMA-335 II.23.3 only requires a System.Type custom
attribute argument to be stored as a SerString holding the type's canonical name,
and neither the CustomAttribute (II.22.10) nor the TypeRef (II.22.38) validity
rules require a matching TypeRef row. The runtime agrees - it resolves these
types by parsing the string, not by looking at the TypeRef table - and the type
map design explicitly says the assembly name in TypeMapAssemblyTargetAttribute
doesn't need a matching AssemblyRef row either.

crossgen2 nonetheless assumes the TypeRef row is there. It fails all four lookups
in ModuleTokenResolver.GetModuleTokenForType (the type doesn't version with the
compilation, there's no TypeRef in the input, and nothing has added it to the
manifest module), and falls through to a bare
`throw new NotImplementedException (type.ToString ())`, so ReadyToRun-compiling
an app with the trimmable-static registrar fails:

    System.NotImplementedException: [Microsoft.macOS]UserNotificationsUI.IUNNotificationContentExtension
       at ILCompiler.DependencyAnalysis.ReadyToRun.ModuleTokenResolver.GetModuleTokenForType(TypeDesc, Boolean, Boolean)
       at ILCompiler.DependencyAnalysis.ReadyToRun.SignatureContext.GetTargetModule(TypeDesc)
       at ILCompiler.DependencyAnalysis.ReadyToRun.TypeFixupSignature.GetData(NodeFactory, Boolean)
       at ILCompiler.DependencyAnalysis.ReadyToRun.ImportSectionNode.MaterializeSignature(NodeFactory)
       at ILCompiler.DependencyAnalysis.ReadyToRun.ManifestMetadataTableNode.ComputeLastSetOfModuleIndices()
       at ILCompiler.DependencyAnalysis.ReadyToRun.ManifestAssemblyMvidHeaderNode.GetData(NodeFactory, Boolean)

Work around it by adding an unused method to each generated type map assembly
that does an `ldtoken` for every externally defined type the maps name. That's
enough for Cecil to emit the TypeRef rows, and crossgen2 then resolves the types
through `TryGetModuleTokenForExternalType`. `ldtoken` is used rather than a field
or parameter because it also works for open generic types. The method is never
called, so it's trimmed away again by ILLink.

Only CoreCLR needs this, so the other runtimes don't pay for it. In particular
NativeAOT app size is unaffected (bit-for-bit identical). For CoreCLR it adds
~64 KB per runtime identifier to the platform type map assembly.

Reported upstream as dotnet/runtime#131527, and tracked
for removal in #26343.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6aeeddfd-3bd8-4129-ad90-7292860c4b66
The workaround emits TypeRef rows for every type map entry into the generated
type map assemblies when using CoreCLR, which makes those assemblies (and thus
the app) slightly bigger.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6aeeddfd-3bd8-4129-ad90-7292860c4b66
The previous commit was generated on a local machine whose build produces a
slightly different Info.plist / Microsoft.macOS.dll / executable than CI does
(a pre-existing ~5.9 KB difference that also shows up in the unmodified
CoreCLR_Interpreter test). Keep only the delta actually caused by this change:
+64,000 bytes per RID in the generated type map assembly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6aeeddfd-3bd8-4129-ad90-7292860c4b66
…NET 11.

On .NET 11+, default the registrar to 'trimmable-static' (instead of
'managed-static') for NativeAOT, and enable the PrepareAssemblies flow by default
for NativeAOT. Both are set at evaluation time (in Xamarin.Shared.Sdk.props) so the
static _SkipILLink / _PostprocessAssembliesAfterIlc gates see them; PostProcessAssemblies
gets the same $(PrepareAssemblies) default here too because that gate reads it earlier
than the default in Xamarin.Shared.targets runs.

Scoped to NativeAOT on purpose: the trimmable-static registrar only produces a smaller
app under NativeAOT (where the TypeMap feature and the post-ILC registrar reorder let us
trim the registered binding surface). On Mono/CoreCLR it keeps the whole registered
NSObject surface and makes apps 2-3x larger, so those runtimes keep managed-static.

This is a no-op on .NET 10 (the condition requires TargetFrameworkVersion >= 11.0).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6aeeddfd-3bd8-4129-ad90-7292860c4b66
The same note is added by "[dotnet] Fix latent trimmable-static registrar bugs on
non-NativeAOT runtimes", so adding it here too would just cause a merge conflict.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6aeeddfd-3bd8-4129-ad90-7292860c4b66
rolfbjarne and others added 2 commits July 29, 2026 16:27
Two different assemblies can contain types with the same full name, in which case
deduplicating on the full name alone would skip a TypeRef we need.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6aeeddfd-3bd8-4129-ad90-7292860c4b66
@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@rolfbjarne
rolfbjarne marked this pull request as ready for review July 29, 2026 14:47
Copilot AI review requested due to automatic review settings July 29, 2026 14:47
@vs-mobiletools-engineering-service2

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 updates the .NET SDK MSBuild defaults so that, on .NET 11+ NativeAOT publishes, the build uses the trimmable-static registrar and enables the PrepareAssemblies/PostProcessAssemblies pipeline early (at evaluation time) to satisfy existing static gating logic in the SDK targets.

Changes:

  • Default $(Registrar) to trimmable-static for NativeAOT publishes on .NET 11+.
  • Default $(PrepareAssemblies) to true for NativeAOT publishes on .NET 11+.
  • Default $(PostProcessAssemblies) to $(PrepareAssemblies) at evaluation time for NativeAOT publishes on .NET 11+ (so evaluation-time gates observe the intended value).

Comment thread dotnet/targets/Xamarin.Shared.Sdk.props Resolved
@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

Base automatically changed from dev/rolf/crossgen2-typemap-workaround to main July 30, 2026 14:24
@rolfbjarne
rolfbjarne requested a review from dalexsoto as a code owner July 30, 2026 14:24
@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@rolfbjarne
rolfbjarne enabled auto-merge (squash) July 31, 2026 13:56
@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

Copy link
Copy Markdown
Collaborator

βœ… API diff for current PR / commit

NET (empty diffs)

βœ… API diff vs stable

NET (empty diffs)

ℹ️ Generator diff

Generator Diff: vsdrops (html) vsdrops (raw diff) gist (raw diff) - Please review changes)

Pipeline on Agent
Hash: c38dc24d89c9fedd2d7600eb6f185c1f3c4916e8 [PR build]

@vs-mobiletools-engineering-service2

This comment has been minimized.

rolfbjarne added a commit that referenced this pull request Aug 4, 2026
…runtimes (#26344)

Two latent bugs that only show up when the trimmable-static registrar is used on a runtime other than NativeAOT (they were found while measuring app size with `Registrar=trimmable-static` on CoreCLR and Mono).

## 1. `ObjCRuntime.TrimProtocolMemberAttributes` was set too early, and unconditionally

The feature switch that lets the trimmer remove `[ProtocolMember]` attributes was set in `_ComputeLinkerInputs`, gated only on `Registrar == trimmable-static And PrepareAssemblies == true`.

The dynamic registrar reads those attributes reflectively at runtime, so when the dynamic registrar is still present the trimmer errors out with **IL2045**. Whether the dynamic registrar is present is only known after `_SetDynamicRegistrationSupportedFeature` has run, so the item is moved into that target and gated on `_DynamicRegistrationSupported == false` instead.

## 2. `ILStrip` failed on the generated TypeMap assemblies

Some `ResolvedFileToPublish` items don't have `OriginalRelativePath` metadata β€” the TypeMap assemblies the trimmable-static registrar generates are one such case. The `OutputPath` expression then collapsed to the bare stripped directory, and `ILStrip` failed with `Access to the path '.../stripped/' is denied`. Fall back to the file name when `OriginalRelativePath` is empty.

The second commit fixes the batching of that fallback: the condition referenced `%(ResolvedFileToPublish.OriginalRelativePath)` while modifying `@(_AssembliesToBeStripped)`, which makes MSBuild batch over the wrong item type β€” a single publish item without `OriginalRelativePath` would have applied the fallback `OutputPath` to *every* assembly.

## Notes

* The comment added to `SelectRegistrar` mentions a .NET 11 NativeAOT `trimmable-static` default that is introduced by a separate PR (#26346); the first commit was kept verbatim as it was validated on the `net11.0` branch.
* No behavior change for any default configuration: `trimmable-static` isn't the default for any runtime today.

πŸ€– Pull request created by Copilot

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@vs-mobiletools-engineering-service2

Copy link
Copy Markdown
Collaborator

πŸš€ [CI Build #c38dc24] Test results πŸš€

Test results

βœ… All tests passed on VSTS: test results.

πŸŽ‰ All 203 tests passed πŸŽ‰

Tests counts

βœ… assembly-processing: All 1 tests passed. Html Report (VSDrops) Download
βœ… cecil: All 1 tests passed. Html Report (VSDrops) Download
βœ… dotnettests (iOS): All 1 tests passed. Html Report (VSDrops) Download
βœ… dotnettests (MacCatalyst): All 1 tests passed. Html Report (VSDrops) Download
βœ… dotnettests (macOS): All 1 tests passed. Html Report (VSDrops) Download
βœ… dotnettests (Multiple platforms): All 1 tests passed. Html Report (VSDrops) Download
βœ… dotnettests (tvOS): All 1 tests passed. Html Report (VSDrops) Download
βœ… framework: All 2 tests passed. Html Report (VSDrops) Download
βœ… fsharp: All 4 tests passed. Html Report (VSDrops) Download
βœ… generator: All 5 tests passed. Html Report (VSDrops) Download
βœ… interdependent-binding-projects: All 4 tests passed. Html Report (VSDrops) Download
βœ… introspection: All 4 tests passed. Html Report (VSDrops) Download
βœ… linker (iOS): All 15 tests passed. Html Report (VSDrops) Download
βœ… linker (MacCatalyst): All 15 tests passed. Html Report (VSDrops) Download
βœ… linker (macOS): All 21 tests passed. Html Report (VSDrops) Download
βœ… linker (tvOS): All 15 tests passed. Html Report (VSDrops) Download
βœ… monotouch (iOS): All 19 tests passed. [attempt 2] Html Report (VSDrops) Download
βœ… monotouch (MacCatalyst): All 18 tests passed. Html Report (VSDrops) Download
βœ… monotouch (macOS): All 19 tests passed. Html Report (VSDrops) Download
βœ… monotouch (tvOS): All 19 tests passed. Html Report (VSDrops) Download
βœ… msbuild: All 2 tests passed. Html Report (VSDrops) Download
βœ… sharpie: All 1 tests passed. Html Report (VSDrops) Download
βœ… windows: All 3 tests passed. Html Report (VSDrops) Download
βœ… xcframework: All 4 tests passed. Html Report (VSDrops) Download
βœ… xtro: All 1 tests passed. Html Report (VSDrops) Download

macOS tests

βœ… Tests on macOS Monterey (12): All 5 tests passed. Html Report (VSDrops) Download
βœ… Tests on macOS Ventura (13): All 5 tests passed. Html Report (VSDrops) Download
βœ… Tests on macOS Sonoma (14): All 5 tests passed. Html Report (VSDrops) Download
βœ… Tests on macOS Sequoia (15): All 5 tests passed. Html Report (VSDrops) Download
βœ… Tests on macOS Tahoe (26): All 5 tests passed. Html Report (VSDrops) Download

Linux Build Verification

βœ… Linux build succeeded

Pipeline on Agent
Hash: c38dc24d89c9fedd2d7600eb6f185c1f3c4916e8 [PR build]

@rolfbjarne rolfbjarne added the ready-to-review This PR is ready to review/merge. label Aug 4, 2026
@rolfbjarne
rolfbjarne merged commit aed6558 into main Aug 4, 2026
56 checks passed
@rolfbjarne
rolfbjarne deleted the dev/rolf/trimmable-static-default branch August 4, 2026 17:05
rolfbjarne added a commit that referenced this pull request Aug 5, 2026
…in .NET 11+. (#26409)

The trimmable-static registrar has been the default for NativeAOT on .NET 11+ since #26346. This makes it the default for **CoreCLR** as well, and enables the `PrepareAssemblies` flow along with it.

Mono keeps the managed-static registrar β€” only CoreCLR and NativeAOT were measured and tested.

## Why `PrepareAssemblies` has to come along

`PrepareAssemblies=true` is *required* for the trimmable-static registrar to produce a small app.

The assembly-preparer writes the generated type map assemblies to disk (marked with `[AssemblyMetadata ("IsTrimmable", "True")]`) *before* the trimmer runs, so the trimmer treats them as ordinary trimmable input assemblies. Without it, the type map assemblies are created from inside the trimmer, where they end up with the `copy` assembly action β€” which means they're never trimmed, and since they reference every Objective-C type in the app, nothing else can be trimmed either.

That's what made trimmable-static apps ~3x larger on CoreCLR, and it's why the old comment claimed the trimmable-static registrar couldn't be used outside of NativeAOT. That comment was simply wrong, and has been rewritten.

## Notes

- The `$(UseMonoRuntime)` defaults had to be **moved above** the registrar defaults, because the latter now check `$(UseMonoRuntime)` at evaluation time and would otherwise read an empty value. This also matches the layout on `net11.0`, which reduces the merge conflict when this flows.
- The `$(Registrar)` check on `PrepareAssemblies`/`PostProcessAssemblies` means a CoreCLR app that explicitly opts back in to the managed-static registrar doesn't get `PrepareAssemblies` enabled β€” that combination makes apps *larger*. NativeAOT keeps getting `PrepareAssemblies` regardless of the registrar (pre-existing behavior).
- **This is inert on `main`**, since `main` is still .NET 10. It takes effect when it flows into `net11.0`.

πŸ€– Pull request created by Copilot

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

copilot ready-to-review This PR is ready to review/merge.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants