Recursive AI

Move from Jint or ClearScript

All oneecma recipes

Move a .NET host from Jint or ClearScript to OneEcma: how engine creation, limits, exposing functions and objects, running code and catching errors map, with one program run on all three engines and what each prints for the same mistake.

The same job on three engines

The script calls a host function load, which returns an object with items, and then reads lines. The packages are Jint 4.17.0, Microsoft.ClearScript.V8 7.5.1.1 with its linux-x64 native package, and RecursiveAI.OneEcma 1.0.0, in one project:

C#
using Jint;
using Microsoft.ClearScript;
using Microsoft.ClearScript.V8;
using RecursiveAI.OneEcma.Diagnostics;
using OneEcmaEngine = RecursiveAI.OneEcma.ScriptEngine;
using RecursiveAI.OneEcma;
using JsValue = RecursiveAI.OneEcma.JsValue;
using JsException = RecursiveAI.OneEcma.JsException;
 
const string script = """
    function invoiceTotal(inv) {
      return inv.lines.reduce((sum, l) => sum + l.qty * l.price, 0);
    }
    const invoice = load("INV-7");
    log("total " + invoiceTotal(invoice));
    """;
 
// Jint 4.17.0
Console.WriteLine("--- Jint");
var jint = new Engine(o => o.TimeoutInterval(TimeSpan.FromSeconds(1)).MaxStatements(100_000).LimitMemory(16_000_000).LimitRecursion(500));
jint.SetValue("log", new Action<string>(Console.WriteLine));
jint.Execute("globalThis.load = id => ({ id, items: [1, 2] });");
try { jint.Execute(script, "invoice.js"); }
catch (Jint.Runtime.JavaScriptException e) { Console.WriteLine(e.Message); Console.WriteLine(e.JavaScriptStackTrace); }
 
// ClearScript V8 7.5.1.1
Console.WriteLine("--- ClearScript");
using (var v8 = new V8ScriptEngine { MaxRuntimeHeapSize = (UIntPtr)(64 << 20) })
{
    v8.AddHostObject("log", new Action<string>(Console.WriteLine));
    v8.Script.load = v8.Evaluate("id => ({ id, items: [1, 2] })");
    try { v8.Execute(new DocumentInfo("invoice.js"), script); }
    catch (ScriptEngineException e) { Console.WriteLine(e.ErrorDetails); }
}
 
// OneEcma 1.0.0
Console.WriteLine("--- OneEcma");
var oneecma = new OneEcmaEngine(new ScriptEngineOptions
{
    Limits = new EngineLimits { WallClockMs = 1_000, Fuel = 100_000, MaxAllocatedBytes = 16_000_000, MaxCallDepth = 500 },
    Configure = e =>
    {
        e.DefineGlobalFunction("log", 1, (_, a) => { Console.WriteLine(e.ToDisplayString(a[0])); return JsValue.Undefined; });
        e.DefineGlobalFunction("load", 1, (_, a) =>
        {
            JsRealm realm = e.MainRealm;
            JsObject inv = realm.NewObject();
            realm.DefineData(inv, "id", a[0]);
            realm.DefineData(inv, "items", JsValue.FromObject(realm.NewArray([JsValue.FromNumber(1), JsValue.FromNumber(2)])));
            return JsValue.FromObject(inv);
        });
    },
});
try { oneecma.Evaluate(script, "invoice.js"); }
catch (JsException e)
{
    Console.WriteLine(oneecma.ToDisplayString(oneecma.MainRealm.Get(e.Value.AsObject!, "stack")));
    Console.WriteLine(DiagnosticRenderer.Render(e.Report!));
}
Text
--- Jint
Cannot read property 'reduce' of undefined
    at invoiceTotal (invoice.js:2:10)
    at invoice.js:5:16
--- ClearScript
TypeError: Cannot read properties of undefined (reading 'reduce')
    at invoiceTotal (invoice.js:2:20) ->   return inv.lines.reduce((sum, l) => sum + l.qty * l.price, 0);
    at invoice.js:5:16
--- OneEcma
TypeError: Cannot read properties of undefined (reading 'reduce')
    at invoiceTotal (invoice.js:2:20)
    at invoice.js:5:16
