Jint 4.17.0

dotnet add package Jint --version 4.17.0
                    
NuGet\Install-Package Jint -Version 4.17.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.

                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.

                    
Directory.Packages.props

                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Jint --version 4.17.0
                    
#r "nuget: Jint, 4.17.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Jint@4.17.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Jint&version=4.17.0
                    
Install as a Cake Addin
#tool nuget:?package=Jint&version=4.17.0
                    
Install as a Cake Tool

Build NuGet NuGet MyGet Join the chat at 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, EventStore, OrchardCore, ELSA Workflows, docfx, JavaScript Engine Switcher, 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, 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:

    // 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.

// 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:

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.

A restore keeps the warmed interpreter caches, and with them the last callee and receiver each warmed call or member-read site served. When those are host objects that must not outlive a request — a delegate closing over a request's IServiceProvider, a wrapped HttpContext — release them as well:

engine.Advanced.RestoreGlobalSnapshot(snapshot);
engine.Advanced.DiscardInterpreterCaches();

The engine, its realm, the prepared scripts and CLR member resolution stay warm; the next run of each script rebuilds its handler tree, roughly a first run on a new engine minus building the engine. Discarding on every return gives up the warm trees, so a pool that keeps busy engines warm can discard only when an engine goes idle. A function expression held only by another object, module code and computed property keys are not reached.

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