DOCUMENTATION XS XS Marshalling
Overview
Getting Started
BASE
Reference
Worker
Setup modules
COMMODETTO
Reference
Poco Renderer
Outlines
Creating Fonts
Crypt
Data
DEVICES
Moddable One
Moddable Two
Moddable Three
Moddable Four
Moddable Six
ESP32
ESP8266
nRF52
nRF52 Low Power Notes
Raspberry Pi Pico
M5Core Ink
M5Paper
Wasm
SiLabs Gecko
QCA4020
Moddable Zero
Moddable Display Developer Guide
DISPLAYS
Overview
Adafruit 1.8" ST7735
Adafruit OLED Display
BuyDisplay 2.8" CTP - ESP8266
Crystalfontz ePaper
DotStar
Generic 1.44"
Generic 2.4" & 2.8" (Resistive Touch) - ESP32
Generic 2.4" & 2.8" (Resistive Touch) - ESP8266
Generic 2.4" & 2.8" (Resistive Touch) - Pico
Sharp Memory
Sharp Memory Screen 1.3"
SparkFun TeensyView
Switch Science Reflective LCD
DRIVERS
DESTM32S display
DotStar display
ILI9341 display
LPM013M126A display
LS013B4DN04 display
MCP230XX GPIO expander
NeoStrand
SSD1306 display
SSD1351 display
ST7735 display
Files
IO
TC53 IO
Firmata
NETWORK
Reference
TLS (SecureSocket)
BLE
Ethernet
Web Things
PINS
Reference
Audio Output
PIU
Reference
Localization
Keyboard
Expanding Keyboard
Die Cut
Using After Effects Motion Data
TOOLS
Reference
Manifest
Defines in Manifests
Testing
XS
Handle
JavaScript language considerations on embedded devices using the XS engine
Mods – User Installed Extensions
ROM Colors
Using XS Preload to Optimize Applications
XS Conformance
XS Marshalling
XS Platforms
XS in C
XS linker warnings
xsbug
xst
XS Compartment
XS Profiler
XS Scopes

XS Marshalling

Copyright 2021-2023 Moddable Tech, Inc.
Revised: August 31, 2023

Introduction

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.

What Is Marshalling?

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.

Alien Marshalling

What can be exchanged between alien machines?

Firstly, everything that can be exchanged thru JSON:

  • false
  • null
  • true
  • numbers
  • strings
  • instances of
    • Object
    • Array

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:

main.js
const a = { b: {} }
a.b.a = a;
worker.postMessage(a);
worker.js
self.onmessage = function(a) {
	trace(`${ a.b.a === a }\n`); // true
}

No Way

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:

main.js
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!

Full Marshalling

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.

Prototypes

Like what happens with JSON.stringify and JSON.parse, custom prototypes are lost when instances are marshalled:

main.js
class C {}
const o = new C();
trace(`${o.constructor.name}\n`); // C
worker.postMessage(o);
worker.js
self.onmessage = function(m) {
	trace(`${m.constructor.name}\n`); // Object
}

But if custom prototypes are in the read-only machine, they are kept:

preload.js
export class C {}
main.js
import { C } from "preload";
const o = new C();
trace(`${o.constructor.name}\n`); // C
worker.postMessage(o);
worker.js
self.onmessage = function(m) {
	trace(`${m.constructor.name}\n`); // C
}

Private Fields

Similarly, private fields are ignored when marshalled.

main.js
class C {
	#x;
	constructor(x) {
		this.#x = x;
	}
	toString() {
		return this.#x;
	}
}
const o = new C("oops");
worker.postMessage(o);
worker.js
self.onmessage = function(m) {
	trace(`${m.toString()}\n`); // [object Object]
}

Except when the class is in the read-only machine:

preload.js
export class C {
	#x;
	constructor(x) {
		this.#x = x;
	}
	toString() {
		return this.#x;
	}
}
main.js
import { C } from "preload";
const o = new C("wow");
worker.postMessage(o);
worker.js
self.onmessage = function(m) {
	trace(`${m.toString()}\n`); // wow
}

References

References to instances in the read-only machine are preserved:

preload.js
export const o = Object.freeze({});
main.js
import { o } from "preload";
worker.postMessage({ o });
worker.js
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:

preload.js
export const handler = Object.freeze({
	get: function (target, key, receiver) {
	if (key === "message2") {
		return "world";
	}
	return Reflect.get(...arguments);
	}
});
main.js
const target = {
	message1: "hello",
	message2: "everyone"
};
import { handler } from "preload";
worker.postMessage(new Proxy(target, handler));
worker.js
self.onmessage = function(m) {
	trace(`${m.message1}\n`); // hello
	trace(`${m.message2}\n`); // world
}

Symbols

Like private fields, properties keyed by symbols are ignored when marshalled, except when the symbol is created in the read-only machine.

preload.js
export const s = Symbol();
main.js
import { s } from "preload";
const o = { [s]: "wow" };
worker.postMessage(o);
worker.js
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.