Skip to content

Instantly share code, notes, and snippets.

@kripken
Last active May 17, 2023 23:12
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kripken/949eab99b7bc34f67c12140814d2b595 to your computer and use it in GitHub Desktop.
Save kripken/949eab99b7bc34f67c12140814d2b595 to your computer and use it in GitHub Desktop.
WebAssembly pthreads + memory growth + JS

WebAssembly pthreads + memory growth + JS

Background

JavaScript can access a wasm memory, using

var buffer = wasmMemory.buffer;
var view8 = new Int8Array(buffer);
view8[100] = 1; // write
console.log(view8[200]); // read
someAPI(view8.subarray(120, 130)); // make a view

Emscripten uses this extensively to allow convenient mixing of JS and wasm. We have both JS code of our own as well as code written by users.

A problem with memory growth

Wasm memories can grow. When they do so, the wasmMemory.buffer changes to a new buffer, and any existing views (like view8 from before) remain valid but do not change length. That means that accessing the new area just grown is not possible. That is,

function func() {
  var ptr = callSomething(); // say that this grows memory
  return view8[ptr]; // if ptr is in the newly grown area, we fail
}

(Here "fail" means that we get undefined, since we read an invalid index in a typed array.) Note that a potentially common case is if callSomething does a malloc, does some writes, and returns that pointer - then if that malloc grew memory, we would not be able to read it, unless we did something like this:

function func() {
  var ptr = callSomething();
  view8 = new Int8Array(wasmMemory.buffer); // create a new view
  return view8[ptr];
}

Creating a new view for every memory access would be significant overhead. We might reduce it by updating the view only in places where it might change - after calls and atomic operations - which might help some inner loops, but this may be hard to get right, in particular for user code.

Another option is to call into wasm for every memory operation, that is, replace view8[ptr] with instance.load8_s(ptr) where load8_s is a tiny function exported from the module:

  (func "load8_s" (param $ptr i32)
    (i32.load8_s (local.get $ptr))
  )

Calling into and out of wasm for each memory operation would also introduce signficant overhead, though.

A proposed solution

The idea of calling into and out of wasm inspires the question, "what if we had an API for that?" That is, what if wasm Memories had methods like wasmMemory.load8_s. That would be identical in behavior to a wasm module exporting a function with such a load, as we just considered, that is, it would let JS say "do a normal load from this wasm memory", where normal includes all the semantics of wasm memory operations - trap on out of bounds, automatic handling of memory growth, etc.

The proposed methods on WebAssembly.Memory objects might be:

  • load8_s(ptr): do a signed 8-bit load, at location ptr
    • load8_u, load16_s, etc.
  • store8(ptr, value): do an 8 bit store of value to location ptr
    • store16, store32, etc.
  • view8_s(start, end): create an 8-bit signed view from [start, end)
    • view8_u, view16_s, etc.

Notes:

  • load8_s etc. is consistent with wasm text; on the other hand the names could be more consistent with JS typed array names if we did loadInt8 etc.
  • The view methods return normal typed array views - we don't need them to be sensitive to later memory growth events.
  • These could easily be polyfilled as discussed earlier (but the polyfill would be quite slow - we probably wouldn't recommend it to users except for testing).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment