Adding Scripting to .NET Applications
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
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);
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;
}
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
|
Sample script environment project, using Jint version 2. |
31/08/2020 | Download | |
ScriptingHost-Jint3.zip
|
Sample script environment project, using Jint version 3. |
31/08/2020 | Download |
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?
Comments
Kvapu
#
I'd also look into PowerShell SDK and ClearScript.
Richard Moss
#
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
#
Hmmmm... could we do something similar with Python.NET ?
Richard Moss
#
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