Extend your UWP apps with Lua scripting

Sometimes we need an app to be updated, extended, patched, or customized in unexpected ways and we don’t want to redeploy a new version of it. In the strongly typed world –especially the one in which Reflection.Emit() is not available- that generally requires some kind of scripting infrastructure. The app will be extended by adding flat files to it that may come from anywhere (mail, disk drive, web service, in app purchase, or manual input). So we have to invent a scripting language and build a parser. In my (long) career I’ve done that a couple of times: designing a scripting languages with a parser and a way to serialize and deserialize scripts to and from XML or JSON.

Well, these days are over. Thanks to Lua and MoonSharp.

The Lua scripting language

Lua is a programming language, originally designed for extending software applications to meet an increasing demand for customization. It provides the basic facilities of most procedural programming languages. Complicated or domain-specific features are not included; instead, it includes mechanisms for extending the language, allowing programmers to implement such features. It is used in Adobe products, Mercedes cars, MediaWiki sites, space vehicles, and Angry Birds. There’s a much longer list right here. For more details on the language, check this crash course, a lot of sample code, and even a whole book.

The MoonSharp interpreter

MoonSharp is a Lua interpreter that is written entirely in C#. It runs on .NET 3.5, .NET 4.x, Mono, Xamarin, Unity3D, and PCL compatible platforms. It has a very lightweight API and no external dependencies. The supported syntax covers 99% of Lua 5.2. The intention is NOT to reach 100%, allow me to quote from the MoonSharp rationale:

The purpose is offering scripting facilities to C# apps at the fastest speed possible. The entire purpose of MoonSharp is to sacrifice Lua raw execution speed in order to gain performance on Lua/C# cross calls. Lua serves the dual purpose of being a stand-alone language AND an embeddable scripting language. MoonSharp takes choices which favors the embeddable scenario at the expense of the stand-alone one.

So the focus lies on extending apps, and MoonSharp does this through

  • easy and fast interop with CLR objects,
  • interop with methods, extension methods, overloads, fields, properties and indexers,
  • interop with async methods,
  • easy to use error handling (script errors are thrown as .NET exceptions),
  • and much more.

For those who want an overview: the per-platform list of features that is covered by MoonSharp is right here.

There is a NuGet package available, but it does not yet contain Windows 10 dll’s. So I cloned the source, added the Windows 10 target to the Portable Class Library project, and recompiled everything. The new dll runs on the Universal Windows Platform, so now we can run Lua scripts from apps on the desktop, tablet, phone, x-box, surface hub, hololens, MS drone and MS car. With that new dll in hand, it was time to build a sample app. That app is built as a playground: it allows you to type in and execute some Lua scripts yourself.

Running MoonSharp on the Universal Windows Platform

All you need to do is add a reference to the PCL dll (I put it in the Libs folder of my Visual Studio solution). In the very near future it will most probably suffice to just add the NuGet package to your app.

Evaluating Lua expressions from C#

Here’s the first page of the sample app. It allows you to type in any Lua expression, and evaluate it:

LuaExpression

The source code is strikingly simple (Expression is the input text box, Result is the text block that displays the result):

var script = "return " + Expression.Text;

try
{
    // Run the script and get the result.
    DynValue result = Script.RunString(script);

    Result.Text = result.ToString();
}
catch (Exception ex)
{
    Result.Text = ex.Message;
}

We just create an executable chunk of Lua by prefixing it with “return”, and run it through the Script class. The result is returned as an instance of the DynValue class, which represents all data structures that are marshalled between Lua and C#.

Calling Lua functions from C#

Let’s take it one step further, and define a function with parameters, call it, and get the result back:

LuaFunction

Here’s the source code for this scenario (Chunk is the input text box – Chunk is also the name for the unit of compilation in Lua):

string scriptCode = "function f(a,b) " + Chunk.Text + " end";

var script = new Script();

try
{
    script.DoString(scriptCode);

    DynValue luaFunction = script.Globals.Get("f");

    // Type conversion for the parameters is optional
    DynValue res = script.Call(
                            luaFunction, 
                            DynValue.NewString(ParameterA.Text).CastToNumber(), 
                            Double.Parse(ParameterB.Text));

    // Check the return type.
    if (res.Type != DataType.Number)
    {
        throw new InvalidCastException("Invalid return type: " + res.Type.ToString());
    }

    // Type conversion swallows exceptions.
    Result.Text = res.Number.ToString();
    // Result.Text = res.CastToNumber().ToString();
}
catch (Exception ex)
{
    Result.Text = ex.Message;
}

