Copyright 2021-2023 Moddable Tech, Inc.
Revised: August 31, 2023
To exchange data between machines that can run in separate threads, XS always supported marshalling thru the XS in C programming interface.
Even if you do not write C code, this document is useful if you write JavaScript code using the standard Worker
programming interface provided by the worker module of the Moddable SDK.
Similar to what JSON.stringify
and JSON.parse
do with a string, marshalling creates a memory block from a JavaScript value and demarshalling creates a JavaScript value from a memory block. The format of the memory block is binary.
XS machines can be created from scratch or cloned from a read-only machine. Typically on micro-controllers, multiple small machines can be created in RAM based based on one large read-only machine in ROM.
For those familiar with Secure ECMAScript (SES), a cloned machine is in a state analogous to the post-lockdown state in SES. In this state, all built-ins are deeply frozen. An XS created from scratch is fully mutable and therefore analogous to the pre-lockdown state in SES.
When two machines are created from scratch or cloned from different read-only machines, they are alien and the alien programming interface must be used:
void* xsMarshallAlien(xsSlot slot);
xsSlot xsDemarshallAlien(void* data);
In mcsim, the simulator and the app are alien machines. They communicate using the standard Worker
programming interface, which is implemented here with xsMarshallAlien
and xsDemarshallAlien
.
When two machines are cloned from the same read-ony machine, marshalling can take advantage of what is shared by the two machines and the full programming interface can be used:
void* xsMarshall(xsSlot slot);
xsSlot xsDemarshall(void* data);
The worker module clones all machines from the same read-only machine. They communicate using the standard Worker
programming interface, which is implemented here with xsMarshall
and xsDemarshall
.
What can be exchanged between alien machines?
Firstly, everything that can be exchanged thru JSON:
false
null
true
- numbers
- strings
- instances of
Usually, the marshalled memory block is smaller that the equivalent JSON string and xsMarshallAlien
/xsDemarshallAlien
are faster than JSON.stringify
/ JSON.parse
.
XS can also marshall other values not possible using JSON:
undefined
- bigints
- instances of
-
Boolean
, Error
, EvalError
, RangeError
, ReferenceError
, SyntaxError
, TypeError
, URIError
, AggregateError
-
Number
, Date
-
String
, RegExp
-
BigInt64Array
, BigUint64Array
, Float32Array
, Float64Array
, Int8Array
, Int16Array
, Int32Array
, Uint8Array
, Uint8ClampedArray
, Uint16Array
, Uint32Array
-
Map
, Set
-
ArrayBuffer
, SharedArrayBuffer
, DataView
Proxy
Furthermore, XS can marshall cyclic references:
const a = { b: {} }
a.b.a = a;
worker.postMessage(a);
self.onmessage = function(a) {
trace(`${ a.b.a === a }\n`); // true
}
Marshalling is for data. Objects related to code, or to the execution of code, cannot be marshalled: accessors, arguments, classes, functions, generators, modules and promises.
Also, objects related to garbage collection cannot be marshalled: finalization registries, weak references, weak maps and weak sets. And, of course, objects implemented outside XS cannot be marshalled: host objects and host functions.
In these cases, XS tries to report a meaningful error:
try {
worker.postMessage([ { p: { get x() {} } } ]);
}
catch {
}
breaks into xsbug with
main.js (2) # Break: (host): marshall [0].p.x: accessor!
Historically, XS reported marshalling errors as marshall: no way!
What can be exchanged between machines cloned from the same read-only machine?
Firstly, everything that can be marshalled between alien machines. Then XS takes advantage of what is shared in the read-only machine to complete the marshalled instances.
In the following examples, preload.js is a module that is preloaded by the XS linker. Its body is executed at link time to define classes and objects in the read-only machine.
Like what happens with JSON.stringify
and JSON.parse
, custom prototypes are lost when instances are marshalled:
class C {}
const o = new C();
trace(`${o.constructor.name}\n`); // C
worker.postMessage(o);
self.onmessage = function(m) {
trace(`${m.constructor.name}\n`); // Object
}
But if custom prototypes are in the read-only machine, they are kept:
import { C } from "preload";
const o = new C();
trace(`${o.constructor.name}\n`); // C
worker.postMessage(o);
self.onmessage = function(m) {
trace(`${m.constructor.name}\n`); // C
}
Similarly, private fields are ignored when marshalled.
class C {
#x;
constructor(x) {
this.#x = x;
}
toString() {
return this.#x;
}
}
const o = new C("oops");
worker.postMessage(o);
self.onmessage = function(m) {
trace(`${m.toString()}\n`); // [object Object]
}
Except when the class is in the read-only machine:
export class C {
#x;
constructor(x) {
this.#x = x;
}
toString() {
return this.#x;
}
}
import { C } from "preload";
const o = new C("wow");
worker.postMessage(o);
self.onmessage = function(m) {
trace(`${m.toString()}\n`); // wow
}
References to instances in the read-only machine are preserved:
export const o = Object.freeze({});
import { o } from "preload";
worker.postMessage({ o });
import { o } from "preload";
self.onmessage = function(m) {
trace(`${m.o === o}\n`); // true
}
That is especially useful for exchanging references to objects with methods, like the handler of a proxy. Here is an example inspired by the MDN Proxy documentation:
export const handler = Object.freeze({
get: function (target, key, receiver) {
if (key === "message2") {
return "world";
}
return Reflect.get(...arguments);
}
});
const target = {
message1: "hello",
message2: "everyone"
};
import { handler } from "preload";
worker.postMessage(new Proxy(target, handler));
self.onmessage = function(m) {
trace(`${m.message1}\n`); // hello
trace(`${m.message2}\n`); // world
}
Like private fields, properties keyed by symbols are ignored when marshalled, except when the symbol is created in the read-only machine.
export const s = Symbol();
import { s } from "preload";
const o = { [s]: "wow" };
worker.postMessage(o);
import { s } from "preload";
self.onmessage = function(m) {
trace(`${m[s]}\n`); // wow
}
Exchange Data, Share Code
Marshalling is a way to safely exchange data between machines. Prototypes, classes, and proxy handlers in the read-only machine are ways to safely share code between cloned machines.