Spark += IronPython
programming, python, spark, tech November 18th, 2008
Something interesting coming down the line in dotnet is a newly enhanced support for dynamic languages. Those are all build on a dynamic language runtime (DLR) that’s a fast and self-optimizing execution engine build on the CLR. Specific scripting languages are parsed and realized as DLR expressions - first on the list are IronPython and IronRuby. I’m pretty sure DLR support for javascript is in the mix also. Yep, dynamic language runtime will include Managed JScript as well as Visual Basic .NET version 10.
To see what was involved in hosting a dynamic language I integrated Python as a language option in the Spark view engine. It’s not bad at all - in fact you can use IronPython in Spark now if you prefer that language. The hard part was figuring out what sort of angle to take presenting the view’s context to the language. With the default C# all of the helpers, viewdata, context variables, and current TextWriter Output are implemented as public members of the base class. That could be done in IronPython, but in that language it would mean “self.” would need to be in front of every reference to those members. Not really optimal.
In the end, after spending a little time trying to get inside Python’s head, it looked like the Tao of Python approach to a view would be to skip the object-oriented mechanisms altogether and instead create the view as flat procedural code that relies on global variables.
What does that look like? Looks a lot like Spark with csharp actually.
<for each="c in categories">
<h3>${H(c.Name)}</h3>
<ul>
<li each="p in c.Products" class="alt?{pIndex%2}">${H(p.Name)}</li>
</ul>
</for>
Two concerns come up right away with this… For one thing it would be ideal to parse and compile the script for a view one time and re-execute it repeatedly. The other concern is how you would introduce all of the view class methods and properties (along with all of the items added to the view data) as global variables.
The first part couldn’t be easier. The scripting runtime (the overall environment) and python scripting engine (the language-specific component) can be both be created with the single statement Python.CreateEngine(). There is also an abstract way of creating language engines, but if you don’t mind being tightly coupled the utility class named Python lets you cut right to the chase. The resulting ScriptEngine can parse a string containing script into a ScriptSource object, and that object can turn itself into a CompiledCode object.
Here’s a class that manages that process. It caches an instance of CompiledCode object for each distinct class of view. The view.ScriptSource string property provides the python script that was generated from the spark file. The view receives a reference to the CompiledCode which it will execute.
public class PythonEngineManager
{
private readonly ScriptEngine _scriptEngine;
private readonly Dictionary<Guid, CompiledCode> _compiledViewScripts =
new Dictionary<Guid, CompiledCode>();
public PythonEngineManager()
{
_scriptEngine = Python.CreateEngine();
}
public ScriptEngine ScriptEngine
{
get { return _scriptEngine; }
}
public void InstanceCreated(IScriptingSparkView view)
{
CompiledCode compiledCode;
if (!_compiledViewScripts.TryGetValue(
view.GeneratedViewId,
out compiledCode))
{
var scriptSource = ScriptEngine.CreateScriptSourceFromString(
view.ScriptSource, SourceCodeKind.File);
compiledCode = scriptSource.Compile();
_compiledViewScripts.Add(view.GeneratedViewId, compiledCode);
}
view.CompiledCode = compiledCode;
}
public void InstanceReleased(IScriptingSparkView view)
{
}
}
The second part where you provide a class member and dictionary values as global variables is a little bit tricker. In the end it’s also connected to what you need to do to re-execute compiled code on various threads, which is to say you create a new ScriptScope each time you call the CompiledCode’s Execute method. The nice part is when you create that ScriptScope you can provide a class which be given the chance to fill in the values of any global symbols the script may reference.
void RenderView(IScriptingSparkView view, ScriptEngine scriptEngine)
{
var scope = scriptEngine.CreateScope(new ViewSymbolDictionary(view));
view.CompiledCode.Execute(scope);
}
And the following ViewSymbolDictionary class acts as the glue to expose view data as global values.
public class ViewSymbolDictionary : CustomSymbolDictionary
{
private readonly IScriptingSparkView _view;
private readonly Type _viewType;
public ScriptingViewSymbolDictionary(IScriptingSparkView view)
{
_view = view;
_viewType = view.GetType();
}
public override SymbolId[] GetExtraKeys()
{
throw new System.NotImplementedException();
}
protected override bool TrySetExtraValue(SymbolId key, object value)
{
// setting a global value will assign view properties or
// fields if available
var property = _viewType.GetProperty(key.ToString());
if (property != null)
{
property.SetValue(_view, value, null);
return true;
}
var field = _viewType.GetField(key.ToString());
if (field != null)
{
field.SetValue(_view, value);
return true;
}
return false;
}
protected override bool TryGetExtraValue(SymbolId key, out object value)
{
// properties or fields will be returned if available
var property = _viewType.GetProperty(key.ToString());
if (property != null)
{
value = property.GetValue(_view, null);
return true;
}
var field = _viewType.GetField(key.ToString());
if (field != null)
{
value = field.GetValue(_view);
return true;
}
// global symbols may refer to public methods, in which case the
// value returned is a delegate to the method
var method = _viewType.GetMethod(key.ToString());
if (method != null)
{
var parameterTypes = method.GetParameters().Select(
p => p.ParameterType).ToList();
parameterTypes.Add(method.ReturnType);
// the scripting runtime has a nice little helper which takes the
// Type array and returns the type of the correct Func generic,
// this type can be used to instantiate the delegate
value = Delegate.CreateDelegate(
CompilerHelpers.MakeCallSiteDelegateType(
parameterTypes.ToArray()),
_view,
key.ToString());
return true;
}
// Finally call an abstract method on the view. The override on this
// method will return true if the named value could be pulled out of
// the view data dictionary
if (_view.TryGetViewData(key.ToString(), out value))
return true;
value = null;
return false;
}
}
November 18th, 2008 at 12:35 pm
Nice, great work!
I am looking forward to playing with the DLR, the IDynamicObject and IMetaObject interfaces creates some intersting possibilites.
November 18th, 2008 at 1:46 pm
Working with dynamic languages definitely leaves you with a different perspective. It’s remarkable how functional the CLR objects are as they’re exposed to the DLR.
At first I was thinking the IMetaObject or IDynamicObject would be necessary, but the CustomSymbolDictionary was a better fit in this case. Good thing too, the IMetaObject is especially mind bending.
Looking at IronRuby next. Even though they’re both DLR it’s starting to look like the mechanisms for integration won’t have much meaningful overlap.