reversemarkdown-net icon indicating copy to clipboard operation
reversemarkdown-net copied to clipboard

Make the library trim-compatible and NativeAOT-compatible.

Open DL444 opened this issue 1 year ago • 0 comments

The library implementation today relies on dynamic reflection and is therefore unsafe for trimming and NativeAOT publishing. There are currently 3 problems that need to be addressed to make the library trim-safe, all of which are related to Converter construction:

public Converter(Config config, params Assembly[] additionalAssemblies)
{
    // ...
    foreach (var converterType in assembly.GetTypes() // Types without static references can be trimmed away
        .Where(t => t.GetTypeInfo().GetInterfaces().Contains(typeof(IConverter)) && // Interface implementation info can be trimmed away
    // ...
    Activator.CreateInstance(converterType, this); // Required constructor can be trimmed away
    // ...
}

The first two issues are related to converter discovery, whose current mechanism is fundamentally incompatible with trimming and NativeAOT. A trim-friendly mechanism cannot rely on run-time reflection to discover converter types, as non-static references can be trimmed. Instead, a hard-coded list of converters is required for the compiler to statically determine all converter types consumed, which is usually generated automatically with a source generator. Note that System.Text.Json also had the similar problem which is solved with the same approach.

The third issue is related to converter registration, which can be made compatible by annotations that inform the compiler to keep the required constructor. However, the required annotations cannot be applied with the existing API shape, so a new API is needed to solve both issues.

Proposal

  1. Implement a source generator that generates a static list of converter types.
  2. Add an Converter constructor which consumes the generated list and informs the compiler to keep the converter constructors.
  3. Annotate the existing reflection-based API as trim-unsafe.

Developer Experience

  1. The source generator injects two annotation definitions: ConverterAttribute and ConverterTypesInfoAttribute (name TBD).
  2. Developer annotates each converter type with ConverterAttribute.
    This attribute is not required but is strongly recommended because it simplifies source generator implementation and drastically improves code editor performance by filtering the syntax tree effectively.
  3. Developer declares a partial class and annotate the class with ConverterTypesInfoAttribute. The source generator injects a auto-generated list of converter types into this class.
  4. Developer pass the injected type list into a new Converter constructor to build the converter.

Code sample:

[Converter] // Converters classes are annotated.
internal sealed class MyConverter : IConverter
{
    public MyConverter(Converter converter)
    {
        // Register converter.
    }
}

[Converter]
internal sealed class AnotherConverter : IConverter
{
    public MyConverter(Converter converter)
    {
        // Register converter.
    }
}

[ConverterTypesInfo] // Tells the source generator to inject the list into this class.
internal static partial class MyConverterTypesInfo { }

Usage:

// ConverterTypes is the auto-generated converter type list.
var converter = new Converter(new Config(), MyConverterTypesInfo.ConverterTypes);

I can help implementing the proposal if it gets approved. Actually, see my fork for a working proof-of-concept. This POC still needs some polishing to be suitable for production, but the core experience is there.

DL444 avatar Oct 20 '24 23:10 DL444