Again we only need the Script and the DynValue classes: Script to ‘compile’ the script and call it, and DynValue to do the marshalling and type casting.

Calling C# functions from Lua

As already mentioned, MoonSharp focuses on interoperability. You can expose C# methods –even asynchronous ones- to the interpreter and have these called by Lua scripts. The sample app defines the following two methods:

public async static void Say(string message)
{
    var d = new MessageDialog(message);
    await d.ShowAsync();
}

public static string Reverse(string tobereversed)
{
    char[] charArray = tobereversed.ToCharArray();
    Array.Reverse(charArray);
    return new string(charArray);
}
Here’s what happens when you run a Lua script that calls these methods (print is a Lua function that writes to the output stream):

C#Function

Here’s the full script. You expose C# methods to a Lua script by adding a delegate to the global script environment:

string scriptCode = Chunk.Text;

Script script = new Script();

try
{
    // Expose the C# methods.
    script.Globals["say"] = (Action<String>)(Say);
    script.Globals["reverse"] = (Func<String, String>)(Reverse);

    script.DoString(scriptCode);

    Result.Text = "done";
}
catch (Exception ex)
{
    Result.Text = ex.Message;
}

Sharing C# objects to Lua

You can not only register methods to the Lua environment, but also whole instances of classes. The sample app contains a –simple- business object:

public class BusinessObject
{
    private double vatRate = 20.0;

    public double VatRate
    {
        get { return vatRate; }
        set { vatRate = value; }
    }

    public Double CalculateVAT(double amount)
    {
        return amount * vatRate / 100;
    }
}
Here’s a screenshot of a running Lua script that accesses an instance of this class:

ObjectSharing

The types that can be accessed by the Lua environment must be registered – you definitely want to be in control of that. One option is to decorate the class with an attribute:

[MoonSharpUserData]
public class BusinessObject
{
    // ...
}

and then call RegisterAssembly to register all decorated types in the assembly:

UserData.RegisterAssembly();

Unfortunately that didn’t work for me, not in UWP and not in WPF.

Here’s how to explicitly register a type (the attribute is not needed for this to work):

UserData.RegisterType<BusinessObject>();

By the way, here’s the lazy (and potentially dangerous) way to expose the whole C# and .NET environment to Lua:

UserData.RegistrationPolicy = InteropRegistrationPolicy.Automatic;

Sharing instances of these classes is done the same way as sharing methods – by adding these to the global script environment:

var obj = UserData.Create(new BusinessObject());

// Actually, this also works:
// var obj = new BusinessObject();

script.Globals["obj"] = obj;

// Alternatively:
//script.Globals.Set("obj", obj);

The script can now access the obj instance: fields, properties, methods, events. MoonSharp supports extension methods, asynchronous calls, byRef parameters, indexers, operators, … basically everything. You can even alter the visibility of class members to make private members accessible to Lua scripts or hide sensitive public members from it.

Event Handling

For the last scenario in this introduction –and in the sample app- I decorated the BusinessObject with an event and a helper method to call it:

public event EventHandler SomethingHappened;

public void RaiseTheEvent()
{
    if (SomethingHappened != null)
        SomethingHappened(this, EventArgs.Empty);
}

Here’s how a Lua script registers and unregisters its own event handler to this event, and the result of raising the event from within the script and from C#:

EventHandling

The source code should look very familiar now. We just need an extra registration of the EventArgs type. Here’s the complete code for this scenario:

string scriptCode = Chunk.Text;

// Register types to be used in the script.
UserData.RegisterType<BusinessObject>();
UserData.RegisterType<EventArgs>();

Script script = new Script();

try
{
    var obj = UserData.Create(bus);
    script.Globals.Set("obj", obj);

    script.DoString(scriptCode);

    Result.Text = "done";
}
catch (Exception ex)
{
    Result.Text = ex.Message;
}

Honestly: I am impressed with this.

Visual Studio Support

The next step is to dive deeper into the Lua language itself, and start writing some test scripts (.lua files) in Visual Studio. Well, you can extend VS 2012/2013 and 2015 with Babelua. This extension comes with code highlighting, auto-completion, syntax checking, formatting, file previewing, and debugging capabilities. Here’s a screenshot Babelua in action. And yes, MoonSharp executes this script –based on the mentioned crash course- perfectly:

BabeLua

The Source

As usual the sample app is on GitHub. It’s an app for the Universal Windows Platform. The screenshots in this article were taken from the desktop, but it also runs on the phone:

PhoneVersion

Enjoy!

XAML Brewer