error[TypeError]: Cannot read properties of undefined (reading 'reduce')
 --> invoice.js:2:14
  |
2 |   return inv.lines.reduce((sum, l) => sum + l.qty * l.price, 0);
  |              ^^^^^ `inv.lines` is undefined
  |
  = help: did you mean `inv.items`? (Array, 2 items)
  = stack: invoiceTotal (invoice.js:2) ← <top-level> (invoice.js:5)

log is never reached: all three stop at inv.lines.reduce. OneEcma’s err.stack is the same as V8’s (through ClearScript) here, line and column included, and its report adds what the object held.

How the APIs map

JobJint 4ClearScript (V8)OneEcma
Create an enginenew Engine(options => …)new V8ScriptEngine()new ScriptEngine(new ScriptEngineOptions { … })
LimitsLimitMemory, TimeoutInterval, MaxStatements, LimitRecursion, CancellationToken on the optionsMaxRuntimeHeapSize; Interrupt() from another thread for a deadlineEngineLimits: MaxAllocatedBytes, WallClockMs, Fuel, MaxCallDepth, MaxStringLength; a CancellationToken on each call
Run codeExecute(code, source), Evaluate(code)Execute(documentInfo, code), Evaluate(code)Evaluate(code, fileName); EvaluateAsyncBody for top-level await and return; EvaluateModule
Expose a functionSetValue("log", new Action<string>(…))AddHostObject("log", new Action<string>(…))DefineGlobalFunction("log", 1, (this, args) => …) or realm.NewFunction
Expose an object or typeSetValue("api", obj); TypeReference.CreateTypeReference<T>(engine); AllowClr() for whole assembliesAddHostObject("api", obj); AddHostType(typeof(T))[JsExport] on the type, then JsExports.InstallAll(realm) and realm.Wrap(obj)
Call script from C#Invoke("fn", args)Script.fn(args)engine.Call(fn, this, args) or realm.Call
Catch a script errorJavaScriptException: Message, JavaScriptStackTraceScriptEngineException: ErrorDetailsJsException: Value, Report, Frames; JsLimitException for a limit

What changes when you move

  • Exposing .NET is opt-in, member by member. Jint wraps any CLR object passed to SetValue, and AllowClr() opens whole assemblies to script (Jint README, v4.17.0). ClearScript’s exposed objects “require no modification, decoration, or special coding of any kind” and scripts reach “most of the features of exposed objects and types” (ClearScript README). In OneEcma only types marked [JsExport] and their public members exist in script, the bindings are written at build time, and nothing is found by reflection. Plan one pass that marks the types your scripts use: see Expose .NET objects with [JsExport].
  • Conversions are strict. A string isn’t accepted where a number is expected, or the other way round; the error names the member, the argument path and both types.
  • Host setup moves into Configure. OneEcma explains a failure by replaying the run, and the replay rebuilds the world by calling Configure again. Setup done after the engine exists still works, but its failures aren’t explained.
  • Limits are counted differently. Jint’s MaxStatements counts statements; OneEcma’s Fuel counts calls, loop back-edges, RegExp backtracking steps and units of built-in work, so retune the number rather than copying it. ClearScript’s heap limit is V8’s heap; OneEcma’s MaxAllocatedBytes counts bytes requested over the run.
  • Error text. OneEcma’s err.message and err.stack follow V8’s wording and format, so code that matched ClearScript’s V8 messages keeps matching. Jint 4.17.0 printed “Cannot read property 'reduce' of undefined” for this script, where V8 prints “Cannot read properties of undefined (reading 'reduce')”.
  • No native code. ClearScript embeds V8 through a native library per platform. OneEcma is managed C# with no dependency beyond the base class library, and runs under NativeAOT and trimming.
  • No console. OneEcma gives scripts the language and its built-ins only. Define console.log, print or a logger yourself, as log is defined above.

Good to know

  • ClearScript’s MaxRuntimeHeapSize, Interrupt and ScriptEngineException.ErrorDetails are in the ClearScript reference. Jint’s constraints are under “Execution Constraints” in its README.
  • How OneEcma’s speed compares with Jint on lodash, preact and a React server render, with the method, is in benchmarks.