I've been playing with the Dynamic Language Runtime (DLR) and IronRuby lately, and one of the things I've found particularly interesting is the DLR Hosting API, which allows you to bring up the DLR and execute and evaluate scripts inside any .NET 2.0 application.

If you haven't played with the DLR before, it might be worth mentioning that the DLR is the Microsoft.Scripting.dll assembly you'll find in the one of the projects shipping with it (it may sound stupid to actually mention it, but it actually took me some time to find out that was it).

Several versions of the DLR are around, given that it is still in in heavy development, including some early builds in the Silverlight 1.1 alpha, IronPython and IronRuby. For now, I've been playing with the version that's in the IronRuby SVN repository.

The core, low-level hosting interfaces in the DLR are in the Microsoft.Scripting.Hosting namespace, but there are several classes you can use in Microsoft.Scripting  that make the whole process a lot easier.

Keep in mind that I'm writing this as I learn more of the DLR and IronRuby, so there's a chance I make some mistakes along the way :-). Also, the whole hosting API (and IronRuby itself) is in full development, and will likely change a lot in the following months.

The "long" way around

There are actually several ways you can use the DLR hosting interfaces, depending on the level of control and flexibility you need, as well as whether you want to host it locally (inside the same Appdomain) or remotely. Let's start with the long way of doing it (I will only discuss local hosting for now).

The first thing to look at is the ScriptDomainManager class. When hosting the DLR locally, this works as a singleton instance through the CurrentManager property. The ScriptDomainManager gives you a way to get access to the language providers supported by the DLR, like those of IronRuby or IronPythong. In fact, you can register new language providers if needed using the RegisterLanguageProvider() method.

Once you have the ScriptDomainManager, you can use the LanguageProvider for the language you want to; the easiest way is by looking it up using the file extension used for the language, like ".py" for Python, ".rb" for IronRuby and so on, or by using it's name. Each of the currently supported languages are recognized by several names; for example the language provider for IronRuby can be retrieved using "rb", "ruby" or "ironruby".

ScriptDomainManager manager = ScriptDomainManager.CurrentManager;
LanguageProvider ruby =
   manager.GetLanguageProvider("ruby");
   // could also be
   // manager.GetLanguageProviderByFileExtension(".rb");

Now we can ask for the ScriptEngine for our language and ask it to execute a script directly from a file:

ScriptEngine eng = ruby.GetEngine();
eng.ExecuteFile(@"e:\Tests\DlrHost\SampleScript.rb");

We could also first compile the script into a module, manipulate it, and then execute it:

IScriptModule mod = 
   eng.CompileFile(@"e:\Tests\DlrHost\SampleScript.rb""Sample");
// manipulate module
mod.Execute();

Or perhaps you have some piece of script in a string somewhere and you want to execute it directly instead of writing it to a file first:

string script = "puts \"Hello World!\"";
ICompiledCode code = eng.CompileCode(script);
code.Execute();

The engine also allows you to evaluate an expression instead of simply executing the code, which can be useful as well. As you can see, there are many ways you can load and execute code in the DLR.

The short way

Even though "the long way" isn't really lengthy or complex at all, the DLR does provide an easier way: the Microsoft.Scripting.Script class. Script has a bunch of static methods that allow you to compile, evaluate and execute code on the local ScriptDomainManager.

Executing a script file is as easy as:

Script.ExecuteFile(@"e:\Tests\DlrHost\SampleScript.rb");

You can also evaluate a simple expression directly:

int result = (int)Script.Evaluate("ruby""1+8-4");

Notice that if you're not executing/compiling a script from a file, you need to explicitly state what language the expression to execute is written in.

Changing Options

The ScriptDomainManager class has an Options property (an instance of the ScriptDomainOptions class), which contains some interesting switches you can change to diagnose and debug stuff within the DLR and the language provider. (This may go away or change in the feature, at least according to the comments on the source code)

One such option is the ShowASTs property: when set to true, the state abstract syntax tree for the expression/script being evaluated will be dumped to the console. For example, here's the AST output for the last example.

//
// AST unknown_ast
//

.codeblock Object main ( global,)() {
    .var RuntimeFlowControl #rfc (Local,InParameterArray)
    .var Object #self (Local,InParameterArray)

    {
        (.bound #rfc) = (RubyOps.GetRfc)(
            .context,
        );
        (.bound #self) = (RubyOps.GetSelf)(
            .context,
        );
        {
            .return .action InvokeMember -(
                .action InvokeMember +(
                    1
                    8
                )
                4
            );
        }
    }
}

Passing data to Ruby

You can pass data/variables to a script from your host application using the Script.SetVariable() method (if using the script class) or one of the other ways to set values of variables into scopes as necessary.

For IronRuby, however, I believe the only way to do this available is to define a global variable from your DLR hosting application. Doing it, however, isn't very intuitive at this time.

To do this, you need to go "the long way", and explicitly use ScriptDomainManager to get a reference to the Ruby LanguageProvider and its ScriptEngine. Then you need to downcast the ScriptEngine to a variable of type Ruby.Hosting.RubyEngine, and get the execution context to set a global variable there.

A lot of the scope and global variables, once you dig below the Script class, work using symbol IDs instead of names. You can get a symbol ID for the global variable you're about to define using the SymbolTable.StringToId() method.

Let's build an example. First, we'll define a simple class called OurHost:

public class OurHost
{
   public void Callback(string text)
   {
      Console.WriteLine("ruby says: {0}", text);
   }
}

This class will be used by our Ruby script to call back into our C# hosting application.

Now, create the scripting engine and get a reference to the RubyEngine class:

ScriptDomainManager manager = ScriptDomainManager.CurrentManager;
LanguageProvider ruby =
manager.GetLanguageProvider("ruby");
ScriptEngine eng = ruby.GetEngine();
RubyEngine rubyEng = ((RubyEngine)eng);

Now we can add a global variable called $host into the engine execution context and execute our script:

// Add a global variable called $host
SymbolId var = SymbolTable.StringToId("host");
rubyEng.ExecutionContext.GlobalVariables.Add(var, new OurHost());

eng.ExecuteFile(@"e:\Tests\DlrHost\SampleScript.rb");

Our Ruby script, SampleScript.rb would now be able to use the $host variable to call the DLR hosting application:

$host.Callback "calling home!"

Technorati tags: ,


Tomas Restrepo

Software developer located in Colombia. Sr. PFE at Microsoft.