Animating expression trees with code generation

Expression trees System.Linq.Expressions



make it possible to express intentions not only by the code itself, but also by its structure and syntax.





Their creation from lambda expressions is, in fact, syntactic sugar, in which ordinary code is written, and the compiler builds a syntax tree ( AST ) from it , which includes references to objects in memory, and captures variables. This allows you to manipulate not only data, but also the code in the context of which they are used: rewrite, supplement, transfer, and only then compile and execute.





Run-time compilation produces productive delegates that are often faster than those compiled at build time ( at the cost of less overhead ). However, the compilation itself takes up to tens of thousands of times longer than calling the compilation result.





(benchmark)

Act





Time, ns





Cached Compile Invoke





0.5895 ± 0.0132 ns





Compile and Invoke





83,292.3139 ± 922.4315 ns









This is especially offensive when the expression is simple, for example, it contains only access to a property (in libraries for mapping, serialization, data binding), calling a constructor or method (for IoC / DI solutions).





Compiled delegates are usually cached to be reused, but this does not save in scripts when the first access happens to a lot at a time. In such cases, the run-time of compiling expressions becomes significant and delays the launch of the application or individual windows.





To reduce the time it takes to get delegates from expression trees, use:





  • Built-in interpretation.

    The need to use an interpreter instead of a compiler is indicated by the corresponding flag:





    Expression.Compile(preferInterpretation: true)
          
          



    It happens through reflection, but with the overhead of forming an instruction stack.





    Xamarin.iOS, Xamarin.watchOS, Xamarin.tvOS, Mono.PS4 Mono.XBox IL (System.Reflection.Emit



    ) .





  • FastExpressionCompile @dadhi.

    p IL .





    JIT Mono Interpreter.





  • .

    , .





    , . , Fasterflect, System.Reflection.Emit



    Mono Interpreter.





, , :





- (design-time) (compile-time).





compile-time .





API , . , , . - DI — , .





API , . : , run-time compile-time — . , — .





,





namespace Namespace
{
  public class TestClass
  {
    public int Property { get; set; }
  }
}
      
      



System.Linq.Expressions.Expression<T>







Expression<Func<TestClass, int>> expression = o => o.Property;
      
      







Func<object, object> _ = obj => ((Namespace.TestClass)obj).Property;
Action<object, object> _ => (t, m) => ((Namespace.TestClass)t).Property
  = (System.Int32)m;
      
      



:





namespace ExpressionDelegates.AccessorRegistration
{
  public static class ModuleInitializer
  {
    public static void Initialize()
    {
      ExpressionDelegates.Accessors.Add("Namespace.TestClass.Property",
        getter: obj => ((Namespace.TestClass)obj).Property,
        setter: (t, m) => ((Namespace.TestClass)t).Property = (System.Int32)m);
    }
  }
}
      
      



, , :





  • Microsoft.CodeDom,





  • T4,





  • PostSharp,





  • Fody,





  • Roslyn Source Generators.





, Roslyn Source Generators C# .





, Roslyn Source Generators , . . Roslyn API, code-fix.





Roslyn Source Generators - ( !) .





:





namespace Microsoft.CodeAnalysis
{
  public interface ISourceGenerator
  {
    void Initialize(GeneratorInitializationContext context);
    void Execute(GeneratorExecutionContext context);
  }
}
      
      



.





Initialize



- . GeneratorInitializationContext



.





Execute



, , , , .





Roslyn SyntaxTree



:





GeneratorExecutionContext.Compilation.SyntaxTrees
      
      



:





semanticModel =
  GeneratorExecutionContext.Compilation.GetSemanticModel(SyntaxTree)
      
      



, ( ) , , .





- System.Linq.Expressions.Expression<T>



- , , :





, (Symbol



), :





  • , ;





  • ;





  • IsStatic



    , IsConst



    , IsReadOnly



    .





.





Roslyn API (Microsoft.CodeAnalysis



) , c API (System.Reflection



). ISymbol.ToDisplayString(SymbolDisplayFormat)



