Engines and realms

edit

Engines and realms

The API has two levels:

  • A JsEngine is an agent of the specification: it runs code on one thread at a time, owns the job queue of the promises and applies the limits of its JsEngineOptions. It is disposable.
  • A JsRealm is a global environment created by engine.CreateRealm(): a global object (realm.GlobalObject) and its own copy of the built-ins. The scripts of a realm share their global variables.

Create a realm for each independent context, such as each document or each user script, and an engine for each thread that runs code. The realms of an engine are isolated from each other, but can exchange objects:

using var engine = new JsEngine();
var first = engine.CreateRealm();
var second = engine.CreateRealm();

// Each realm has its own global object and its own built-ins
first.Execute("globalThis.owner = 'first'; Array.prototype.last = function () { return this.at(-1); };");
Assert.Equal("undefined", second.Execute("typeof owner").AsString());
Assert.Equal("undefined", second.Execute("typeof [].last").AsString());

// The objects of a realm can be given to the other realms of the same engine
using (engine.Enter())
{
    second.GlobalObject.SetProperty("firstGlobal", first.GlobalObject);
}

Assert.Equal("first", second.Execute("firstGlobal.owner").AsString());
Assert.False(second.Execute("firstGlobal.Array === Array").AsBoolean());
  • A change to the built-ins of a realm, such as a new method on Array.prototype, is not visible from the other realms.
  • An object given to another realm keeps its own built-ins: firstGlobal.Array is not the Array of the second realm, so instanceof Array is false for its arrays (Array.isArray is true).
  • engine.Realms lists the realms created by CreateRealm.

The scripts can also create realms themselves with ShadowRealm.

The scope of an engine

The methods of the objects (GetProperty, SetProperty, DefineProperty...) and the factories of the realm (CreateObject, CreateFunction...) are used inside a scope of the engine, opened by engine.Enter() and closed by disposing it. JsRealm.Execute, JsEngine.Call and JsEngine.Construct enter the engine themselves:

using var engine = new JsEngine();
var realm = engine.CreateRealm();

// Execute, Call and Construct enter the engine themselves
var point = realm.Execute("({ x: 1, y: 2 })").AsObject();

// The members of the objects are used inside a scope of the engine
using (engine.Enter())
{
    point.SetProperty("z", 3);
    Assert.Equal(2, point.GetProperty("y").AsNumber());
    realm.GlobalObject.SetProperty("point", point);
}

Assert.Equal("1,2,3", realm.Execute("Object.values(point).join()").AsString());

A .NET function called by a script already runs inside the scope: it does not need to enter the engine again.

Without a scope, the members of the objects throw an InvalidOperationException, even when they would not run code of the script: reading a data property needs a scope as reading a getter does. The scopes can be nested, and a callback can enter the engine again.

The current realm

engine.CurrentRealm is the realm of the running code. A function created by realm.CreateFunction runs in its own realm: the objects that the built-ins create while it runs belong to that realm. RunInRealm runs a delegate with another realm as the current realm.

Threads

An engine is used by one thread at a time, and its objects belong to it. To run scripts in parallel, create one engine per thread. A script compiled once with JsPreparedScript can run in all of them: see Reuse a compiled script.

The thread that enters an engine owns it until it disposes its outermost scope. Another thread that enters the engine in the meantime, or calls Execute, Call..., gets an InvalidOperationException: the engine detects the mistake instead of corrupting its state. An engine can move to another thread between two entries, for instance after an await.

The objects of an engine cannot be given to another engine, even on the same thread:

  • Call, Construct, RunInRealm and the members of the objects throw an InvalidOperationException for a function, a realm or an object of another engine. The engine knows the owner of the functions and of the objects that have properties: an empty object of another engine is not detected, so do not rely on the check.
  • The primitive values (numbers, strings, booleans, BigInts) can be given to any engine, and so can the symbols. A symbol of the registry (Symbol.for) belongs to the registry of one engine: the same key gives another symbol in another engine.
  • The realms of one engine share their objects freely.

Engines share memory through SharedArrayBuffer: see Binary data.

Lifetime

JsEngine is disposable: dispose it when you no longer need it. The realms and the JavaScript objects are .NET objects, collected by the garbage collector once nothing references them.