1 of 23

Shared structs stage 2 feature set

Shu-yu Guo

Ron Buckton

April 2024 TC39

2 of 23

Unshared structs

  • Fixed layout
    • Transitively immutable [[Prototype]] slot
  • One-shot initialization, then sealed
    • Superclasses must be structs
  • No constructors per se, but post-construction initializer
    • No return override!
  • Struct methods are not generic
    • Throws on incompatible receivers (cf Set method precedent)

3 of 23

Unshared structs

struct Box {

// maybe should be named init?

constructor(x) { this.x = x; }

rizz() { }

x;

}

let box = new Box();

box.x = 42; // x is declared

// structs are sealed

assertThrows(() => { box.y = 8.8; });

assertThrows(() => { box.__proto__ = {} });

// methods are non-generic on receiver

assertThrows(() => box.rizz.call({}));

4 of 23

Shared structs

Same restrictions as unshared structs, plus:

  • Can be shared across agents (keeps identity across postMessage)
  • Data fields are shared
  • Only reference other primitives or shared objects
  • Have either a null [[Prototype]] or a realm-local [[Prototype]]
    • Do not have a .constructor property if the struct has a null [[Prototype]].
    • Getters, setters, and methods are allowed in the case of a realm-local [[Prototype]].

5 of 23

Realm-local prototypes

  • Enable attaching behavior by having thread-local prototypes
  • These can be POJOs, and can reference anything in the thread
  • Each thread will set up its own prototype
  • If you squint, this is how primitive prototypes already work

6 of 23

// in main

shared struct SharedPoint extends B {

// realm-local either by default or

<hand-wavy realm-local proto mechanism>

x;

y;

where() {}

}

let p = new SharedPoint;

p.x = 0;

p.y = 1;

Object.getPrototypeOf(p).where =

function() { console.log("in main"); };

p.where(); // in main

worker.postMessage(p);

7 of 23

// in worker

onmessage = (p) => {

// undefined is not a function

assertThrows(() => p.where());

Object.getPrototypeOf(p).where =

function() { console.log("in main"); };

p.where(); // in worker

}

// in main

...

p.where() // still "in main"

8 of 23

The Correlation Problem

9 of 23

// structs.js

shared struct SharedPoint {

<hand-wavy realm-local proto mechanism>

}

SharedPoint.prototype = { foo() {} };

// worker A

import { SharedPoint } from 'structs.js'

workerB.postMessage(new SharedPoint);

// worker B

import { SharedPoint } from 'structs.js'

// Oops, 2 different SharedPoints!

onmessage = (p) => { p.foo(); }

10 of 23

Shared heap

Worker A

Worker B

SharedPoint shape A

prototype

TLS key A

TLS key B

{ foo }

TLS key A

{ foo }

SharedPoint shape B

prototype

TLS key B

11 of 23

The Correlation Problem

  • Ideally, we want to correlate SharedPoints somehow
  • Correlation ideally means:
    • Share TLS key for prototype
    • VM deduplicates shapes: more monomorphism etc
  • Coming from other ecosystems, devs' intuitive mental model is types are already correlated

Design constraints

  • Ideally no new global communication channel
  • Better DX than manual correlation
  • Performance: you don't want your ICs to become megamorphic as you scale # of threads

12 of 23

Proposal

// structs.js

shared struct SharedPoint {

// strawperson

<hand-wavy correlation mechanism>

<hand-wavy realm-local proto mechanism>

}

SharedPoint.prototype = { foo() {} }

// worker A

import { SharedPoint } from 'structs.js'

workerB.postMessage(new SharedPoint);

// worker B

import { SharedPoint } from 'structs.js'

// works

onmessage = (p) => { p.foo(); }

13 of 23

Shared heap

Worker A

Worker B

SharedPoint shape registered

prototype

TLS key

TLS key

{ foo }

TLS key

{ foo }

14 of 23

Semantics

  • Agent-cluster wide registry
  • Keyed off of source location (like template objects)
  • On registry miss (i.e. first evaluation of a registered shared struct)
    • Insert into registry
  • On registry hit (i.e. subsequent evaluations in different workers)
    • Check shape matches exactly
      • Order and name of fields
      • Thread-localness of prototype
    • Deduplicate if matches
    • Otherwise open question:
      • Silently do nothing (console can warn)
      • Or throw

15 of 23

Communication channel?

  • Key (source location) is unforgeable
  • If registry hits do nothing on layout mismatch, unobservable
  • If registry hits throw on layout mismatch, to attack requires modifying + triggering re-evaluation of modules/scripts
    • If you can trigger re-eval, you can also directly observe things, so why leak bits via this?

This registry is not a communication channel IMO

16 of 23

Bundler guidance

  • Source location meaningful for registered shared struct declarations
  • Cannot be duplicated in semantics-preserving way by bundlers
  • Tree-shaking bundlers would need to be aware
  • Not a dealbreaker IMO

17 of 23

Shared fixed-length arrays

  • Length is required at construction time
  • Instances cannot be resized
  • Elements can only be other primitives or other shared objects
  • Shared arrays have a realm-local [[Prototype]] (with a complement of helper functions)
  • The shared array constructor has [Symbol.hasInstance] to support instanceof

18 of 23

Shared fixed-length arrays

// main.js

let sharedArray = new SharedFixedArray(10);

assert(sharedArray.length === 10);

let worker = new Worker('worker.js');

worker.postMessage({ sharedArray });

sharedArray[0] = "main";

console.log(sharedArray[0]);

// worker.js

onmessage = function(e) {

let sharedArray = e.data.sharedArray;

sharedArray[0] = "worker";

console.log(sharedArray[0]);

};

19 of 23

Mutex

  • Atomics.Mutex has a [Symbol.hasInstance] to support instanceof
  • Instances are sealed
  • Instances have no properties
  • Instances are shared with instead of copied to other agents
  • Instances have a realm-local [[Prototype]] with the following methods
    • lock([ timeout ]): Acquire mutex by blocking for a maximum of timeout milliseconds. Once acquired. The timeout parameter defaults to Infinity.
    • tryLock(): Try to acquire mutex, returning undefined if already locked.
    • unlock()

20 of 23

Condition

  • Atomics.Condition has a [Symbol.hasInstance] to support instanceof
  • Instances are sealed
  • Instances have no properties
  • Instances are shared with instead of copied to other agents
  • Instances have a realm-local [[Prototype]] with the following methods
    • wait(mutex, [ timeout ]): Block the agent until cv is notified or until timeout milliseconds have passed. mutex must be locked. It is atomically released when the agent blocks and is reacquired after notification. timeout defaults to Infinity. Throw a TypeError of the agent's [[CanBlock]] is false.
    • notify(count): Notify count number of cv's waiters. A count of Infinity notifies all waiters.

21 of 23

Memory model

  • Default unordered
  • Atomics methods extended to support seqcst
  • Allocation-is-publication
  • Synchronization primitives are seqcst

22 of 23

One-way disablement

globalThis.disableSharedMemory() // hand-wavy

23 of 23

Wasm

Will move only as fast as the shared WasmGC proposal