Home

Awesome

jsrt-dotnet

A library for accessing the Chakra and ChakraCore JavaScript Runtime hosting interface from the .NET Framework. It is inspired by jsrt-winrt but is not directly compatible.

Why do I care? If you want to extend your .NET application, or allow your users to do so, by writing some JavaScript at runtime, this allows you to do so without paying the cost of loading an HTML engine with it. And, it's far more convenient than the programming model supported by the HTML engine. (Unless you're using HTML rendering within your app, in which case using the HTML engine is pretty efficient for that).

What is special about jsrt-dotnet?

This project aims to create as seamless a bridge between your JavaScript and .NET code as possible. .NET objects can be directly exposed to JavaScript, and JavaScript objects can be accessed from C# using dynamic or via normal early binding. It also aims to be an accurate representation of the JavaScript type system from .NET.

Getting started

As an example, let's create a simple host function called Echo:

static JavaScriptValue Echo(JavaScriptEngine engine, bool construct, JavaScriptValue thisValue, IEnumerable<JavaScriptValue> arguments)
{
    string fmt = arguments.First().ToString();
    object[] args = (object[])arguments.Skip(1).ToArray();
    Console.WriteLine(fmt, args);
    return engine.UndefinedValue;
}

The function must return a JavaScriptValue (because all JavaScript functions return something - even if that something is undefined). The parameters to the function represent the things that the JavaScript code is calling:

The function expects at least a single parameter to be passed, and will accept multiple other parameters. It then uses the Console.WriteLine(string, params object[]) overload to write a formatted string. We then add this function to the global object:

using (var runtime = new JavaScriptRuntime())
using (var engine = runtime.CreateEngine())
using (var context = engine.AcquireContext())
{
    engine.SetGlobalFunction("echo", Echo);

    // TODO: Call echo
}

So what we do here are:

All that's left is to run some script that will call it:

using (var runtime = new JavaScriptRuntime())
using (var engine = runtime.CreateEngine())
using (var context = engine.AcquireContext())
{
    engine.SetGlobalFunction("echo", Echo);

	var fn = engine.EvaluateScriptText(@"(function() {    
	echo('{0}, {1}!', 'Hello', 'World');
})();");
    fn.Invoke(Enumerable.Empty<JavaScriptValue>());
}

If you run this from a console, you'll see

Hello, World!

output on the screen.

Using dynamic

Let's start getting crazy.

using (var runtime = new JavaScriptRuntime())
using (var engine = runtime.CreateEngine())
using (var context = engine.AcquireContext())
{
    engine.SetGlobalFunction("echo", Echo);
    dynamic global = engine.GlobalObject;
    global.hello = "Hello";
    global.world = "world";

	var fn = engine.EvaluateScriptText(@"(function() {    
        echo('{0}, {1}!', hello, world);
    })();");
    fn.Invoke(Enumerable.Empty<JavaScriptValue>());
}

What happened here?

The dynamic keyword instructs the C# compiler to perform runtime late binding on things that are performed against the dynamic thing. The engine's GlobalObject property is a JavaScript Object (it's the thing that provides all of those handy things like ArrayBuffer and Math). That Object, like any other JavaScript object, has properties, and those properties are accessible via the normal name resolution rules per normal JavaScript semantics.

Because JavaScript Objects are property bags, we can assign anything to them. What happens under the covers is:

  1. JavaScriptObject derives from JavaScriptValue, which in turn derives from DynamicObject, provided in the .NET base class library
  2. The C# language bindings call JavaScriptObject.TrySetMember, passing information about the operation, namely, that the name is hello and that the set-member operation is case-sensitive. (Because JavaScript is case-sensitive, we ignore this flag, and always treat it as case-sensitive).
  3. That operation converts the right-hand side value (in this case, a C# string of "Hello") to its JavaScript equivalent, a JavaScriptValue, and calls JavaScriptObject.SetPropertyByName(string, JavaScriptValue) method.
  4. When the script text is executed, the environment record has bindings for hello and world as properties of the global, so they're passed back out to C# as JavaScriptValues in the arguments. They get converted back to strings, without changing the code.

I can blow your mind even more. Without even calling script, I can call into the script engine:

using (var runtime = new JavaScriptRuntime())
using (var engine = runtime.CreateEngine())
using (var context = engine.AcquireContext())
{
    engine.SetGlobalFunction("echo", Echo);
    dynamic global = engine.GlobalObject;
    
    global.echo("{0}, {1}, from dynamic.", "Hello", "world");
}

Here, the C# dynamic binder calls into JavaScriptObject.TryInvokeMember, which resolves the property, casts it to a JavaScriptFunction, and then calls it. That function happens to be a host function, so it calls back into C#, using the same round-trip behaviors as shown previously.

Accessing CLR objects from script

CLR objects can be added to and accessed via script. Wherever possible, we try to preserve object behavioral semantics across the script-host boundary. That is, if you have a C# object that you add to the script engine, and then mutate that object from script, those changes will be reflected in the C# object.

** Important: ** .NET objects to JavaScript are still an incomplete and experimental feature. The following outlines how this feature is intended to function, but may not be implemented as described.

To convert a host object to a JavaScript representation, call myJavaScriptEngine.Converter.FromObject, which will return a JavaScriptValue.

var pt = new Point3D { X = 18, Y = 27, Z = -1 };
engine.SetGlobalVariable("pt", engine.Converter.FromObject(pt));

For any type that isn't just represented by a JavaScript primitive, we attempt to follow this algorithm:

In addition to providing these via objects sent into the engine directly, the developer can add a constructor to the global namespace via the AddTypeToGlobal function:

engine.AddTypeToGlobal<Point3D>();

Instead of providing an instance of an object to the engine, this example creates a function named Point3D on the global object, representing the Point3D public constructors.

Example

Given the following type definition:

public class Point
{
    public double X
    {
        get;
        set;
    }
    
    public double Y
    {
        get;
        set;
    }
    
    public override string ToString()
    {
        return $"({X}, {Y})";
    }
}

public class Point3D : Point
{
    public double Z
    {
        get;
        set;
    }
    
    public override string ToString()
    {
        return $"({X}, {Y}, {Z})";
    }
}

If the Point3D type is added to the global object, the equivalent code is executed:

(function(global, createPoint, getX, setX, getY, setY, pointToString, createPoint3D, getZ, setZ, point3DToString) {
	function Point() {
		if (!(this instanceof Point))
			return new Point(arguments);

		createPoint.call(this);
	}
	Object.defineProperty(Point.prototype, 'X', {
		get: getX,
		set: setX, 
		enumerable: true
	});
	Object.defineProperty(Point.prototype, 'Y', {
		get: getY, 
		set: setY,
		enumerable: true
	});
	Point.prototype.toString = pointToString;

	function Point3D() {
		if (!(this instanceof Point3D))
			return new Point3D(arguments);

		createPoint3D.call(this);
	}
	Point3D.prototype = Object.create(Point.prototype);
	Point3D.prototype.constructor = Point3D;
	Object.defineProperty(Point3D.prototype, 'Z', {
		get: getZ,
		set: setZ,
		enumerable: true
	});
	Point3D.prototype.toString = point3DToString;

	global.Point3D = Point3D;
})([native method representations]);

The work to project Point in this example is preserved, and reused, so if Point3D is added to the global before Point, adding Point will only need to add the identifier Point to the global object; the initialization of the prototype properties doesn't need to occur.