c :





/, :





:





var sourceBuilder = new StringBuilder(
@"namespace ExpressionDelegates.AccessorRegistration
{
  public static class ModuleInitializer
  {
    public static void Initialize()
    {");

      foreach (var line in registrationLines)
      {
        sourceBuilder.AppendLine();
        sourceBuilder.Append(' ', 6).Append(line);
      }

      sourceBuilder.Append(@"
    }
  }
}");

GeneratorExecutionContext.AddSource(
  "AccessorRegistration",
  SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
      
      



... :)





, Source Generators , C# 9+. .NET 5.





Roslyn Source Generators API .NET Standard, .NET Core, .NET Framework Xamarin Uno.SourceGeneration.





Uno.SourceGeneration ISourceGenerator [Generator], # 9 Microsoft.CodeAnalysis



Uno:





using Uno.SourceGeneration;
using GeneratorAttribute = Uno.SourceGeneration.GeneratorAttribute;
using ISourceGenerator = Uno.SourceGeneration.ISourceGenerator;
      
      



.

, :





<ItemGroup>
  <SourceGenerator Include="PATH\TO\GENERATOR.dll" />
</ItemGroup>
      
      



, nuget, MSBuild props :









API , , .





Module Initializer. ( ), . CLR, , C# c [ModuleInitializer]



9 .





FodyFody.ModuleInit



. ModuleInitializer



. .





Fody.ModuleInit



MSBuild FodyWeavers.xml



Weaver- Fody .





, :





  1. Source Generator , , ModuleInitializer



    .





  2. Fody.ModuleInit



    ModuleInitializer



    .





  3. ModuleInitializer



    , .





:





Expression<Func<string, int>> expression = s => s.Length;

MemberInfo accessorInfo = ((MemberExpression)expression.Body).Member;
Accessor lengthAccessor = ExpressionDelegates.Accessors.Find(accessorInfo);

var length = lengthAccessor.Get("17 letters string");
// length == 17
      
      



, :





, - .









,









4.6937 ± 0.0443









5.8940 ± 0.0459









191.1785 ± 2.0766









88,701.7674 ± 962.4325

















1.7740 ± 0.0291









5.8792 ± 0.1525









163.2990 ± 1.4388









88,103.7519 ± 235.3721

















1.1767 ± 0.0289









4.1000 ± 0.0185









186.4856 ± 2.5224









83,292.3139 ± 922.4315





, .





, — , .





Flame plot of benchmark search and invocation of generated property access delegate
Flame-

System.Reflection.MemberInfo



. .





.





: github/ExpressionDelegates, nuget.





, Source Generators :





  • Source Generator Playground (github).

    Roslyn Source Generators , .





  • Visual Studio.

    Roslyn Syntax API .





  • Source Generator . .

    Visual Studio «Just-In-Time debugger» Tools -> Options -> Debugging -> Just-In-Time Debugging -> ☑ Managed



    .





  • *.cs



    , Visual Studio 16.8.

    Uno.SourceGeneration : \obj\{configuration}\{platform}\g\



    .

    Roslyn Source Generators MSBuild EmitCompilerGeneratedFiles



    .

    : \obj\{configuration}\{platform}\generated\



    , CompilerGeneratedFilesOutputPath



    .





  • Source Generators MSBuild.

    Uno.SourceGeneration





    GeneratorExecutionContext.GetMSBuildPropertyValue(string)
          
          



    For Roslyn Source Generators, the required properties must first be separately designated in the MSBuild group CompilerVisibleProperty



    and only then called:





    GeneratorExecutionContext.AnalyzerConfigOptions.GlobalOptions
      .TryGetValue("build_property.<PROPERTY_NAME>", out var propertyValue)
          
          



  • From the generator, you can throw warnings and build errors.





    //Roslyn Source Generators
    GeneratorExecutionContext.ReportDiagnostic(Diagnostic)
    //Uno.SourceGeneration:
    GeneratorExecutionContext.GetLogger().Warn/Error().
          
          










All Articles