Description
Goals
- Remove assembly loading from the trimmer process to enable ILLink to be published trimmed and AOT compiled
- Enable transition from Mono.Cecil to System.Reflection.Metadata
- Reduce API surface of ILLink
- Enable more code sharing between ILLink and ILCompiler
Non-Goals
- Remove custom steps entirely from Xamarin.iOS and Xamarin.Android
- Design a new model for trimming customization
- Remove the requirement for running ILLink with custom steps before Native AOT compilation. This would require reworking the custom steps to not depend on marking information or editing IL after trimming.
Current Custom Steps
Searching for the _TrimmerCustomSteps
msbuild property in github search, I found the following repositories that use custom steps:
- xamarin/xamarin-macios: 27 custom steps
- dotnet/android (formerly xamarin/xamarin-android): 14 custom steps
- emclient/MailClient.Linker: 1 custom step
A short summary of each step is below (please update or correct if inaccurate or missing info):
xamarin-macios
All steps inherit from ConfigurationAwareStep, ConfigurationAwareSubStep, or ConfigurationAwareMarkHandler, which provide a config, DerivedLinkContext object, and Application object.
Before MarkStep
- SetupStep: S
- Logs a config file to the console and creates the directories needed by the config
- Can be easily replaced with an msbuild task
- CollectAssembliesStep: S
- This would not be needed if LinkContext.GetAssemblies () was exposed
- Calls LinkContext.GetAssemblies () via reflection and caches the result
- CoreTypeMapStep: S
- Go through all the assemblies that may reference the framework and track which types are NSObject subclasses
- This is an optimization to cache results for IsNSObject and IsDirectBinding that isn't strictly necessary for correctness.
- ProcessExportedFields M
- Adds (property, FieldAttribute) pairs to the ExportedField CustomAnnotation dictionary to be used in ListExportedSymbols later.
- Could instead write the exports to a file?
- RegistrarRemovalTrackingStep M
- Determines if DynamicRegistrar is required and stubs ObjCRuntime.Runtime.get_DynamicRegistrationSupported to return false if not required by any assembly. Warns if it is required by any assembly.
- Sets the RemoveDynamicRegistrar Optimization in the config.
- DynamicRegistrationSupported should be made a FeatureSwitch and the feature value could be set when this step runs pre-link.
- PreMarkDispatcher
- CollectUnmarkedMembersSubstep L / Unknown
- This ideally should run between MarkStep and SweepStep when members are marked, but resolution still works.
- Creates a list of all types that have been linked away (All types since this runs before MarkStep)
- Creates a list of all protocol interfaces.
- Creates a map from each type to the interface types of all their unmarked interfaceImpls
- This info is used later in the static registrar
- How can we save this information for use in the static registrar without storing the metadata objects themselves? What's the minimum information required?
- StoreAttributesStep substep
- Base: AttributeIteratorBaseStep L / Unknown
- Processes Attributes: tracks some attributes for later inspection with StoreCustomAttribute
- System.Runtime.CompilerServices..ExtensionAttribute
- System.Runtime.Versioning..SupportedOSPlatformAttribute
- System.Runtime.Versioning..UnsupportedOSPlatformAttribute
- Foundation..ProtocolAttribute
- This will require figuring out each attribute's purpose and how to transfer the relevant data from pre-link to post-link steps
- CollectUnmarkedMembersSubstep L / Unknown
- ManagedRegistrarStep L
- See Managed Static Registrar Docs and ManagedRegistrarLookupTablesStep
- Everything is only done if the registrar mode is not ManagedStatic
- Looks through each assembly to find all methods that need an UnmanagedCallersOnlyAttribute, generates a wrapper for them, adds them to the assembly.
- Perhaps could be a generator? Looks like a similar job as LibraryImportGenerator and ComInterfaceGenerator. Probably a stretch goal though.
- Marks exported types on modified assemblies
- Unconditionally Marks the RegistrarHelper.Register method
- The ManagedStatic registrar could add a DynamicDependency on the UnmanagedCallerOnly wrapper to automatically mark the wrapper when the method is marked.
IMarkHandlers which run during Mark
- PreserveBlockCodeHandler S
- Preserves a method and a field in SDInnerBlock type if the type is marked
- Should be replaced by DynamicDependency
- OptimizeGeneratedCodeHandler L
- Implements dead code elimination and maybe some others, but only if there's a CompilerGeneratedCodeAttribute or BindingImplAttribute
- It also does some extra optimizations specialized for mac-ios.
- This step modifies IL during MarkStep. We should investigate if the linker keeps things that this step removes and/or if this step adds dependencies that the linker isn't aware of.
- BackingFieldDelayHandler L / Unknown
- For Dispose methods of NSObjects, stub the body and save to replace for later so that the linker doesn't mark the backing fields that aren't otherwise used. Later the Dispose method is restored to its original body (but without otherwise unused fields).
- Modifies the method during MarkStep, which could lead to issues.
- Could this be a feature added to the linker itself? Or could a write-only field have side effects even if it's never read?
- There's not an obvious way to save the method body for reapplying later. This would require more than translating to a pre- or post-linker step.
- MarkIProtocolHandler S
- For all marked NSObject subclasses, mark all the interfaces that have the ProtocolAttribute
- We could inject DynamicDependency for the protocols in a pre-linker step.
MarkSubStepDispatcher steps: Only need to run on marked assemblies
- MarkDispatcher
- ApplyPreserveAtribute S
- Base: ApplyPreserveAttributeBase
- Mobile Variant: MobileApplyPreserveAttribute
- Apply preserve / mark all types with PreserveAttribute
- MarkNSObjects S
- Preserve all on any NSObject subclass
- ApplyPreserveAtribute S
- PreserveSmartEnumConversionsHandler S
- Looks like it finds the EnumNameExtensions GetConstant and GetValue methods and preserves them if they are found in a method param, return, or property
- Could be replaced with a DynamicDependency
Pre-sweep custom steps
- ManagedRegistrarLookupTablesStep M
- This type will generate and inject a new type to the lookup code to:
- Convert between types and their type IDs.
- Map between a protocol interface and its wrapper type.
- Find the UnmanagedCallersOnly method for a given method ID.
- Also needs to inject a call to the class .cctor in the module ctor
- Info for each exported method is stored in "ManagedRegistrarStep" CustomAnnotation for the AssemblyDefinition from ManagedRegisrarStep.
- If the name of the UCO method is deterministic and a dynamicDependency is added to keep the UCO, we can lookup the wrapper from the exported method rather than keeping the info in an object.
- This type will generate and inject a new type to the lookup code to:
Post-sweep custom steps
- PostSweepDispatcher
- RemoveAttributesStep S
- Base: AttributeIteratorBaseStep
- Remove ObjCRuntime.NativeNameAttribute, ObjCRuntime.AdoptsAttribute (if RegisterProtocolsOptimization == true), Foundation.ProcolAttribute, and Foundation.ProcolMemberAttribute (if RegisterProtocolsOptimization==true)
- RemoveAttributesStep S
Pre-output custom steps
-
LoadNonSkippedAssembliesStep S
- saves some info (name, path, isDedupAssembly) about the assemblies that aren't skipped or deleted in the config (all the assemblies that are outputted)
-
- Saves some MSBuild items to the config
-
- The ListExportedSymbols must run after ExtractBindingLibrariesStep, otherwise we won't properly list exported Objective-C classes from binding libraries
- Accumulates a list of exports
- Generates native code for pinvokes
-
- RemoveUserResourcesSubStep S
- Removes some resource files from non-sdk and non-framework assemblies
- BackingFieldReintroductionSubStep L / Unknown
- Re-add references to backing fields in Dispose that were removed in BackingFieldDelayHandler
- RemoveUserResourcesSubStep S
-
- Makes edits to the static registrar to rewrite the usage of class_ptr in NSObject subclasses and potentially remove static initializers.
- Don't think it edits IL
Post-output steps
- RegistrarStep M
- Finalizes the registrar with the final trim data
- Generates C or Objective C? code
- GenerateMainStep S
- Generates a main.mm file for each abi in the app and outputs some msbuild items
- GenerateReferencesStep S
- Creates MSBuild items for a reference.m file?
- GatherFrameworksStep S
- Creates MSBuild items for the framework references
- ComputeNativeBuildFlagsStep S
- Creates some MSBuild items for the native build flags
- ComputeAOTArguments S
- Generates MSBuild items related to calling AOT tools
- DoneStep
- Flushes the MSBuild items into a .items file to be loaded by msbuild
android
Before MarkStep
- SetupStep
- Saves XATargetFrameworkDirectories to a config object
During MarkStep
- PreserveSubStepDispatcher
- ApplyPreserveAttribute substep
- Preserves members with the PreserveAttribute
- PreserveExportedTypes substep
- Mark members with JavaExport attributes
- This runs on marked types but marks them again if they have the attribute, redundant?
- ApplyPreserveAttribute substep
- MarkJavaObjects
- Registers a MarkTypeAction and MarkAssemblyAction
- AssemblyAction: finds IJavaObjects that are CustomViews or IJniNameProviderAttribute and marks them
- TypeAction: if the type implements IJavaObject, preserve the implementor / implementation?
- Looks like just marking items for each marked type
- Registers a MarkTypeAction and MarkAssemblyAction
- PreserveJavaExceptions
- If the type is a Java exception, mark the string ctor
- PreserveApplications
- Mark default ctor of type references in the ApplicationAttribute
- Make these properties DAM(DAMT.PublicParameterlessConstructor)
- PreserveRegistrations
- Find members in an attribute and mark them
- Marks all methods in type hierarchy with the name of the attribute
- DAM with name?
- PreserveJavaInterfaces
- Registers MarkTypeAction
- If the interface implements IJavaObject, mark all the methods
- Registers MarkTypeAction
- FixAbstractMethodsStep
- Creates implementations for abstract methods in Java interfaces that throw
- It seems like this step was made before .NET supported Default interface implementations. This step could try injecting default implementations on the interfaces instead of on the implementing types.
- Alternatively, maybe the tool that generates the java bindings could add default implementations? Then devs wouldn't get warnings if they only partially implement the interfaces, though. Maybe the ref assembly has abstract methods and the implementation assembly has default implementations?
After MarkStep
- GenerateProguardConfiguration
- Write some data about the linked assemblies and registered types to a file
- AddKeepAlivesStep
- Insert GC::KeepAlive calls for all nonstring and nonvaluetype parameters on methdos with a RegisterAttribute
After CleanStep
- StripEmbeddedLibraries
- Goes through assembly resources and removes some unnecessary ones
- RemoveResourcesDesignerStep
- Remove references to a Resouce class and replaces references to the fields to the resource id exactly
- GetAssembliesStep
- Saves the assemblies to a config object
- FixLegacyResourceDesignerStep
- Rewrites some field references to property references
emclient/MailClient.Linker
- Removes ObjCRuntime.ThrowHelper.ThrowArgumentNullException(string) from beginning of ReleaseBlockWhenDelegateIsCollected
Summary of Custom Step requirements and potential replacements
Edit Metadata
This can be done with separate tools that run before or after ILLink.
Unconditionally mark metadata
This could be replaced with a generated ILLink.Descriptors.xml.
Conditionally mark / create dynamic dependencies
Basic dependencies could be implemented with DynamicDependencyAttributes in a generated ILLink.LinkAttributes.xml. Dependencies that are conditional on more than one metadatum would not work with this system. This also will require that the depending item can have a CustomAttribute applied to it. This may not be an issue for the current set of custom steps.
Optimizations to avoid processing unmarked metadata
The primary use of IsMarked and MarkHandler steps is primarily to reduce the cost of processing unnecessary metadata. While adding DynamicDependency's could be used to achieve the same effect, this would likely require more processing time.
Save data for msbuild, and pass data between steps
Some steps must save information from before trimming to be used after trimming. Some info is required for platform linker or other subsequent build steps.
Some steps save references to metadata objects before MarkStep and then retrieve those objects after. This would not be possible if they are run in separate processes. In particular, CollectUnmarkedMembersSubstep
in xamarin-macios would cause issues.
Proposal
Custom steps could be mostly left as they are but run either entirely before or entirely after ILLink. A new custom step host and msbuild task would be created to load and run the custom steps before and after ILLink. IMarkHandler steps would run after the trimmer with the assumption that all metadata in the assembly after ILLink is marked.
A custom step that runs before the trimmer:
- May unconditionally mark metadata
- May create dynamic dependencies that are conditional on an ICustomAttributeProvider being marked
- May set a TypePreserve on a type.
- May edit metadata
- Must not check for Annotations such as IsMarked
A custom step that runs after ILLink:
- Shall consider all the metadata it processes as marked
- May edit metadata that does not create additional dependencies
- Must not mark additional metadata
- Must not create additional dependencies
A custom step host project would exist in each of the repos where they are used to allow custom behavior for each platform and decouple custom steps from the trimmer.
Considerations
The primary incompatibility with this approach is dependencies that cannot be expressed with DynamicDependencyAttributes. For example, if a dependency is required if and only if two or more types are marked. It's not clear how prevalent these kinds of dependencies are in the custom steps.
DynamicDependency is also treated as a reflection dependency in ILCompiler, which may lead to complications and unintended affects (for example, warnings if the depended-on member is annotated with DAM).
This would require loading and writing the assemblies two more times than currently. A good proxy for this is the time spent in the Mono.Cecil dll. Testing on my laptop showed illink spends about 12.05% of its time in Mono.Cecil when linking a sample Maui android app, and 31% of its time in Mono.Cecil when trimming a hello world app (I haven't been able to trim an IOS or Mac app without a Mac).
Creating unconditional dependencies and dynamic dependencies is a model very similar to the DependencyAnalysis in ILCompiler. If the custom steps are able to declare their dependencies in this way and the trimmer is transitioned to the DependencyAnalysisFramework, the trimmer could offer a different method for declaring these DynamicDependencies that integrates well with the DependencyAnalysisFramework.
The custom steps may also be a great place to test transitioning from Mono.Cecil to System.Reflection.Metadata. Since the custom steps themselves and the tool that runs them would likely be smaller than the trimmer, we could try transitioning the custom steps first to find the pain points before transitioning the trimmer. This would also alleviate the performance impact of loading the assemblies multiple times.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status