This content has moved - please find it at https://devblog.cyotek.com.

Although these pages remain accessible, some content may not display correctly in future as the new blog evolves.

Visit https://devblog.cyotek.com.

Adding Scripting to .NET Applications

The demonstration application converting an image to greyscale

Adding scripting to your application can be a good way of providing your power users with advanced control or for allowing it to be extended in ways you didn't anticipate.

Our Color Palette Editor software allows the use of dynamic templates via the use of the Liquid template language by Shopify. This is quite powerful in its own right and I will likely use it again in future for other templating activities. But as powerful as it is, it cannot replace scripting.

I also experimented with adding macro support to our Gif Animator, but given I was too busy making a mess of the program I never got around to releasing the macro extension.

Recently I was working on a simulator sample and ended up integrating part of the aforementioned macro solution so I could set up the simulation via a script. I thought that was interesting enough in its own right to deserve a post.

However, I am not interesting in writing a scripting language. (Actually, that's not entirely true... as soon as a print version of Crafting Interpreters is available I plan on trying to implement it in C#.) As most of the world seems to run on JavaScript these days, it is reasonable to use this as a base. And fortunately, there is a decent JavaScript interpreter available for .NET, namely Jint.

Using Jint

Note: This article is written on the assumption that you are using Jint version 3. However, the basic code will also work with version 2, albeit with a noticeable performance decrease. Unfortunately, there are breaking changes between version 2 and 3 in how you parse the JavaScript so some of the examples in this article may not work directly in Jint 2.

The demonstration that accompanies this article has variants for both Jint versions.

After installing the Jint NuGet package, you could simply execute a script like this

    var engine = new Engine()
        .SetValue("log", new Action<object>(Console.WriteLine))
        ;

    engine.Execute(@"
      function hello() {
        log('Hello World');
      };

      hello();
    ");

(Example code from the Jint project page)

This is pretty powerful stuff. The SetValue method can be used to add .NET object instances to be accessible from JavaScript, or you could define function methods that can be executed by JavaScript.

You can selectively include either the full .NET Framework or a list of "safe" assemblies - this is done via the AllowClr method when creating the engine.

  var Engine = new Engine(options => options.AllowClr()); // full CLR

  var engine = new Engine(cfg => cfg
      .AllowClr(typeof(Bar).Assembly)
  ); // partial clr
  var file = new System.IO.StreamWriter('log.txt');
  file.WriteLine('Hello World !');
  file.Dispose();

  var Foo = importNamespace('Foo');
  var bar = new Foo.Bar();
  log(bar.ToString());

(Example code from the Jint project page)

It is also possible to add specific types to the engine. These can either then be directly instantiated from a script, or static values accessed.

  var engine = new Engine(); // no explicit CLR access

  engine.SetValue("color", TypeReference.CreateTypeReference(engine, typeof(System.Drawing.Color)));
  var c = color.DarkGoldenrod;

While the Execute method can be used to load and run JavaScript, you can also call individual functions via the Invoke method. This could be handy for allowing extensibility via scripts.

  var add = new Engine()
      .Execute("function add(a, b) { return a + b; }")
      .GetValue("add")
      ;

  add.Invoke(1, 2); // -> 3

(Example code from the Jint project page)

Safe and secure

By default, Jint doesn't provide access to the full .NET API, and so scripts should be incapable of performing malicious actions. As noted in the previous section you can easily add CLR assemblies or custom objects which could then allow for malicious actions. You should consider how much functionality you wish to expose via scripts and risk assess your application's needs.

When creating an Engine instance, Jint also allows you to specify constraints such as how much memory can be used, or program size, as well as allowing script execution to be cancelled.

var engine = new Engine(options => {

  // Limit memory allocations to MB
  options.LimitMemory(4_000_000);

  // Set a timeout to 4 seconds.
  options.TimeoutInterval(TimeSpan.FromSeconds(4));

  // Set limit of 1000 executed statements.
  options.MaxStatements(1000);

  // Use a cancellation token.
  options.CancellationToken(cancellationToken);
}

(Example code from the Jint project page)

It is also possible to define your own constraints but this is something I haven't looked into yet.

Creating a base

As I don't really want my applications to have to know about Jint or how it works, I'm going to wrap it around a helper class. This class will take care of managing the scripting engine and providing some common functionality.

public abstract class ScriptEnvironment
{
  bool Interactive { get; set; }
  bool SuppressErrors { get; set; }

  void AddFunction(string name, Delegate value);
  void AddType(string name, Type type);
  void AddValue(string name, object value);

  object Evaluate(string script);
  object Execute(string script);
  object Invoke(string name, params object[] arguments);
  void Load(string script);
  
  abstract void ClearScreen();
  abstract void ShowAlert(string message);
  abstract bool ShowConfirm(string message);
  abstract string ShowPrompt(string message, string defaultValue);
  abstract void WriteLine(string value);
}

I'm making it abstract as the interactivity features will need to be implemented separately for each application type - e.g. once for console applications and once for applications with a GUI.

Initialising the engine

I don't want the JavaScript engine to be created until it is required, so any method that needs the engine to be present will first call InitializeEngine to ensure the instance is created.

I also have a virtual InitializeEnvironment method that is called once after the engine is initialised, allowing subclasses to configure the engine appropriately, for example by making certain types available, or loading objects for scripting use.

  private void InitializeEngine()
  {
    if (_engine == null)
    {
      _engine = new Engine();

      this.InitializeEnvironment();
    }
  }

  protected virtual void InitializeEnvironment()
  {
    this.AddFunction("print", new Action<object>(this.WriteLine));
    this.AddFunction("log", new Action<object>(this.WriteLine));
    this.AddFunction("cls", new Action(this.ClearScreen));

    // interactive functions
    this.AddFunction("alert", new Action<object>(this.ShowAlert));
    this.AddFunction("confirm", new Func<object, bool>(this.ShowConfirm));
    this.AddFunction("prompt", new Func<object, object, string>(this.ShowPrompt));
  }

It will always define basic output methods log and print (both will perform the same action), and also a cls method. I also define three functions to mirror JavaScripts interactive aspects, naming alert, confirm and prompt.

Note: Although the wrapper class has an Interactive property to enable the use of interactive functions, they will still always be defined in the engine so scripts don't crash if interactions are disabled.

Loading a script

Note: This code in this section applies to Jint 3 only. While you can perform the same actions using Jint 2, the API is different.

The easiest way of getting a script into Jint is to call its Execute method and pass in a string containing your JavaScript. However, if you want to perform any sort of examination of the source, for example to check if a given function exists, then you need to parse the code.

Fortunately, this isn't something you need to do manually as Jint 3 uses the Esprima .NET library for this, and we can too.

  JavaScriptParser parser;
  Script program;

  parser = new JavaScriptParser(script);
  program = parser.ParseScript();

The Script object has a ChildNodes property which provides a full Abstract Syntax Tree (AST) for your script, allowing you to query the entire program.

For example, consider the following script. Short and simple?

function main()
{
  for(var i = 0; i < picture.Length; i++)
  {
    let current = picture.getPixel(i);
    let grayscale = toGrayScale(current);

    picture.setPixel(i, grayscale);
  }
}

function toGrayScale(c)
{
  let red = c.R;
  let green = c.G;
  let blue = c.B;
  let gray = red * 0.3 + green * 0.59 + blue * 0.11;

  return color.FromArgb(gray, gray, gray);
}

This is condensed example of the AST generated for the above script

FunctionDeclaration [main()]
  BlockStatement
    ForStatement
      VariableDeclaration [Var]
        VariableDeclarator [i]
          Literal [0]
      BinaryExpression [Less]
        Identifier [i]
        MemberExpression [picture.Length]
      UpdateExpression
        Identifier [i]
      BlockStatement
        VariableDeclaration [Let]
          VariableDeclarator [current]
            CallExpression
              MemberExpression [picture.getPixel]
              Identifier [i]
        VariableDeclaration [Let]
          VariableDeclarator [grayscale]
            CallExpression
              Identifier [toGrayScale]
              Identifier [current]
        ExpressionStatement
          CallExpression
            MemberExpression [picture.setPixel]
            Identifier [i]
            Identifier [grayscale]
FunctionDeclaration [toGrayScale(c)]
  BlockStatement
    VariableDeclaration [Let]
      VariableDeclarator [red]
        MemberExpression [c.R]
    VariableDeclaration [Let]
      VariableDeclarator [green]
        MemberExpression [c.G]
    VariableDeclaration [Let]
      VariableDeclarator [blue]
        MemberExpression [c.B]
    VariableDeclaration [Let]
      VariableDeclarator [gray]
        BinaryExpression [Plus]
          BinaryExpression [Plus]
            BinaryExpression [Times]
              Identifier [red]
              Literal [0.3]
            BinaryExpression [Times]
              Identifier [green]
              Literal [0.59]
          BinaryExpression [Times]
            Identifier [blue]
            Literal [0.11]
    ReturnStatement
      CallExpression
        MemberExpression [color.FromArgb]
        Identifier [gray]
        Identifier [gray]
        Identifier [gray]

Using the AST, you could examine the script before you allowed it to be ran. For example, you could check for the presence of a function named main, and if found, invoke that directly. I suppose you could also use it to try and ensure a script is safe, but given the script could be obfuscated I'm not sure how effective that would be.

Once you have a Program object, you can load this into your engine instance by calling its Execute method the same as you would with a string.

  public void Load(string script)
  {
    this.Load(script, out Script _);
  }

  private void Load(string script, out Script program)
  {
    program = null;

    try
    {
      program = new JavaScriptParser(script).ParseScript();

      // TODO: Validate the program, e.g. check for eval, etc

      this.InitializeEngine();

      _engine.Execute(program);
    }
    catch (Exception ex)
    {
      this.HandleException(ex);

      if (!_suppressErrors)
      {
        throw;
      }
    }
  }

Ideally, after parsing the script I would check it to ensure that only functions are present and no global statements that will be executed. After all, it is a little odd that in order to load a script you actually have to execute it.

The try blocks are a bit off putting too, especially as I will be doing the same "pattern" elsewhere in the class. Sometimes when confronted with this I tend to have a method that accepts an Action and therefore only have the boilerplate in one place, however to keep this example simple (if slightly more verbose), I have choose to keep it as is.

Script execution

Our scripting object will expose three different execution functions - Execute, Evaluate and Invoke.

The first method, Execute, will execute-load a script and then search for a method named main. If one is found, it will invoke this. My intended use case for this particular method is for application plug-ins.

  public object Execute(string script)
  {
    object result;

    this.Load(script, out Script program);

    if (ScriptEnvironment.HasMainFunction(program) && !ScriptEnvironment.HasMainCaller(program))
    {
      result = this.Invoke(MainFunctionName);
    }
    else
    {
      result = _engine.GetCompletionValue().ToObject();
    }

    return result;
  }

After the script has executed, I get any completion value, convert it to a .NET object and then return it.

The second method, Evaluate is intended for read, execute, print, loop (REPL) scenarios... for example an Immediate style window, or a scripting command interface. It simply execute-loads the specified script and then returns any result.

  public object Evaluate(string script)
  {
    this.Load(script);

    return _engine.GetCompletionValue().ToObject();
  }

The final method, Invoke is unique in that it assumes that Load, Execute or Evaluate have been previously called to load script into the engine. It will then attempt to execute a named function.

  public object Invoke(string name)
  {
    return this.Invoke(name, _defaultArguments);
  }

  public object Invoke(string name, params object[] arguments)
  {
    object result;

    try
    {
      this.InitializeEngine();

      result = _engine.Invoke(name, arguments).ToObject();
    }
    catch (Exception ex)
    {
      result = null;

      this.HandleException(ex);

      if (!_suppressErrors)
      {
        throw;
      }
    }

    return result;
  }

Displaying output

In many cases, it would be beneficial for scripts to be able to output content, for whatever reason. While I'm not going to try and reproduce JavaScript's console object, having a log function is of great help. In the initialisation section above, I define both print and log as aliases for outputting content.

Most of the functions that must be overridden to provide functionality, be it logging or interactivity, require a .NET string. Therefore we need some code to transform script engine values into a .NET string, including allowing literal strings for null or undefined values.

  private string GetValueString(object value, bool useLiterals)
  {
    string result;

    if (value is JsValue jsValue)
    {
      result = ScriptEnvironment.GetValueString(jsValue, useLiterals);
    }
    else if (value is null)
    {
      result = useLiterals ? "null" : null;
    }
    else
    {
      result = value.ToString();
    }

    return result;
  }

  private static string GetValueString(JsValue jsValue, bool useLiterals)
  {
    string result;

    switch (jsValue.Type)
    {
      case Types.String:
        result = jsValue.AsString();
        break;

      case Types.Undefined:
        result = useLiterals ? "undefined" : null;
        break;

      case Types.Null:
        result = useLiterals ? "null" : null;
        break;

      case Types.Boolean:
        result = jsValue.AsBoolean().ToString();
        break;

      case Types.Number:
        result = jsValue.AsNumber().ToString();
        break;

      case Types.Object:
        result = jsValue.ToObject().ToString();
        break;

      case Types.None:
        result = string.Empty;
        break;

      default:
        result = jsValue.AsString();
        break;
    }

    return result;
  }

With these helpers in place, we can define object-based methods that are bound to the Jint Engine instance, and then translate these into .NET strings before calling the implementation specific overrides.

    private void WriteLine(object value)
    {
      this.WriteLine(this.GetValueString(value, true));
    }

Interactivity

A example script demonstrating the basic interactive features

Although I'm not going to go out of my way to add a lot of interactivity to the script engine, mirroring JavaScript's alert, confirm and prompt methods would be of great use for some types of scripts.

However, as not all hosts might not support interactivity, or you may wish to disable it on an ad-hoc basic, I have added an Interactive property to the base class. When set to true, the interactive methods will work as expected. When false, no user interface elements will be displayed, and, in the case of functions, default values returned.

  private void ShowAlert(object message)
  {
    if (_interactive)
    {
      this.ShowAlert(this.GetValueString(message, false));
    }
  }

  private bool ShowConfirm(object message)
  {
    return _interactive && this.ShowConfirm(this.GetValueString(message, false));
  }

  private string ShowPrompt(object message, object defaultValue)
  {
    return _interactive
        ? this.ShowPrompt(this.GetValueString(message, false), this.GetValueString(defaultValue, false))
        : null;
  }

For a WinForms GUI application, the following overrides can be used to used to present the UI.

  protected override void ShowAlert(string message)
  {
    MessageBox.Show(message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
  }

  protected override bool ShowConfirm(string message)
  {
    return MessageBox.Show(message, Application.ProductName, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes;
  }

  protected override string ShowPrompt(string message, string defaultValue)
  {
    return InputDialog.ShowInputDialog(_logControl.FindForm(), message, Application.ProductName, defaultValue);
  }

Thread safety

When I first added scripting to an application, it ran in the UI thread. For this demonstration, I decided to run it on a separate thread using a BackgroundWorker. I don't really know if Jint is inherently thread safe or not, but so far I haven't had any problems with this approach.

Except of course, for when I want to update a WinForms control as this isn't possible to do on anything but the UI thread. In addition, the ShowPrompt example above, if called from another thread, will neither be modal nor positioned correctly.

Fire and forget

In the simplest of cases, you can check if the call is occurring on a different thread by using the Control.InvokeRequired property. If this is false, the code is executing on the UI thread and it is safe to continue. If true, it is on a different thread and so you need to use the Control.Invoke method to execute on the UI thread instead.

In the following example, the WriteLine method will either update a text box if it is safe to do so, or will invoke itself on the UI thread if not.

  protected override void WriteLine(string value)
  {
    if (_logControl.InvokeRequired)
    {
      _logControl.Invoke(new Action<string>(this.WriteLine), value);
    }
    else
    {
      _logControl.AppendText(value + Environment.NewLine);
    }
  }

Returning a result

When you need to return a result, it is a little more complicated, but not overly so. As before, I check InvokeRequired to see if the code is on the UI thread and if not I set up an asynchronous operation using Control.BeginInvoke, using an IAsyncResult instance to wait for completion and access the result via Control.EndInvoke.

This API is old, introduced long before .NET's Task class. Unfortunately as WinForms has been stagnating since 2005 or so, the chances of Microsoft modernising the API seem slim.

  protected override string ShowPrompt(string message, string defaultValue)
  {
    string result;
    Form owner;

    owner = _logControl.FindForm();

    if (owner.InvokeRequired)
    {
      Func<string, string, string> caller;
      IAsyncResult asyncResult;

      caller = this.ShowPromptDialog;

      asyncResult = owner.BeginInvoke(caller, message, defaultValue);
      asyncResult.AsyncWaitHandle.WaitOne();

      result = (string)owner.EndInvoke(asyncResult);

      asyncResult.AsyncWaitHandle.Close();
    }
    else
    {
      result = InputDialog.ShowInputDialog(_logControl.FindForm(), message, Application.ProductName, defaultValue);
    }

    return result;
  }

  private string ShowPromptDialog(string message, string defaultValue)
  {
    return InputDialog.ShowInputDialog(_logControl.FindForm(), message, defaultValue);
  }

Stepping through the code when an invoke is required - first, I get a delegate representing the function call I need to make.

Next, I begin an asynchronous operation by calling Control.BeginInvoke, passing in the delete to execute and the parameters it requires. This returns an IAsyncResult instance which I capture.

This result provides access to a WaitHandle which in turn provides a WaitOne method. By calling this, our non-UI thread will pause until it receives a signal indicating that the async operation has completed.

Once the signal has been received and the code continues execution, we call Control.EndInvoke, passing in the async result we captured earlier. The EndInvoke method will return the result of the function call which I then cast appropriately.

Finally, I close the WaitHandle to cause its resources to be released.

There's quite a lot of boiler-plate code involved, I should probably create extension methods to simplify this for future work.

To multi-thread or not to multi-thread

Depending on what functionality you expose to your scripts, running on multiple threads could be a significant issue as if the UI itself isn't thread aware, then any calls which interact with the UI will either have unexpected results or throw the dreaded Cross-thread operation not valid exception.

Other languages

Although I initially created my scripting experiment using JavaScript, other languages are available. Previously I've used Lua via the VikingErik.LuaInterface package (which I only remember because of the name!). I'd probably use NLua for new work.

Then there is Python. This seems to be becoming more popular (or maybe always was and I was unaware!). IronPython is a .NET implementation of Python and I will probably look into this in future. Last time I took note of this library, it had just been dropped by Microsoft (along with IronRuby), although unlike the latter it appears to have landed on its feet.

The sample application

The demonstration program included with this article is a pretend drawing application. I say pretend as I haven't included anything other than a script interface, but you can "draw" with this, and it was a quick and easy way of showing the functionality.

The application defines a PixelPicture class, an instance of which is added to the scripting engine via the picture variable. This exposes a number of methods such as plot, drawLine, drawCircle etc to perform drawing methods.

It also defines an application variable which can be used to manipulate the host application, for example to change the window caption. Nothing exotic, but simple ideas on how you could add scripting to your own applications.

The code for plotting the pixels for lines, circles, ellipses and curves uses Bresenham's line algorithm, with the C# implementation coming from easy.Filter's The Beauty of Bresenham's Algorithm page.

The flood fill implementation was taken from RosettaCode.

While I have provided versions of the sample application using both Jint 2 and Jint 3, I upgraded to Jint 3 after starting to write this article, therefore it is missing refactoring and enhancements made during the course of writing this piece.

You can download the sample projects from the links on this article, or visit the GitHub repository for the latest version.

For example, the following script will draw a smiley.

var size = 64
var half = size / 2;
var quarter = size / 4;
var eighth = size / 8;
var sixteenth = size / 16;

picture.Width = size;
picture.Height = size;

picture.clear(color.White);

picture.drawCircle(half, half, half - 1, color.Goldenrod);
picture.floodFill(half, half, color.White, color.Yellow);

picture.drawCircle(quarter * 1.5, quarter * 1.25, eighth, color.Black);
picture.floodFill(quarter * 1.5, quarter * 1.25, color.Yellow, color.White);
picture.drawCircle(quarter * 1.5, quarter * 1.5, sixteenth, color.Black);
picture.floodFill(quarter * 1.5, quarter * 1.5, color.White, color.Black);

picture.drawCircle(quarter * 2.5, quarter * 1.25, eighth, color.Black);
picture.floodFill(quarter * 2.5, quarter * 1.25, color.Yellow, color.White);
picture.drawCircle(quarter * 2.5, quarter * 1.5, sixteenth, color.Black);
picture.floodFill(quarter * 2.5, quarter * 1.5, color.White, color.Black);

picture.drawEllipse(quarter, quarter * 2.25, half, quarter, color.Black);
picture.floodFill(half, quarter * 3, color.Yellow, color.Black);

picture.drawEllipse(quarter, quarter * 1.75, half, quarter, color.Yellow);
picture.floodFill(half, half * 1.25, color.Black, color.Yellow);

A example of my terrible drawing skills making a smiley

Or, for something more dynamic, the following script will plot the results of sine and cosine.

var sy1;
var sy2;
var cy1;
var cy2;

var width = 128
var height = 64;
var third = height / 3;
var lineColor = color.FromArgb(102, 51, 153);
var lineColor2 = color.FromArgb(153, 51, 102);

picture.Width = width;
picture.Height = height;
picture.clear(color.White);

sy2 = third
cy2 = third * 2;

for(var i = 0; i < width; i++)
{
  sy1 = third + Math.sin(i) * 10;
  cy1 = (third * 2) + Math.cos(i) * 10;

  picture.drawLine(i, sy2, i + 1, sy1, lineColor);
  picture.drawLine(i, cy2, i + 1, cy1, lineColor2);

  sy2 = sy1;
  cy2 = cy1;
}

Plotting a wave using sine and cosine

Although this demonstration project is a little contrived, if you add scripting support to your application you may find it be a very valuable feature.

Downloads

Filename Description Version Release Date
ScriptingHost-Jint2.zip
  • sha256: 1a5c1588ba9e8f2390f5f3bd41f0c7f4b8c86feb65c9cf1b460aca49488addfc

Sample script environment project, using Jint version 2.

31/08/2020 Download
ScriptingHost-Jint3.zip
  • sha256: d68e1b14e0da47d405e8cda8439a07ff45dff6adcdd992397fa190293e76edf6

Sample script environment project, using Jint version 3.

31/08/2020 Download

About The Author

Gravatar

The founder of Cyotek, Richard enjoys creating new blog content for the site. Much more though, he likes to develop programs, and can often found writing reams of code. A long term gamer, he has aspirations in one day creating an epic video game. Until that time, he is mostly content with adding new bugs to WebCopy and the other Cyotek products.

Leave a Comment

While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?

Styling with Markdown is supported

Comments

Gravatar

Kvapu

# Reply

I'd also look into PowerShell SDK and ClearScript.

Gravatar

Richard Moss

# Reply

Hello,

Thanks for your comment! ClearScript I wasn't aware of, that would be worth a look so thank you for pointing that out. My initial thoughts were a) I don't think JScript has been updated for a very long time, and b) I can't imagine the V8 engine is particular small or easy to deploy. Oh, on reading their deployment guide you need to deploy C++ runtimes as well as build V8 from source yourself.

PowerShell though... I'm not so sure. A few years back I actually implemented a PowerShell host... this is going back quite a way, so was one of the earlier versions of PowerShell. And while it worked, I was not impressed with it at all - the performance was absolutely dire, mostly in how long it took to instantiate PowerShell if I recall correctly. I don't know if that is still the case but there isn't much of a chance I would add that sort of slowness deliberately to an application.

I think for me personally, Jint is "good enough" at this point in time. It is easy to deploy, is reasonably sized and doesn't pull in another dozen packages. Still, both these suggestions are worth keeping in mind, so thank you again for the comment.

Regards;
Richard Moss

Dax

# Reply

Hmmmm... could we do something similar with Python.NET ?

Gravatar

Richard Moss

# Reply

Hello,

Thanks for commenting! This library, from first glance, seems to have better scoping options than Jint (which I didn't cover in this article) but there's no reason at all why you couldn't write your own wrapper around it for providing your own common functionality. I think I should probably read a primer on Python and then adapt the sample project to use it!

Regards; Richard Moss