-
-
Notifications
You must be signed in to change notification settings - Fork 520
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
POC - Refactor CLI Project for AOT Compatibility and Trimming Support #1706
Conversation
Summary: This commit refactors the Spectre.Console.Cli project to replace reflection-based object creation with explicit and type-safe constructs to enable support for Ahead-Of-Time (AOT) compilation and code trimming. AOT mode is only enabled for .NET 9. Introduce DynamicallyAccessedMembers Annotations: * Annotated various types and generic parameters (e.g., CommandApp, CommandSettings) with [DynamicallyAccessedMembers] to ensure necessary metadata is preserved during trimming for AOT scenarios. Explicit Object Creation: * Introduced CreateInstanceHelpers to encapsulate type-safe and explicit instance creation logic. This replaces Activator.CreateInstance for array, multimap, and command settings instantiation. Refactor Command Attributes: * Updated constructors of CommandArgumentAttribute and CommandOptionAttribute to explicitly accept Type for argument and option types, with corresponding logic to register instance builders for these types. Remove Reflection-Heavy Logic: * Replaced uses of Activator.CreateInstance across multiple files with explicit methods from CreateInstanceHelpers. * Removed reflection-reliant dynamic code annotations ([RequiresDynamicCode]) from critical paths where alternatives are now implemented. Enhanced Type Conversion: * Consolidated type conversion logic into TypeConverterHelper with static converters for intrinsic and complex types. * Ensured compatibility by defaulting to explicit conversion mechanisms where possible. Array and Collection Initialization: * Migrated array and collection initialization to CreateInstanceHelpers to handle both primitive and complex types without runtime reflection. Builder and Registrar Adjustments: * Refactored ITypeRegistrar and related classes to include explicit type member annotations, ensuring required metadata survives trimming. * Adjusted configurators (Configurator, IConfigurator) to support type-safe default and added commands. Testing Adjustments: * Added internal visibility to support AOT testing scenarios. * Introduced new project files for trimming-specific tests. Risks ========================= Unit Testing: I tried my best to figure out if I could get the TrimTest console project to run the Cli unit tests when published to AOT via xunit.runner.console, but I'm just not smart enough. I'd feel infinitely better about things if it could do so. As is, the features included there do all work. This project is needed to light up the analyzers properly. Explicit Type Requirement: Attributes now require explicit Type definitions for non-intrinsic types (e.g., DirectoryInfo). Users must specify the argumentType or optionType parameter for such types. Failure to do so will result in runtime exceptions or incorrect behavior. Compatibility with Existing Registrations: Type Registrar Changes: Registrations now require explicit [DynamicallyAccessedMembers] annotations on implementation types. Users with their own custom registration and resolver will need to adapt to match Array parameters: Users who for whatever reason might have created their own struct and are using them as IEnumerable parameters are going to get failures in AOT scenarios. We can't dynamically create those arrays.
forgot to include my requisite @agocke mention on this |
Comic Code. Seems to help with my dyslexia when reading code. The ugly inconsistency keeps everything from kinda blending together, but I can see how it would be an affront to scandinavian design sensibilities |
@phil-scott-78 Ah, I didn't know! Interesting! |
Haven’t had time to look through fully, but I want to caution use of feature switches. They’re very useful, but they should be used carefully. Changing behavior drastically using a feature switch, without a corresponding trim warning, might surprise users when the behavior is much different than they expected. Ideally the feature switch is used to shift to an alternate, but compatible implementation. When that’s not possible, we prefer to outright disable features and throw at the earliest opportunity. Otherwise it’s very hard to figure out why things don’t work. Our general philosophy is “trim warning or it works.” |
Had some time when the kids were at preschool and threw together a quick POC of an analyzer and fixes. Needs a decent amount of polish and work to get all the cases, but I had a bit of a battle figuring out how to run the analyzer only in AOT mode and also how to test that scenario. Wanted to push that up before I forget. https://github.com/phil-scott-78/spectre-console-analyzer/tree/aot-analyzer Screen.Recording.2024-12-03.182436.mp4 |
This commit is a proof of concept showing a potential path to refactoring the
Spectre.Console.Cli
project to replace reflection-based object creation with explicit and type-safe constructs to enable support for Ahead-Of-Time (AOT) compilation and code trimming. AOT mode is only enabled for .NET 9. One change over previous attempt that I think allowed for success (or at least not as big of a flop):Feature Switches added in .NET 9 to keep the code much cleaner and explicit about how we are handling things with dynamical instantiation. Compared to previous attempts there are very few #IF preprocessors and uses of
UnconditionalSuppressMessage
attributes.Attacking it with a better understanding of how reflection is used. I moved around quite a bit with the idea that Settings have properties that are either
Using that I was able to create look ups that allows avoiding reflection
It involves multiple changes to critical areas, and there isn't an AOT xunit runner. The only way to verify things still work in AOT mode might be to add scenarios to the
TrimTest
project to try and break it when published. It's a tedious, tedious process. I suspect someone smarter than me could figure out how to bootstrap thexunit.runner.console
package then get CI set up for publishing and running those tests manually, but I gave it a go and came up short.Changes
Introduce
DynamicallyAccessedMembers
Annotations:CommandApp
,CommandSettings
) with[DynamicallyAccessedMembers]
to ensure necessary metadata is preserved during trimming for AOT scenarios.Explicit Object Creation:
CreateInstanceHelpers
to encapsulate type-safe and explicit instance creation logic. This replacesActivator.CreateInstance
for array, multimap, and command settings instantiation.Refactor Command Attributes:
CommandArgumentAttribute
andCommandOptionAttribute
to explicitly acceptType
for argument and option types, with corresponding logic to register instance builders for these types.Remove Reflection-Heavy Logic:
Activator.CreateInstance
across multiple files with explicit methods fromCreateInstanceHelpers
.[RequiresDynamicCode]
) from critical paths where alternatives are now implemented.Enhanced Type Conversion:
TypeConverterHelper
with static converters for intrinsic and complex types.Array and Collection Initialization:
CreateInstanceHelpers
to handle both primitive and complex types without runtime reflection.Builder and Registrar Adjustments:
ITypeRegistrar
and related classes to include explicit type member annotations, ensuring required metadata survives trimming.Configurator
,IConfigurator
) to support type-safe default and added commands.Testing Adjustments:
Risks
Unit Testing:
TrimTest
console project to run theCli
unit tests when published to AOT viaxunit.runner.console
, but I'm just not smart enough. I'd feel infinitely better about things if it could do so. As is, the features included there do all work. This project is needed to light up the analyzers properly.Explicit Type Requirement:
Type
definitions for non-intrinsic types (e.g.,DirectoryInfo
). Users must specify theargumentType
oroptionType
parameter for such types. Failure to do so will result in runtime exceptions or incorrect behavior.Compatibility with Existing Registrations:
[DynamicallyAccessedMembers]
annotations on implementation types. Users with their own custom registration and resolver will need to adapt to match.Array Parameters:
IEnumerable
parameters are going to get failures in AOT scenarios. We can't dynamically create those arrays.Developer experience moving forward:
I have two kids and no time, why can't I have no kids and two time?
2.0-preview
release