## File: README.md [](https://github.com/sebastienros/jint/actions/workflows/build.yml) [](https://www.nuget.org/packages/Jint) [](https://www.nuget.org/packages/Jint) [](https://www.myget.org/feed/jint/package/nuget/Jint) [](https://gitter.im/sebastienros/jint) # Jint Jint is a __Javascript interpreter__ for .NET which can run on __any modern .NET platform__ as it supports .NET Standard 2.0 and .NET 4.6.2 targets (and later). ## Use cases and users - Run JavaScript inside your .NET application in a safe sand-boxed environment - Expose native .NET objects and functions to your JavaScript code (get database query results as JSON, call .NET methods, etc.) - Support scripting in your .NET application, allowing users to customize your application using JavaScript (like Unity games) Some users of Jint include [RavenDB](https://github.com/ravendb/ravendb), [EventStore](https://github.com/EventStore/EventStore), [OrchardCore](https://github.com/OrchardCMS/OrchardCore), [ELSA Workflows](https://github.com/elsa-workflows/elsa-core), [docfx](https://github.com/dotnet/docfx), [JavaScript Engine Switcher](https://github.com/Taritsyn/JavaScriptEngineSwitcher), and many more. ## Supported features #### ECMAScript 2015 (ES6) - ✔ ArrayBuffer - ✔ Arrow function expression - ✔ Binary and octal literals - ✔ Class support - ✔ DataView - ✔ Destructuring - ✔ Default, rest and spread - ✔ Enhanced object literals - ✔ `for...of` - ✔ Generators - ✔ Template strings - ✔ Lexical scoping of variables (let and const) - ✔ Map and Set - ✔ Modules and module loaders - ✔ Promises (Experimental, API is unstable) - ✔ Reflect - ✔ Proxies - ✔ Symbols - ✔ Proper tail calls in strict functions - ✔ Typed arrays - ✔ Unicode - ✔ Weakmap and Weakset Proper tail calls replace the calling strict-function frame, as required by ECMAScript. Consequently, intermediate tail callers are intentionally absent from `error.stack` and host stack telemetry. #### ECMAScript 2016 - ✔ `Array.prototype.includes` - ✔ `await`, `async` - ✔ Block-scoping of variables and functions - ✔ Exponentiation operator `**` - ✔ Destructuring patterns (of variables) #### ECMAScript 2017 - ✔ `Object.values`, `Object.entries` and `Object.getOwnPropertyDescriptors` - ✔ Shared memory and atomics #### ECMAScript 2018 - ✔ Asynchronous iteration - ✔ `Promise.prototype.finally` - ✔ RegExp named capture groups - ✔ Rest/spread operators for object literals (`...identifier`) - ✔ SharedArrayBuffer #### ECMAScript 2019 - ✔ `Array.prototype.flat`, `Array.prototype.flatMap` - ✔ `String.prototype.trimStart`, `String.prototype.trimEnd` - ✔ `Object.fromEntries` - ✔ `Symbol.description` - ✔ Optional catch binding #### ECMAScript 2020 - ✔ `BigInt` - ✔ `export * as ns from` - ✔ `for-in` enhancements - ✔ `globalThis` object - ✔ `import` - ✔ `import.meta` - ✔ Nullish coalescing operator (`??`) - ✔ Optional chaining - ✔ `Promise.allSettled` - ✔ `String.prototype.matchAll` #### ECMAScript 2021 - ✔ Logical Assignment Operators (`&&=` `||=` `??=`) - ✔ Numeric Separators (`1_000`) - ✔ `AggregateError` - ✔ `Promise.any` - ✔ `String.prototype.replaceAll` - ✔ `WeakRef` - ✔ `FinalizationRegistry` #### ECMAScript 2022 - ✔ Class Fields - ✔ RegExp Match Indices - ✔ Top-level await - ✔ Ergonomic brand checks for Private Fields - ✔ `.at()` - ✔ Accessible `Object.prototype.hasOwnProperty` (`Object.hasOwn`) - ✔ Class Static Block - ✔ Error Cause #### ECMAScript 2023 - ✔ Array find from last - ✔ Change Array by copy - ✔ Hashbang Grammar - ✔ Symbols as WeakMap keys #### ECMAScript 2024 - ✔ ArrayBuffer enhancements - `ArrayBuffer.prototype.resize` and `ArrayBuffer.prototype.transfer` - ✔ `Atomics.waitAsync` - ✔ Ensuring that strings are well-formed - `String.prototype.ensureWellFormed` and `String.prototype.isWellFormed` - ✔ Grouping synchronous iterables - `Object.groupBy` and `Map.groupBy` - ✔ `Promise.withResolvers` - ✔ Regular expression flag `/v` #### ECMAScript 2025 - ✔ 16-bit floating point numbers (float16), Requires NET 8 or higher, `Float16Array`, `Math.f16round()` - ✔ Array.fromAsync - ✔ Import attributes - ✔ Iterator helper methods - ✔ JSON modules - ✔ `Promise.try` - ✔ `RegExp.escape()` - ✔ Regular expression pattern modifiers (inline flags) - ✔ Duplicate named capture groups - ✔ Set methods (`intersection`, `union`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`) #### ECMAScript proposals (no version yet) - ✔ Await Dictionary (`Promise.allKeyed`, `Promise.allSettledKeyed`) - ✔ Decorators (`@decorator` syntax for classes, methods, fields, and accessors) - ✔ `Error.isError` - ✔ `Error.prototype.stack` accessor (error-stack-accessor) - ✔ Explicit Resource Management (`using` and `await using`) - ✔ Immutable Arraybuffers - ✔ Import Bytes (`import x from './file' with { type: 'bytes' }`) - ✔ Iterator Chunking (`Iterator.prototype.chunks`, `Iterator.prototype.windows`) - ✔ Iterator Includes (`Iterator.prototype.includes`) - ✔ Iterator Join (`Iterator.prototype.join`) - ✔ Iterator Sequencing - ✔ Joint Iteration - ✔ JSON.parse source text access - ✔ `Math.sumPrecise` - ✔ `ShadowRealm` - ✔ `Temporal` - ✔ `Uint8Array` to/from base64 - ✔ `Upsert` #### Other - Further refined .NET CLR interop capabilities - Constraints for execution (recursion, memory usage, duration) ## Performance - Because Jint neither generates any .NET bytecode nor uses the DLR it runs relatively small scripts really fast - If you repeatedly run the same script, you should prepare it for execution using `Engine.PrepareScript` or `Engine.PrepareModule`, cache the returned `Prepared<...>` object and feed it to Jint instead of the content string - You should prefer running engine in strict mode, it improves performance You can check out [the engine comparison results](Jint.Benchmark), bear in mind that every use case is different and benchmarks might not reflect your real-world usage. ## Embedding performance Notes for hosts that project their own objects into script, pool engines, or bound execution. Each of these is a cost model rather than a rule; the XML documentation on the named APIs has the detail. **Projecting host data.** Subclassing `ObjectInstance` is the most expensive way to expose data. Such a receiver gets no own-property inline caching — every own read reaches your `GetOwnProperty` and allocates the `PropertyDescriptor` it returns. Cheaper options, in order of preference: - For fixed-shape records, do not subclass at all: `JsObject.Create(engine, layout, values)` and `JsObject.CreateFromEntries` build straight into the hidden-class representation, so every object sharing a `JsObjectLayout` shares one hidden class and a script reading a batch of them keeps a monomorphic inline cache. A record with expensive members most items never have read — a body that must be parsed, a field that must be decoded — declares them with `JsObjectLayout.CreateBuilder().AddLazy(name, factory)` and passes the raw payload as the `lazySlotState` argument of `JsObject.Create`: the factory runs on the first read that observes that member's value and the result is memoized on the object, while enumerating keys, `in` and `hasOwnProperty` never run it. The object stays a hidden-class object throughout. - For CLR objects, `engine.SetValue(name, obj)` wraps them in `ObjectWrapper`, whose member resolution and compiled accessors are cached process-wide on the `TypeResolver`. - For the *prototypes* those objects sit behind, declare the members once per process with `JsObjectShape` and create one object per engine with `Instantiate`: members materialize only when a script touches them, and because the engine stores and versions the whole member set itself, a shaped prototype can serve the prototype-method inline cache — which an `ObjectInstance` subclass used as a prototype can never do. - For a **live indexed collection** — a DOM `NodeList`, a result window, any list computed on demand — derive from `ArrayLikeObject` rather than assembling the property model yourself. You implement two members, `uint Length` and `bool TryGetIndex(uint index, out JsValue value)`; the base class derives everything else (index and `length` descriptors, enumeration order, the existence and value hooks, WebIDL-shaped `delete` / `defineProperty` refusals) and the engine keys two lanes on the type, so `list[i]`, `Array.prototype` generics, `for-of` / spread / `Array.from` / destructuring and `JSON.stringify` each cost one `TryGetIndex` per element with no descriptor and no key allocation. It is array-*like*, not an array: `Array.isArray` stays `false` by design, the same answer a browser gives for a `NodeList`. If your backing store can test containment more cheaply than it can produce an element, also override `protected virtual bool HasIndex(uint index)`, and `in` / `hasOwnProperty` / `Object.keys` / `delete` stop projecting elements they only ever discard. Reach for it when the collection is live; when it is a snapshot, copying into a `JsArray` once is cheaper still — and give it a `JsObjectShape` prototype, per the bullet above, for the collection's own methods. - If you must subclass, override `TryGetOwnPropertyValue` so an own read hands the value over with no descriptor at all, and `ProbeOwnProperty` so existence and enumerability questions (`in`, `Object.keys`, spread, `JSON.stringify`) are answered without materializing one either. Both carry an obligation to agree with `GetOwnProperty`, and neither is re-verified on the hot path — a `ProbeOwnProperty` that wrongly reports a key as absent drops it from every enumeration, silently. Run your integration suite once with **host-contract verification** on and every such disagreement throws instead, naming the type, the key and both answers: ```csharp // before the first use of any Jint type — the flag is read once, at type initialization AppContext.SetSwitch("Jint.EnableHostContractVerification", true); ``` It also checks a declared `PropertyAccessSemantics.Ordinary`, an `ArrayLikeObject`'s `HasIndex`, and an `IObjectConverter` registered with `AddObjectConverter(converter, handledTypes)` converting a type it did not declare. Turn it on in a test or staging host, never in production: the checks deliberately redo the work the hooks exist to avoid. A Debug build of Jint has them on already and needs no switch. **Lazy values.** `PropertyFlag.CustomJsValue` is the supported hook for a property whose value is *computed on every read*: a `PropertyDescriptor` subclass overriding `CustomValue` keeps working under the read inline caches, because every caching lane re-reads the flag on each hit and caches the descriptor reference rather than a value snapshot. When the value is lazy only *once*, use `PropertyDescriptor.CreateLazy(state, factory)` instead — it memoizes the produced value and then stops being custom-valued, which readmits the property to the member-write fast path and the global-identifier cache that a permanently custom-valued descriptor is declined by; store it wherever you store descriptors (`SetOwnProperty` or `GetOwnProperty` on a host subclass, `FastSetProperty`, a hand-rolled global). It is the descriptor-shaped member of the same family as `JsObjectLayout.AddLazy` (records) and `JsObjectShape` (prototypes), and it does not exempt you from the rule above them: storing any raw descriptor under a string key still moves a shape-mode object to the dictionary representation. For a whole global that may never be touched, `Options.AddLazyGlobal` defers building the value until script reads the name, and `engine.Advanced.AddLazyGlobal` does the same on an engine that already exists — which is what you need when the value comes from the request you are about to serve rather than from process-wide configuration. Both install the property eagerly, so `in`, `hasOwnProperty` and `Object.keys(globalThis)` see the name without building anything; only reading the value runs the factory, once. The per-engine overload receives its engine, so unlike an `Options`-registered factory it may capture engine-affine state — and where it would do nothing but capture, the overload taking the state, `engine.Advanced.AddLazyGlobal(name, state, static (e, s) => ...)`, hands it to a `static` factory instead. That matters only because this registration is per engine: a capturing factory costs a display class and a delegate for every global on every engine you build, which on `FreshEngineGlobalsBenchmark`'s forty-global row is 32 bytes per global. There is deliberately no `Options` counterpart — a registration made there is recorded once for the process and replayed per engine, so its closure is already a one-off. **Per-request state behind an engine.** Every host-facing factory in this API receives the engine and nothing else, which is a problem when the value depends on the request rather than on process-wide configuration. `engine.Advanced.HostDefined` closes that gap: an opaque `object?` the engine never reads or interprets — the `[[HostDefined]]` field the specification reserves on a Realm Record — so a factory that captures nothing can still reach the scope it is running in. The alternative embedders reach for is a `static ConditionalWeakTable`, which takes its internal write lock every time a host associates state with an engine, across every tenant in the process. ```c# // once per process — the factory captures nothing, so one Options serves every engine var options = new Options() .AddLazyGlobal("user", static engine => JsValue.FromObject(engine, ((RequestContext) engine.Advanced.HostDefined!).User)); // per request, before you run anything engine.Advanced.HostDefined = requestContext; ``` It is the *principal* realm's field, not the current one, so it answers the same value inside a `ShadowRealm` callback as outside. A shadow realm is a distinct realm and gets its own, empty by specification — deliberate, since propagating the outer request's services into sandboxed code is exactly the ambient authority a shadow realm exists to withhold; `Host.InitializeShadowRealm` is the hook if you do want to populate it. The value dies with the engine, and nothing in the engine clears it: a restore (below) does not touch it, so a pooled engine keeps it across one and you replace it yourself, typically right after restoring. **Sparse data.** Hosts that read deep chains off optional data — `input.Address.City.length`, where any link may be absent — usually install an `IReferenceResolver` so a nullish base yields a value instead of throwing. Register `NullPropagatingReferenceResolver.Instance` rather than writing that class yourself: the engine recognizes the singleton and serves the propagation inline, with no interface call and no pooled `Reference` per nullish read, which an equivalent hand-written resolver cannot get. Pass `ReferenceResolverInterests.NullishPropertyBase` alongside it so every unrelated read lane stays armed. The boundary is that *reads* propagate: a call on a nullish base still throws, and a host that needs callable substitution or unresolvable-identifier handling writes its own resolver and forgoes the inline lane. **Prepared scripts and engine reuse.** `Engine.PrepareScript` / `PrepareModule` return an object that is reusable *and* thread-safe: prepare once at startup and feed the same `Prepared` to as many engines, on as many threads, as you like. The engine's own per-node caches are a separate matter — they are engine-owned and engage only on the **second** evaluation of a given script on a given engine, so a host that builds a fresh engine per operation never reaches them by design. Note the mirror image if you pool engines instead: a warmed call site holds a reference to the last receiver it served until it caches a different one, so pooled engines can keep host objects alive between runs. **Sharing a module graph across pooled engines.** A host whose templates or plugins are ES modules pays for that graph per engine: `IModuleLoader.LoadModule` is called by every engine that imports a module, so the obvious loader re-reads and re-parses the whole graph for every engine in the pool. Prepare each module once per *build* instead — cache the `Prepared` (`AstModule` being Acornima's `Module`, which an alias keeps apart from Jint's runtime `Module`) keyed by module location, and hand it to the overload that takes an already prepared AST, `ModuleFactory.BuildSourceTextModule(engine, in prepared)`. Name the prepared module exactly as the engine would: `Engine.PrepareModule(code, source)` takes the name up front, and it becomes `Module.Location` and therefore the `referencingModuleLocation` echoed back into `IModuleLoader.Resolve` for that module's own imports — so a name derived by hand that differs from the engine's breaks relative-import resolution with nothing to point at. `ModuleFactory.LocationOf(resolved)` *is* that rule; call it rather than reimplementing it. Single-flight the cache. `ConcurrentDictionary.GetOrAdd` does not run its factory under a lock, so N engines filling a pool concurrently each prepare the same module and N-1 results are prepared only to be discarded. Wrapping the value in a `Lazy` — whose default thread-safety mode is exactly "one factory run, everyone else waits" — is enough: ```c# using AstModule = Acornima.Ast.Module; // Jint.Runtime.Modules.Module is a different type private readonly ConcurrentDictionary> _prepared = new(StringComparer.Ordinal); public Module LoadModule(Engine engine, ResolvedSpecifier resolved) { var location = ModuleFactory.LocationOf(resolved); var prepared = _prepared.GetOrAdd(location, static key => new Lazy>(() => Engine.PrepareModule(ReadSource(key), key))).Value; return ModuleFactory.BuildSourceTextModule(engine, in prepared); } ``` Invalidate by build, not by file. The prepared ASTs are derived from a compilation, so key the cache to the same identity the rest of your host already uses for that compilation and let a rebuild drop the whole set at once, rather than expiring entries per file and serving a graph half of which is stale. Be clear about what the thread-safety of `Prepared` buys. It is safe to share and safe to run concurrently, which is what lets one cache serve the pool — it does not make the *engines* shareable (one engine, one thread) and it does not make the module registry shared: every engine still builds, links and evaluates its own module records from the shared ASTs, and `engine.Modules` is per engine. That per-engine cost is real, and it is what the pool pays no matter how much preparation is shared. `StaticAnalysis` is a trade with a break-even, not a speed-up. Preparing with `new ModulePreparationOptions { StaticAnalysis = false }` skips the pass that pre-publishes interpreter state onto the parsed tree, leaving preparation at roughly the cost of a plain parse; each engine then rebuilds lazily what the pass would have published once for all of them. Measured on `ModuleGraphEmbeddingBenchmark`'s ten-module graph, preparation cost -47.6% time and -27.5% allocation, while each engine materializing that graph from the shared cache paid +4.8% time and +9.5% allocation — break-even, on that graph, at about ten engines on time and about two on allocation. A long-lived pool should therefore keep the default (`true`); the option is for preparation that sits on a latency-critical path — a cold start, a rebuild in a dev loop — or for a host whose prepared programs outnumber the engines that ever run them. `ScriptPreparationOptions.StaticAnalysis` is the same option for scripts. **Registering only what a script uses.** When the ambient API is large and scripts touch little of it, prepare with `ScriptPreparationOptions.CollectReferencedGlobals` and read `Prepared.ReferencedGlobals`: the free identifiers the program actually references, resolved per binding site, as an immutable set you can intersect with your registry — including the CLR-side context you would otherwise build speculatively, which lazy globals cannot defer. Honor `HasDirectEvalCall`: a program with direct `eval` can reference anything, so install everything for those. **Reusing a configured engine.** If you build a fresh engine per evaluation only because you need a clean global, `engine.Advanced.CaptureGlobalSnapshot()` and `RestoreGlobalSnapshot(snapshot)` are the cheaper route: capture once after your `SetValue` calls and module setup, then restore between evaluations. Restore reverts the global object's own properties, its prototype and extensibility, and the top-level `let`/`const`/`class` declarations (which nothing else can clear, so a script with a top-level `let` can otherwise only be run once per engine); it also clears the `RegExp.$1`-style legacy statics and resets the interop wrapper caches. The per-node caches above are deliberately kept, so the next run starts warm. Keep the snapshot in a field beside the engine and put the restore in a `finally` — a script that throws still declared its globals, so restoring only on the success path hands them to the next caller. `engine.Advanced.WithRestoredGlobals(snapshot, action)` is exactly that `try`/`finally` in one call, so the restore cannot be left off the throwing path; it adds nothing else, and in particular no isolation the restore does not already give you. Choosing between this and `AddLazyGlobal` is a question of engine lifetime: a fresh-engine-per-evaluation host wants lazy globals (nothing to restore — the win is never building what the script does not read); a pooled host wants the snapshot. They compose: restore returns a global that was still lazy at capture to its unmaterialized state, so a pooled engine keeps both benefits. Restore also ends the previous cycle on the event loop. Queued jobs are discarded, and — because discarding cannot reach work that has not been enqueued yet — any promise registered before the restore is dropped when it settles instead of resuming its continuation, so a fire-and-forget `async` function suspended on a host `Task` never wakes up against the restored globals. Register a promise that is meant to outlive a restore *after* it. Restore refuses (`InvalidOperationException`) while an evaluation is in progress, including an `EvaluateAsync`/`ExecuteAsync`/`InvokeAsync` whose `Task` you still hold. What it cannot fence is you calling back in: invoking a function a previous evaluation handed you runs it against the restored surface. **It is a configuration-reuse primitive, not an isolation boundary** — mutations of `Object.prototype` and other intrinsics, of object graphs behind restored bindings, of host CLR state, plus `Symbol.for` registrations and registered modules, all survive a restore, so mutually distrusting scripts still need separate engines. Note that surviving intrinsic pollution is a surviving *binding*, not just a surviving property: bare identifiers resolve through the global's prototype chain, so `Object.prototype.leaked = 1` in one evaluation makes `leaked` a name the next one can read with no qualifier. (The global's own `[[Prototype]]` is captured and restored, so a `setPrototypeOf(globalThis, …)` is reverted.) A snapshot also keeps its engine and every captured value strongly reachable, so do not cache one past that engine's lifetime. **Constraints and options.** An `Options` instance is meant to be shared across engines, including concurrent ones — the built-in constraint helpers register a *factory*, so each engine gets its own counter and its own deadline. (Sharing it is not required: building an `Options` per scope is fine when your globals depend on scoped state.) Watch for the sentinel trap: `MaxStatements(int.MaxValue)`, `LimitMemory(long.MaxValue)` and `TimeoutInterval(TimeSpan.MaxValue)` register *no* constraint at all and remove any previously registered one of that kind, so spelling "effectively unlimited" that way leaves you with no limit rather than a large one. **Values do not cross engines.** A `JsValue` that is an `ObjectInstance` holds a hard reference to the engine and realm that created it, and passing one to a different engine is not supported — it is neither validated nor made safe. `Prepared