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:
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!)); }
--- 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
| Job | Jint 4 | ClearScript (V8) | OneEcma |
|---|---|---|---|
| Create an engine | new Engine(options => …) | new V8ScriptEngine() | new ScriptEngine(new ScriptEngineOptions { … }) |
| Limits | LimitMemory, TimeoutInterval, MaxStatements, LimitRecursion, CancellationToken on the options | MaxRuntimeHeapSize; Interrupt() from another thread for a deadline | EngineLimits: MaxAllocatedBytes, WallClockMs, Fuel, MaxCallDepth, MaxStringLength; a CancellationToken on each call |
| Run code | Execute(code, source), Evaluate(code) | Execute(documentInfo, code), Evaluate(code) | Evaluate(code, fileName); EvaluateAsyncBody for top-level await and return; EvaluateModule |
| Expose a function | SetValue("log", new Action<string>(…)) | AddHostObject("log", new Action<string>(…)) | DefineGlobalFunction("log", 1, (this, args) => …) or realm.NewFunction |
| Expose an object or type | SetValue("api", obj); TypeReference.CreateTypeReference<T>(engine); AllowClr() for whole assemblies | AddHostObject("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 error | JavaScriptException: Message, JavaScriptStackTrace | ScriptEngineException: ErrorDetails | JsException: 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, andAllowClr()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 callingConfigureagain. Setup done after the engine exists still works, but its failures aren’t explained. - Limits are counted differently. Jint’s
MaxStatementscounts statements; OneEcma’sFuelcounts 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’sMaxAllocatedBytescounts bytes requested over the run. - Error text. OneEcma’s
err.messageanderr.stackfollow 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. Defineconsole.log,printor a logger yourself, aslogis defined above.
Good to know
- ClearScript’s
MaxRuntimeHeapSize,InterruptandScriptEngineException.ErrorDetailsare 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.