Solving problems with Compiler-as-a-Service

Tags: walkthoughs, code, roslyn, diagrams, reactive extensions

Picture the Problem

Ever been in one of those situations when a solution almost seems too easy? That’s how I felt recently when I integrated Roslyn’s scripting engine into at an existing codebase with a speed that very nearly made my eyeballs bleed.

I was stumped. As I'd mentioned previously, I'd been experimenting with creating marble diagrams to help me define the different events and state transformations in a simple proof of concept game.

My understanding and knowledge of many of the concepts and patterns involved with using the Reactive Extensions is unfortunately sparse, so I couldn’t make much progress without feedback so I could work through different ways of composing the logic.

Doing that requires that there be some means of comparison between the two, but my thick skull can’t make the translation without help. I needed a way to go backwards, from code to diagram. A quick set of searches revealed two main options.

The official Rx samples contain a simple app that allows you to select from a list of expressions to diagram, and supposedly allows direct text input of expressions. Promising! Our other pre-made option is the RxSandbox marble diagram generator. Screenshots seem to give the RxSandbox a solid edge over the first, but before making any judgments, we’ll have to actually run it to see what it’s capable of doing. Two solid contenders for getting me up and running, so let’s get them running and do some assessing!

The Rx have had a lot of churn since their first release almost 4 years ago, and as a result, code samples and documentation aren’t always functioning the way they were intended – if they even compile. The sample marble diagram generator did compile and run, but the output wasn’t optimal. Many of the expressions available in the list didn’t generate any readable output, and a quick test revealed no joy in the text input department.

The RxSandbox is clearly explained by the author, and does exactly as advertised: it produces clean, attractive-looking diagrams from code expressions. It didn't take too long for me to come across a potential show-stopper though when I found that the expressions to be diagrammed have to be hard-coded and compiled into the app at design-time. If it only I could enter an expression at runtime and see the resulting diagram, I’d be able to really improve my learning cycle. But alas, another promising solution an early demise.

…or did I?

 

Focus on the problem

The core problem I was facing is that I needed to be able to compile a string into a programmatically analyzable expression. Better yet, what if I could compile that string into a directly executable delegate, and diagram the results? The author of RxSandbox, Marcin Najder hints in his post, which I later confirmed by looking through the source, that the extensibility model can handle delegates, Funcs, and lambda expressions with no problem. Perfect!

…except that…

Unfortunately, it’s not possible to directly input text at run-time without some heavy hoop-jumping with Code DOM, assembly loading, and the like. Bummer. Struck out again!

…or did I?

PM> Install-Package Roslyn

PM> install-package roslyn
You are downloading Roslyn from Microsoft, the license agreement to which is available at
http://go.microsoft.com/fwlink/?LinkID=235993. Check the package for additional dependencies, which may come with their own license agreement(s). Your use of the package and dependencies constitutes your acceptance of their license agreements. If you do not accept the license agreement(s), then delete the relevant components from your device.
Successfully installed 'Roslyn 1.0.11014.5'.
Successfully added 'Roslyn 1.0.11014.5' to RxSandbox.

 

That Just Happened

 

