[dotnet-linker] Mark the generated type map assemblies as trimmable. - #26351
Conversation
The trimmable-static registrar generates one type map assembly per assembly
containing Objective-C types. When PrepareAssemblies=true these assemblies are
written to disk before ILLink runs, and are then passed to ILLink as ordinary
input assemblies.
ILLink's default trim mode for our apps is 'partial', which means that only
assemblies marked with [assembly: AssemblyMetadata ("IsTrimmable", "True")] are
trimmed - every other assembly is copied as-is, and everything it references is
rooted.
The generated type map assemblies weren't marked as trimmable, so they were
copied as-is, which rooted every Objective-C type in the app - making it
impossible to trim anything at all.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6aeeddfd-3bd8-4129-ad90-7292860c4b66
There was a problem hiding this comment.
Pull request overview
Improves trimming behavior for the trimmable-static registrar’s generated “type map” assemblies so they don’t block ILLink trimming when PrepareAssemblies=true (where those assemblies are generated on disk before ILLink runs).
Changes:
- Add
AssemblyMetadata("IsTrimmable","True")to generated per-assembly type map assemblies. - Extend
AppBundleRewriterwith Cecil references forAssemblyMetadataAttributeand its(string,string)constructor.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| tools/dotnet-linker/Steps/TrimmableRegistrarStep.cs | Adds logic to stamp generated _* .TypeMap.dll assemblies as trimmable so ILLink will trim them in TrimMode=partial. |
| tools/dotnet-linker/AppBundleRewriter.cs | Adds type/method reference helpers for System.Reflection.AssemblyMetadataAttribute to support emitting the new metadata. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
When we create a separate root type map assembly (.NET 11+), we already force it to AssemblyAction.Link when we're running as a custom linker step. Do the same when we generate it before ILLink runs (PrepareAssemblies=true) by marking it as trimmable, so that the two code paths behave the same way. Verified that ILLink keeps all the TypeMapAssemblyTarget attributes when linking the root type map assembly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6aeeddfd-3bd8-4129-ad90-7292860c4b66
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…vive trimming. When the app assembly's linker action is 'Copy' (which is the case for MtouchLink=SdkOnly), ILLink's RootAssemblyInput never calls Annotations.SetEntryPointAssembly. As a result ILLink's TypeMapHandler has no starting point, records nothing, and every TypeMapAttribute / TypeMapAssociationAttribute in the generated type map assemblies is swept away. This didn't matter until the generated type map assemblies were marked as IsTrimmable (a832d80), because before that they were copied as-is and their attributes were preserved. Afterwards the app would crash at startup with: ObjCRuntime.RuntimeException: Unable to find the managed function with id -1 Fix this by adding a custom ILLink step that runs before MarkStep and sets the entry-point assembly annotation if ILLink didn't already do it. .NET 11+ is not affected: we pass --typemap-entry-assembly there, and .NET 11's MarkStep prefers that over the entry-point assembly annotation. Also fix an unrelated pre-existing bug in tests/common/shared-dotnet.mk: the recursive 'make delete-saved-state' at the end of 'run-bare' inherited the exported RUNTIMEIDENTIFIER/RUNTIMEIDENTIFIERS variables and thus tripped the guard against setting them, making the whole make invocation fail. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f879a89-7c49-4886-93e9-e43d848c5846
The step is only needed on .NET 10, so: * Condition the item group in Xamarin.Shared.Sdk.targets on the target framework version being less than 11.0. * Surround the step itself with '#if !NET11_0_OR_GREATER', so that it isn't even compiled on .NET 11+. Also revert the change to tests/common/shared-dotnet.mk, that's fixed in a separate pull request. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3f879a89-7c49-4886-93e9-e43d848c5846
✅ API diff for current PR / commitNET (empty diffs)✅ API diff vs stableNET (empty diffs)ℹ️ Generator diffGenerator Diff: vsdrops (html) vsdrops (raw diff) gist (raw diff) - Please review changes) Pipeline on Agent |
🚀 [CI Build #768c1fd] 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 macOS tests✅ Tests on macOS Monterey (12): All 5 tests passed. Html Report (VSDrops) Download Linux Build VerificationPipeline on Agent |
The trimmable-static registrar generates one type map assembly per assembly that
contains Objective-C types (
_<AssemblyName>.TypeMap.dll). WhenPrepareAssemblies=truethese assemblies are generated before ILLink runs, arewritten to disk, and are then handed to ILLink as ordinary input assemblies.
ILLink's trim mode for our apps defaults to
partial, which means ILLink onlytrims assemblies that are marked with:
Every other assembly gets
AssemblyAction.Copy, which also roots everything theassembly references.
The generated type map assemblies weren't marked as trimmable, so ILLink copied
them verbatim - and since they reference every Objective-C type in the app,
nothing could be trimmed. The platform assembly in particular survived almost
untouched, which then cascaded into the ReadyToRun image, the static registrar's
native code, and a pile of extra BCL assemblies.
This is fixed by adding the
IsTrimmableassembly metadata to the generated typemap assemblies.
Results
iOS, Release,
ios-arm64, CoreCLR + ReadyToRun, default link mode,PrepareAssemblies=true+PostProcessAssemblies=true(tests/dotnet/SizeTestApp):managed-static(baseline)trimmable-static(before)trimmable-static(after)The biggest individual changes (before → after):
_Microsoft.iOS.TypeMap.dllMicrosoft.iOS.dll(linked)The remaining ~1.6% difference versus
managed-staticis inherent to thetrimmable-static design (the registrar runs before trimming, so it roots the
exported members of every type it keeps).
Testing
AppSizeTestpasses onmainwith no expected-size changes at all: on.NET 10 the type map assemblies are either generated inside ILLink (where
Annotations.SetActionalready applies) or ILLink is skipped entirely(NativeAOT), so this change is a no-op there. It only takes effect on .NET 11+.
AppSizeTestpasses on thenet11.0branch (23/23).monotouch-testwas run on macOS withRegistrar=trimmable-static PrepareAssemblies=true PostProcessAssemblies=true UseMonoRuntime=false MtouchLink=SdkOnlyboth with and without this change: the failures are identical, so they're
pre-existing (generic
NSObjectsubclasses on CoreCLR + trimmable-static) andnot caused by this change.
🤖 Pull request created by Copilot