1 of 24

Fixed layout objects

Shu-yu Guo (Google)

Co-champions: Asumu Takikawa (Igalia), Ron Buckton (Microsoft), Apple?

2 of 24

What's the big idea?

Structs are objects with fixed layout at construction, i.e. "closed" instead of "open"

Attractive for a bunch of things!

  1. Shared memory concurrency
  2. WasmGC interop
  3. FFIs (marshalling)
  4. Packing memory layout
  5. Predictable performance
  6. Would help for userland data types like Complex, especially together with operator overloading

This proposal considers (1) and (2) to be requirements and seeks to be a minimal building block for others

3 of 24

Shared memory concurrency

But like, why via scary shared memory?

  • Mega-apps running into performance wall today (GSuite, MSO, TSC)
  • Mega-apps and experts will need expressivity and power of shared memory
  • Will be here via WasmGC in N years
  • SAB exists, so not new power

Let's use more cores!

4 of 24

Why not just SABs and Atomics?

If you want to shared structured data, you need wrappers. And wrappers have issues

  • O(n) wrappers per-thread. Sure you can share your payloads, but complex apps have complex object graphs. Recreating wrappers per-thread wastes memory, is slow, and hard to use
  • Have to manually choose representation
  • Manual lifetime management

5 of 24

Continuation of SAB story

Players have changed! It was game engines back then, now it's mega-apps. Higher-level shared memory concurrency is a power they need for a better product

TC39 drove SABs to be the shared foundation for shared memory between JS and Wasm. We succeeded.

Let's do it again at a higher abstraction level.

6 of 24

WasmGC Interop

Should we care? You betcha

  • Responsibility to ecosystem to ensure Wasm+JS work well together
  • It'd be a damned shame to end up with similar but different features
  • It'd be a worse shame to end up with wrappers
  • WasmGC will be multithreaded in N years, want common foundation

Managed languages are coming via WasmGC!

7 of 24

Shades of interop

Interop here means "works with", not "exactly the same feature set in both WasmGC and JS"

Wasm -> JS

JS -> Wasm

Struct instances are usable and explainable

Goal of this proposal

Goal of future extensions

Type reflection

Non-goal of TC39

Non-goal of TC39

8 of 24

Too early for WasmGC?

WasmGC is in significant flux, so is it too early for this proposal?

No in the scoped sense: this proposal seeks to add common machinery compatible with all possible WasmGC futures, which all have managed memory (i.e. not in linear memory) structs with fixed layout

Yes in the general sense: other proposals needed to flesh out full WasmGC story

9 of 24

Other use cases should not be precluded, but are out of scope

10 of 24

MVP Proposal

  1. This is all about semantics of instances
  2. Non-shared heap can reference shared heap, but not other way
  3. Minimal to enable new expressivity and build foundations
  4. Request: hold off on syntax discussions please!

Leitmotifs

11 of 24

MVP Proposal

Plain Structs

Shared Structs

  • All instance fields initialized in one shot
  • Otherwise "just" sealed objects
  • New expressivity!
  • Instances are shared when communicated across agents
  • Can only reference primitives or other shared structs
  • [[Prototype]] throws

12 of 24

Plain Structs

  • Contextual keyword for now
  • Sealed instances whose instance fields are all initialized to undefined in one shot at the beginning
  • Superclasses have to be all structs
  • Otherwise just like plain objects

"use strict";

struct class Box {

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

getX() { return x; }

x;

}

let box = new Box();

// x is declared

box.x = 42;

// Returns 42

box.getX();

// Structs are sealed

assertThrows(() => {

box.y = 8.8;

});

assertThrows(() => {

box.__proto__ = {}

});

13 of 24

Plain Structs

  • One-shotness works with inheritance by concatenating layouts

struct class C {

f1;

f2;

}

struct class D extends C {

f3;

f4;

}

// d always has all of f1,f2,f3,f4

// No partial initialization!