Bam. Compiler-as-a-service has just been made available to the app. It's just that easy! It turns out there’s this could be a compromise third option! Roslyn is still in the experimental stage, but it includes a scripting API that allows arbitrary strings of C# code to be executed in-place. It also turns out that integrating Roslyn into the app is easier than you may think. Here’s how:

  1. Add the Roslyn NuGet Package to the project. Just go to the package manager console prompt and type Install-Package Roslyn and hit the Enter key.
  2. Step 1: Create the provider class
    Create a new class and name it StringExpressionProvider. Have it implement IExpressionProvider (located in the RxSandbox.Infrastructure namespace):
    public class StringExpressionProvider : IExpressionProvider

    Don't forget the usings to the top of the file:

    using System;
    using System.Collections.Generic;
    using System.Reactive.Linq;
    using Roslyn.Scripting.CSharp;
    
  3. Initialize the provider. Since the provider is instantiated via a call to Activator.CreateInstance, we must implement a default, parameterless constructor in order to get things up and running along with some class members and static data.
    private ScriptEngine _scriptEngine;
    private readonly List<string> _scriptUsings = new List<string> {
    "System", "System.Collections.Generic", "System.Reactive", "System.Reactive.Linq" };
  4. I'm not worrying right now about how the string expression is passed into the provider, since that can come from almost literally anywhere. So for now we'll just use a hard-coded string declared as a private member variable of the provider:

    private const string TestExpression = @"
       Func<IObservable<long>,IObservable<string>> GetDel() { 
           return ival =>  Observable.Interval(TimeSpan
               .FromSeconds(1))
               .Timestamp()
               .Sample(ival)
               .Take(5)
               .Select(x => string.Format(""{0} - {1}"", 
                        x.Timestamp, x.Value));
            } 
            GetDel();";

    public StringExpressionProvider() { _scriptEngine = new ScriptEngine( new[] { typeof(Observable).Assembly.Location }, _scriptUsings); }

    Part of this code lets Roslyn know what and where assembly references and usings needed. This is accomplished using some light reflection hackery, but since it only happens when the class is first created, so it's not enough that I want to address it immediately. Put it on a list of refactorings to-do. On a random note, if this concept proves out, it would demonstrate how we could add new features so extremely cool they border the absurd to this application. I imagine a module would be possible that upon given the path to a .csproj file, walks the project structure crunching and analyzing source to generate interaction marble diagrams for each state interaction in an application by virtually executing the source code line-by-line and analyzing the results of passing sample data through the system. Neat.

  5.  

    Now it's time to implement the core logic of IExpressionProvider. Now, the ultimate goal of this method is simply to return a list of ExpressionDefinition instances. ExpressionDefinition has a nice set of overloaded static factory methods hanging off of it, all of which accept an executable representation of a method as part of their parameter list. I love the simplicity that comes about when you can treat executable logic as data! Again, the goal at the moment is simply to see if this crazy concept is feasible, not making it look good, so we don't have to worry about niceties like perfectly formatting the description or sample display:

    public IEnumerable<ExpressionDefinition> GetExpressions()
    {
        try
        {
            var fun = _scriptEngine
                 .Execute<Delegate>(TestExpression);
            var def = ExpressionDefinition.Create(fun, 
                      new ExpressionSettings
                      { 
                          GroupPath = "String Expressions",
                          Name="TestExpr",
                          Description = TestExpression
                      }); 
            return new[] {def};
         } 
         catch (Roslyn.Compilers.CompilationErrorException ex)
         {
            Console.WriteLine("{0}{1}", Environment.NewLine, 
                                        ex.Diagnostics);
            throw;
        }
    }
    

    After all that lead-in, the actual creation of the delegate will likely be a bit anticlimactic. Simply call Execute(…) on the scripting engine object, passing the current instance of our class (via this) to serve as context. What's returned depends on the last expression in the executed script string, which is why Execute has generic and non-generic overloads.

 

Build and run the app, and you should see a new entry in the treeview on the left side of the window under the group "StringExpressionProvider". Double-click that entry and you'll be presented with what could seem a non-result:

 

Seeing is believing

 

Fret not dear reader! Clicking on the Live diagram, entering a value and clicking the OnNext a bunch of times reveals that we have hit pay dirt! The fact that the provider showed up in the tree view means that it is being created and loaded. If it's been loaded, then that means the Roslyn integration worked, and the expressions were created and passed to RxSandbox.

image

    image

     

    So that's it - we're done, right? Well, yes and no.

    Yes, because we've successfully completed our spike of this functionality, and no because while it has proved itself a working candidate for solving our original problem - namely that of generating marble diagrams from arbitrary code, we still have a number of stories that haven't yet been told.

    What's next

    Just because we can display some pretty circles in a swim lane doesn't mean we're totally there yet. There are still some important limitations with our current implementation. Here's our scratch backlog:

    • Template or somehow format the displayed code to be more in line with the other samples and for clarity. This could be done by separating boilerplate code in the script from the actual code being executed.
    • Support for non-observable inputs to observables. Currently, you need to structure the code Func so that any input is an IObservable<Tinput>. It'd be nice if we could just use an int, for example. This could be accomplished in a number of ways, some more disruptive than others. Roslyn could be used to analyze the inputted string and convert any non-observable inputs to Observables, or RxSandbox could be modified to accept them directly
    • Allow users to be able to enter text directly into the app and see the results without having to change the source.
    • The shortest path to this is to have the provider simply read a text file containing the expression(s) in the local directory.
    • Currently, the WPF app isn't really factored to allow real-time updates. Making the required modifications isn't too onerous, but it does mean changing the core RxSandbox app, which means it has potential to break the current functionality.
    • Factor out existing use of ObservableCollections and other non-Rx UI stuff to streamline and allow for richer display and scenarios.

    Something I just can't leave alone is the fact that there are tools out there that give us many of the capabilities we've been looking for that we currently don't have, with less overhead. I think that something worth exploring is to retain the cleanly separated RxSandbox.Infrastructure classes and wrap it into a LINQPad plugin. Related thoughts:

    1. LINQPad already has a C# script interpreter, so there is overlap with Roslyn. However, I was able to do much of the functional design and testing in LINQPad while using Roslyn at the same time, that just yields greater possibilities in my mind
    2. The output for LINQPad is HTML-based, which means that we can use CSS to style the diagrams any way we wish. I'm more comfortable and familiar with HTML/CSS than I am XAML/WPF, so this reduces my intellectual overhead, which will allow me to focus more on implementing features rather than how it looks.
    3. Come to think of it, if the rendering is HTML, why not just make it a web app? We've already proven that Roslyn can provide a good chunk of the interpretive faculties we need, and we'd be using the already-written Infrastructure libraries in any case, so this is a strong option as well.