User:Roc/SharedMemoryWorkersProposal
Our dilemma is that we want to restrict to a worker "running asm.js only", but we want regular JS code to act as a glue layer between asm.js and Web platform APIs. So, what if we allowed regular JS to run, but only at the bottom of the call graph?
Slightly more formally: ensure that accesses to a SharedArrayBuffer only occur when everything on the call stack is asm.js (and on a worker). Implications:
- It's impossible to create an asm.js wrapper around a SharedArrayBuffer for use by regular JS, because regular JS cannot call into SharedArrayBuffer-using asm.js code.
- But calling in the other direction works: asm.js can still call into regular JS (and thence into existing Web platform APIs).
- Non-asm.js tasks can run in the worker: each turn of the worker's event loop runs either a regular JS task or a SharedArrayBuffer-aware asm.js task. Thus existing async Web APIs can be used.
Here's one way to enforce that invariant:
- Create a special API that links an asm.js module with a SharedArrayBuffer. This does not return a regular JS object, but an opaque object, let's call it a SharedArrayBufferModule.
- The SharedArrayBufferModule exposes a method postMessage(data). This method queues a task on the worker's event loop which invokes the asm.js module's entry point. There is no other way to enter the module.
- The asm.js type system prevents asm.js code from passing a SharedArrayBuffer to any external JS methods it calls.
No state flags, stack inspection or other dynamic checks required.
Exchanging Buffers With Web APIs
We need low-overhead data exchange with Web APIs that take typed-array parameters, let's say ArrayBufferViews. Here's one possible way to do that:
Give each SharedArrayBufferModule a map from ints to ArrayBufferViews. Let's call this the buffer map. Add the following APIs to SharedArrayBufferModule:
- int putBuffer(ArrayBufferView view): Lets regular JS attach an ArrayBufferView to the buffer map, returning the index of the new entry.
- ArrayBufferView takeBuffer(int index): Lets regular JS take an ArrayBufferView out of the buffer map, removing the entry.
Add the following APIs to SharedArrayBuffer:
- int createBuffer(int fromAddr, int length): Lets asm.js code copy a range of the SharedArrayBuffer to a new entry in the buffer map, returning the index of the new entry.
- void consumeBuffer(int index, int toAddr): Lets asm.js code copy the contents of a buffer map entry to the SharedArrayBuffer and remove the entry.
[Obvious variations on these APIs might make sense.]
Thus, to pass data from asm.js to regular JS, asm.js calls createBuffer to copy the data, passes the resulting buffer index to regular JS, and then regular JS calls takeBuffer to retrieve the buffer. To pass data in the other direction, regular JS attaches the ArrayBufferView to the buffer map, then passes the index of the buffer to asm.js, which calls consumeBuffer to copy the data to its heap.
Zero-Copy Data Exchange With Web APIs
For performance-critical APIs where data copying is a problem (e.g. perhaps WebGLRenderingContext::texImage2D), we could introduce a SharedArrayBufferView representing a region of a SharedArrayBuffer. This is unrelated to an ArrayBufferView in the WebIDL type hierarchy so WebIDL objects would need to add explicit overloads taking SharedArrayBufferView. This is good because SharedArrayBufferView is dangerous to work with, since its contents can be changed asynchronously by other threads.
Then, similar to the previous section, we add a shared-buffer map to to SharedArrayBufferModule. SharedArrayBufferModule gets the following API:
- SharedArrayBufferView takeSharedBuffer(int index): Lets regular JS take a SharedArrayBufferView out of the shared-buffer map, deleting the entry.
Add the following API to SharedArrayBuffer:
- int createSharedBuffer(int fromAddr, int length): Lets asm.js create a new shared-buffer entry and returns its index.