let d = new D;

14 of 24

Shared Structs

// main.js

"use strict";

shared struct class SharedBox {

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

x;

}

let sharedBox = new SharedBox();

// x declared

sharedBox.x = 42;

assertThrows(() => {

// y not declared

sharedBox.y = 84;

});

Like plain structs, but with several restrictions as to be shareable among agents

  • Instances are sealed and one-shot initialized
  • Only fields and constructor allowed. No methods and accessors
  • Static fields and methods okay; constructor isn't shared

15 of 24

Shared Structs

  • Can reference non-Symbol primitives
  • Can reference shared structs
  • Cannot reference local objects
  • Cannot access [[Prototype]]

// Primitives okay

sharedBox.x = 42.42;

sharedBox.x = undefined;

sharedBox.x = "foo";

// [1,2,3] is a local object

assertThrows(() => {

sharedBox.x = [1,2,3];

});

// Touching [[Prototype]] throws

assertThrows(() => {

sharedBox.y;

});

16 of 24

Shared Structs

  • Can be communicated to other agents
  • Keeps identity!

Can print any of the following interleavings:

main main

main worker

worker main

worker worker

// main.js

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

worker.postMessage({ sharedBox });

sharedBox.x = "main";

console.log(sharedBox.x);

// worker.js

onmessage = function(e) {

let sharedBox = e.data.sharedBox;

sharedBox.x = "worker";

console.log(sharedBox.x);

};

17 of 24

Shared Structs

  • Non-arithmetic Atomics methods will be extended accordingly

Atomics.store(sharedBox, 'x', 42);

// Returns 42

Atomics.load(sharedBox, 'x');

// Returns 42

Atomics.exchange(sharedBox, 'x', 84);

// Returns 84

Atomics.compareExchange(

sharedBox, 'x', 84, 42);

18 of 24

Aside: what are plain structs for?

  • Some value in having declarative sealed one-shot initialized objects
  • Maybe somewhat more predictable performance

But honestly, probably mainly future-proofing for future additions to both shared and plain structs, like sized fields for memory packing

19 of 24

Why don't fields have types?

  • Even machine types not needed for concurrency, which just cares about alignment and width
  • Even machine types not strictly needed for WasmGC; WasmGC objects reflected into JS are free to perform additional type checks on field access, like how Wasm-originated ArrayBuffers have additional behavior

More complex type system discussions definitely out of scope

20 of 24

Implementation guidance

The hard work for this MVP proposal is infrastructure retrofitting in engines for shared structs

  • Designed to be implementable with either separate heaps or unified heap
  • Hidden class should be immutable and shared
  • Shared struct fields need to be at least pointer-width and pointer-width aligned, accessing them must not tear

21 of 24

Stuff that's hard: GC

Choosing separate heaps or unified heap

  • Separate heaps:
    • Independent GC of local heaps, but need to coordinate on GC on shared heap
    • Global safepoint needed for shared heap GC
    • How to tell if shared heap should be GCed?
  • Unified heap
    • Easier tracing, one kind of GC
    • May punish all threads w/ longer pause times if one thread has heavier workload
    • Probably scales better?

22 of 24

Stuff that's real hard: strings

First of all, it's funny to me that for all the high prestige problems in compilers and PLs around concurrency, the actual really hard thing is strings

  • All engines have complex string optimizations where strings mutate their representation in-place
    • Rope flattening
    • Canonicalization (a.k.a. interning)
    • Externalization (moving ownership of chars to embedder)
  • It is hard to make these thread-safe and performant
  • Fun problem, though! Puts that CS education to work

23 of 24

Companion proposals needed

  • High(er)-level synchronization, like Atomics.Lock and Atomics.ConditionVariable
  • Code sharing

24 of 24

Namely, to explore the space of fixed layout objects for the following use cases:

  • Shared memory concurrency
  • WasmGC interop

with an eye on extending them in the future as use cases arise

Stage 1?