-
-
Notifications
You must be signed in to change notification settings - Fork 524
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor CLI Project for AOT Compatibility and Trimming Support
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.
- Loading branch information
1 parent
92daeb7
commit adaac27
Showing
52 changed files
with
889 additions
and
101 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
using System.Runtime.CompilerServices; | ||
|
||
namespace Spectre.Console.Cli; | ||
|
||
internal static class CreateInstanceHelpers | ||
{ | ||
private static readonly Dictionary<Type, Func<int, Array>> _arrayBuilder; | ||
private static readonly Dictionary<Type, Func<IMultiMap>> _multiMapBuilder; | ||
private static readonly Dictionary<Type, Func<object[], object>> _instanceBuilder = new(); | ||
|
||
static CreateInstanceHelpers() | ||
{ | ||
_arrayBuilder = new Dictionary<Type, Func<int, Array>> | ||
{ | ||
[typeof(bool)] = size => new bool[size], | ||
[typeof(byte)] = size => new byte[size], | ||
[typeof(sbyte)] = size => new sbyte[size], | ||
[typeof(char)] = size => new char[size], | ||
[typeof(double)] = size => new double[size], | ||
[typeof(string)] = size => new string[size], | ||
[typeof(int)] = size => new int[size], | ||
[typeof(short)] = size => new short[size], | ||
[typeof(long)] = size => new long[size], | ||
[typeof(float)] = size => new float[size], | ||
[typeof(ushort)] = size => new ushort[size], | ||
[typeof(uint)] = size => new uint[size], | ||
[typeof(ulong)] = size => new ulong[size], | ||
[typeof(DateTime)] = size => new DateTime[size], | ||
[typeof(DateTimeOffset)] = size => new DateTimeOffset[size], | ||
[typeof(decimal)] = size => new decimal[size], | ||
[typeof(TimeSpan)] = size => new TimeSpan[size], | ||
[typeof(Guid)] = size => new Guid[size], | ||
#if !NETSTANDARD2_0 | ||
[typeof(Int128)] = size => new Int128[size], | ||
[typeof(Half)] = size => new Half[size], | ||
[typeof(UInt128)] = size => new UInt128[size], | ||
[typeof(DateOnly)] = size => new DateOnly[size], | ||
[typeof(TimeOnly)] = size => new TimeOnly[size], | ||
#endif | ||
}; | ||
|
||
_multiMapBuilder = new Dictionary<Type, Func<IMultiMap>> | ||
{ | ||
[typeof(bool)] = () => new MultiMap<string, bool>(), | ||
[typeof(bool)] = () => new MultiMap<string, bool>(), | ||
[typeof(byte)] = () => new MultiMap<string, byte>(), | ||
[typeof(sbyte)] = () => new MultiMap<string, sbyte>(), | ||
[typeof(char)] = () => new MultiMap<string, char>(), | ||
[typeof(double)] = () => new MultiMap<string, double>(), | ||
[typeof(string)] = () => new MultiMap<string, string>(), | ||
[typeof(int)] = () => new MultiMap<string, int>(), | ||
[typeof(short)] = () => new MultiMap<string, short>(), | ||
[typeof(long)] = () => new MultiMap<string, long>(), | ||
[typeof(float)] = () => new MultiMap<string, float>(), | ||
[typeof(ushort)] = () => new MultiMap<string, ushort>(), | ||
[typeof(uint)] = () => new MultiMap<string, uint>(), | ||
[typeof(ulong)] = () => new MultiMap<string, ulong>(), | ||
[typeof(DateTime)] = () => new MultiMap<string, DateTime>(), | ||
[typeof(DateTimeOffset)] = () => new MultiMap<string, DateTimeOffset>(), | ||
[typeof(decimal)] = () => new MultiMap<string, decimal>(), | ||
[typeof(TimeSpan)] = () => new MultiMap<string, TimeSpan>(), | ||
[typeof(Guid)] = () => new MultiMap<string, Guid>(), | ||
#if !NETSTANDARD2_0 | ||
[typeof(Int128)] = () => new MultiMap<string, Int128>(), | ||
[typeof(Half)] = () => new MultiMap<string, Half>(), | ||
[typeof(UInt128)] = () => new MultiMap<string, UInt128>(), | ||
[typeof(DateOnly)] = () => new MultiMap<string, DateOnly>(), | ||
[typeof(TimeOnly)] = () => new MultiMap<string, TimeOnly>(), | ||
#endif | ||
}; | ||
} | ||
|
||
/// <summary> | ||
/// Add a new known type instance builder. | ||
/// </summary> | ||
/// <param name="type">The type to build.</param> | ||
public static void RegisterNewInstanceBuilder([DynamicallyAccessedMembers(PublicConstructors)] Type type) | ||
{ | ||
_instanceBuilder.Add(type, input => | ||
{ | ||
var constructor = type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, input.Select(i => i.GetType()).ToArray(), null); | ||
if (constructor == null) | ||
{ | ||
throw new InvalidOperationException("Could not create single parameter instance."); | ||
} | ||
|
||
return constructor.Invoke(input); | ||
}); | ||
} | ||
|
||
public static bool TryGetInstance(Type type, object[] parameters, [NotNullWhen(true)] out object? result) | ||
{ | ||
if (_instanceBuilder.TryGetValue(type, out var factory)) | ||
{ | ||
result = factory(parameters); | ||
return true; | ||
} | ||
|
||
if (CanDoCreateInstance) | ||
{ | ||
result = BuildInstance(type, parameters); | ||
if (result != null) | ||
{ | ||
return true; | ||
} | ||
} | ||
|
||
result = null; | ||
return false; | ||
} | ||
|
||
private static object? BuildInstance(Type type, object[] parameters) | ||
{ | ||
if (CanDoUnreferencedCode) | ||
{ | ||
var constructor = type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, parameters.Select(i => i.GetType()).ToArray(), null); | ||
if (constructor == null) | ||
{ | ||
throw new InvalidOperationException("Could not create single parameter instance."); | ||
} | ||
|
||
return constructor.Invoke(parameters); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
public static Array CreateArrayInstance(Type type, int size) | ||
{ | ||
#if NET9_0_OR_GREATER | ||
return Array.CreateInstanceFromArrayType(type, size); | ||
#else | ||
var elementType = type.GetElementType(); | ||
if (elementType == null) | ||
{ | ||
throw new InvalidOperationException("Could not create an array of type " + type.FullName + "."); | ||
} | ||
|
||
return CreateArrayInstanceFromElementType(elementType, size); | ||
#endif | ||
} | ||
|
||
public static IMultiMap? CreateMultiMapInstance(Type type1, Type type2) | ||
{ | ||
if (type1 == typeof(string)) | ||
{ | ||
if (_multiMapBuilder.TryGetValue(type2, out var multiMapBuilder)) | ||
{ | ||
return multiMapBuilder.Invoke(); | ||
} | ||
} | ||
|
||
if (CanDoCreateInstance) | ||
{ | ||
return Activator.CreateInstance(typeof(MultiMap<,>).MakeGenericType(type1, type2)) as IMultiMap; | ||
} | ||
|
||
throw new InvalidOperationException("Could not create a multi map of type " + type1.FullName + " and " + type2.FullName + ". If you are running in AOT, only dictionaries with a string key and an .NET primitive value are supported."); | ||
} | ||
|
||
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "We are only creating an array instance dynamically for non-value types not in our dictionary.")] | ||
public static Array CreateArrayInstanceFromElementType(Type getType, int sourceArrayLength) | ||
{ | ||
if (_arrayBuilder.TryGetValue(getType, out var value)) | ||
{ | ||
return value(sourceArrayLength); | ||
} | ||
|
||
if (!getType.IsValueType || CanDoCreateInstance) | ||
{ | ||
return Array.CreateInstance(getType, sourceArrayLength); | ||
} | ||
|
||
throw new InvalidOperationException("Cannot create array instance of type " + getType.FullName + ". "); | ||
} | ||
|
||
[FeatureGuard(typeof(RequiresUnreferencedCodeAttribute))] | ||
public static bool CanDoUnreferencedCode | ||
{ | ||
get | ||
{ | ||
#if NET9_0_OR_GREATER | ||
#pragma warning disable IL4000 | ||
return RuntimeFeature.IsDynamicCodeSupported; | ||
#pragma warning restore IL4000 | ||
#else | ||
return true; | ||
#endif | ||
} | ||
} | ||
|
||
[FeatureGuard(typeof(RequiresDynamicCodeAttribute))] | ||
public static bool CanDoCreateInstance | ||
{ | ||
get | ||
{ | ||
#if NET9_0_OR_GREATER | ||
return RuntimeFeature.IsDynamicCodeSupported; | ||
#else | ||
return true; | ||
#endif | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.