Skip to content

JavaScript Engine Example

Akash Kava edited this page Oct 30, 2022 · 10 revisions

There three kinds of JavaScript contexts

  1. JSContext - plain JavaScript context
  2. JSModuleContext - context with modules and clr support
  3. YantraJSContext - context with modules, clr and CSX Module support

JSContext

Synchronous

FastEval is single threaded method that will compile and execute JavaScript, it will not wait for any setTimeout and it will not resolve any promises. It is useful for simple calculations.

var context = new JSContext();

// create global function
context["add"] = new JSFunction((in Arguments a) => {
    return new JSNumber(
         (a[0]?.IntValue ?? 0) + (a[1]?.IntValue ?? 0)
    );
});

var result = context.Eval("add(4,5)", "script.js");

Asynchronous

In order to use setTimeout or Promise, context needs a SynchronizationContext. So either SynchronizationContext.Current must be non null or you must provide while constructing JSContext.

Eval

If SynchronizationContext is present, for example if you are invoking script in a UI Thread, then you can use Eval and convert result into Task and await on it as shown below.

var context = new JSContext();

var r = context.Eval("some async code", "script.js");

var result = await (r is JSPromise promise).Task;

ExecuteAsync

In absence of SynchronizationContext you can use ExecuteAsync method which will execute JavaScript code synchronously along with new SynchronizationContext, and it will return after all setTimeout/setInterval methods or Promises are resolved. This method is not asynchronous.

var context = new JSContext();

var r = await context.ExecuteAsync("some async code", "script.js");

You can use Execute which runs its own AsyncPump to convert async to sync.

var context = new JSContext();

var r = context.Execute("some async code", "script.js");

Arguments Object

Every method/function are called as JSFunctionDelegate which is defined as delegate JSValue JSFunctionDelegate(in Arguments a). So basically every JavaScript function receives single readonly struct Arguments. Arguments struct contains This, NewTarget and other arguments passed along. In order to improve performance, struct lives on the stack and only a reference to struct is passed in each method. This reduces unnecessary array allocation for every method call. However less than 4 arguments are passed as field inside struct, all of them live on the stack. Only when actual arguments passed are more than 4, an Array of JSValue is created and passed in Arguments struct.

Create Function

New native C# function can be created with help of JSFunctionDelegate as shown below. It must return a JSValue instance. And you can create JSString, JSNumber from their respective constructors as they are all derived from JSValue. For any other .NET type, object instance can be wrapped in ClrProxy which is derived from JSValue.

Create a global function

Lets create a global function which will add all the numbers passed in.

context["add"] = context.CreateFunction((in Arguments a) => {
    var result = 0.0;
    for(var i = 0; i<a.Length; i++) {
        result += a[i].DoubleValue;
    }
    return new JSNumber(result);
}, "add");

Console.WriteLine(context.Eval("add(1,2,3)"));

Marshal CLR Object

Custom CLR types can be wrapped in ClrProxy which will allow you to call any methods directly from JavaScript.

context["createUri"] = context.CreateFunction((in Arguments a) => {
    var uri = new Uri(a[0]?.ToString() ?? throw context.NewReferenceError("At least one parameter expected");
    return new ClrProxy(uri);
}, "createUri");

Console.WriteLine(context.Eval("var uri = createUri('https://yantrajs.com'); uri.host"));

Naming Convention

All CLR public methods/properties are available in JavaScript as camel case to maintain JavaScript naming convention.

JSModuleContext

To use Modules, you can create JSModuleContext. This context exposes clr module which has following features.

Setup JSModuleContext

var rootFolder = "... global folder to load node_modules from ... , set this to global npm installed";
var context = new JSModuleContext(rootFolder);

var currentWorkingDirectory = ".... project folder..  that contains local node_modules";
var scriptPath = "... actual script location";
var result = await context.RunAsync(currentWorkingDirectory,  scriptPath);

// result contains exports....

clr.getClass

import clr from "clr";

var int32 = clr.getClass("System.Int32"); // this will return typeof(System.Int32);

new CLR Constructor

Let's see an example of how to use System.Random by creating new instance of it in JavaScript and print next number.

import clr from "clr";
var Random = clr.getClass("System.Random");
var r = new Random(); // this will create instance of `System.Random`.
var n = r.next(); // this will call `Random.Next` method

// n is `number` and not `System.Double`.
assert(typeof n === 'number');

Basic type conversions are as follow.

CLR Type JavaScript
byte, sbyte, short, ushort, int, uint, double, float, number
boolean boolean
enum string
long, ulong BigInt
string, char string
DateTime, DateTimeOffset Date
IEnumerable iterable
Task Promise
any other CLR type object (wrapped in ClrProxy)

YantraJSContext

We have added CSX module support to easily create and integrate CSX modules in JavaScript. This is easy to ship module as source code.

Yantra gives higher precedence to CSX module over JavaScript module with same file name. This gives us ability to create two files for the same module. So when you are executing script in Yantra, Yantra will use CSX module and Node can continue to use JavaScript module for the same name.

CSX Module Delegate Export

CSX module can have a ModuleDelegate defined as Task Module(JSModule module).

This is helpful when you want to load other JavaScript modules and invoke a custom logic. Importing other CSX files inside CSX are not supported, this is by design as we want to maintain dependency as JavaScript modules and do not want to create dependency graph of CSX.

#r "nuget: YantraJS.Core,1.0.18"
using System;
using System.Linq;
using System.Threading.Tasks;
using YantraJS.Core;
using YantraJS.Core.Clr;

static async Task Module(JSModule module) {

    // load some other JavaScript module    
    var importedModule = await module.ImportAsync("other-module");
    var importedFun = importedModule["namedImport"]

    // named export
    module.Exports["namedExport"] = JSNumber.Zero;

    // export *
    module.Export = new JSFunction((in Arguments a) => {
        // do something here...
        // a.This is `this`
        return JSUndefined.Value;
    }, "Function Description");


}

return (JSModuleDelegate)Module;

CSX Module Export Type

Exporting C# Type as module is easier compared to Module delegate, but you cannot import other modules in it.

#r "nuget: YantraJS.Core,1.0.18"
using System;
using System.Linq;
using YantraJS.Core;
using YantraJS.Core.Clr;


// Export attribute with parameter is considered as `*`
[Export]
// Named Export
[Export("url")]
// Default export
[DefaultExport]
public class JSUrl {

    private Uri uri;

    public JSUrl(in Arguments a) {
        this.uri = new Uri(a.Get1().ToString());
    }

    public string Host => uri.Host;

}

The only drawback here is you cannot import other JavaScript modules.

Alternative to Razor View in ASP.NET Core

We have created our website using JavaScript as view instead of Razor view, though it started as a simple application, but we realized that by using JavaScript as view, we can easily plugin Server Side Rendering and improve page delivery speed. However, using traditional JSDom is not yet supported due to very heavy dependency on various built in node modules. But you can easily create a wrapper with mocks to render content of your React/Angular components on server easily with YantraJS. Check out source code for our website at Github Repository for YantraJS Website