-
-
Notifications
You must be signed in to change notification settings - Fork 7
JavaScript Engine Example
- JSContext - plain JavaScript context
- JSModuleContext - context with modules and clr support
- YantraJSContext - context with modules, clr and CSX Module support
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");
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
.
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;
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");
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.
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
.
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)"));
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"));
All CLR public methods/properties are available in JavaScript as camel case to maintain JavaScript naming convention.
To use Modules, you can create JSModuleContext
. This context exposes clr
module which has following features.
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....
import clr from "clr";
var int32 = clr.getClass("System.Int32"); // this will return typeof(System.Int32);
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) |
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 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;
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.
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