Skip to content

Commit a7c2481

Browse files
authored
Describe a "distinguished value" API for JS prototypes (#7)
The JS interop design previously described in the overview depended on the identities of prototypes attached to custom descriptors being mutable. Implementer feedback from both SpiderMonkey and V8 was that the identities need to be immutable, so that design was not feasible. Although the conversation about what the final design should look like is not over, replace the old design in the overview with one that meets more of our requirements.
1 parent 46de3c4 commit a7c2481

File tree

1 file changed

+82
-19
lines changed

1 file changed

+82
-19
lines changed

proposals/custom-descriptors/Overview.md

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -378,39 +378,90 @@ C |- br_on_cast_desc_fail l rt_1 rt_2 : t* rt_1 (ref null exact_1 y) -> t* rt_2
378378
In JS engines,
379379
WebAssembly RTTs correspond to JS shape descriptors.
380380
Custom descriptors act as first-class handles to the engine-managed RTTs,
381-
so they can serve as extension points for the JS reflection of the Wasm objects they describe.
382-
383-
We introduce a new `WebAssembly.setDescriptorPrototype()` API
384-
that takes a custom descriptor instance and a prototype object.
385-
The provided prototype will be attached to the descriptor's RTT
386-
and will become the prototype for the JS reflection
387-
of all Wasm objects the custom descriptor describes.
388-
389-
The following is a full example that uses `WebAssembly.setDescriptorPrototype()`
381+
so they can serve as extension points for the JS reflection of the Wasm objects they describe,
382+
and in particular they can allow JS prototypes to be associated with the described objects.
383+
To make this work, we allow information about the intended JS reflection of Wasm objects
384+
to be imported into a Wasm module and held by custom descriptors.
385+
The `[[GetPrototypeOf]]` algorithm for a WebAssembly object can then look up this information
386+
on the object's custom descriptor. The details of how this works are described below.
387+
388+
We introduce a new `WebAssembly.DescriptorOptions` type
389+
that holds relevant information about the JS reflection of Wasm objects.
390+
A `DescriptorOptions` is constructed with an option bag containing
391+
an object to be used as a prototype. Other options may be added in the future,
392+
for example to expose Wasm struct fields as own properties.
393+
394+
```webidl
395+
dictionary DescriptorOptionsOptions {
396+
object? prototype;
397+
};
398+
399+
[LegacyNamespace=WebAssembly, Exposed=*]
400+
interface DescriptorOptions {
401+
constructor(DescriptorOptionsOptions options);
402+
}
403+
```
404+
405+
A `DescriptorOptions` object has a `[[WebAssemblyDescriptorOptions]]`
406+
internal slot with the value `true`.
407+
This allows it to be identified by the `[[GetPrototypeOf]]` algorithm.
408+
Its constructor copies all of the options into the constructed `DescriptorOptions`.
409+
410+
The specification of the `[[GetPrototypeOf]]` internal method
411+
of an Exported GC Object `O` is updated to perform the following steps
412+
(which will be made more precise in the final spec):
413+
414+
1. If `O.[[ObjectKind]]` is not "struct":
415+
1. Return `null`.
416+
1. Let `store` be the surrounding agent's associated store
417+
1. Look up the object's heap type from the store.
418+
1. If the heap type does not have a descriptor clause:
419+
1. Return `null`.
420+
1. Get the descriptor value and descriptor type.
421+
1. If the descriptor type has has no fields or its first field is not immutable or does not match `externref`:
422+
1. Return `null`.
423+
1. Get the value `v` of the first field.
424+
1. Let `u` be `ToJSValue(v)`.
425+
1. If `u` does not have a `[[WebAssemblyDescriptorOptions]]` internal slot:
426+
1. Return `null`.
427+
1. Return the prototype stored in `u`.
428+
429+
> Note: it would also be good to ensure a `DescriptorOptions` is opaque and can
430+
> only be used once to avoid having to keep the configuration data live for the
431+
> lifetime of the custom descriptor. TODO.
432+
433+
The only new capability required in the WebAssembly embedding interface is the
434+
ability to inspect a reference's heap type.
435+
The algorithm also needs to access the value's descriptor and its fields,
436+
but in principle it could do that by synthesizing a new Wasm instance
437+
exporting the functions necessary to perform that access,
438+
so those would not be fundamentally new capabilities.
439+
440+
The following is a full example that uses `WebAssembly.DescriptorOptions`
390441
to allow JS to call `get()` and `inc()` methods on counter objects implemented in
391442
WebAssembly.
392443

393-
> Note: If there is demand for it,
394-
> a similar API could configure property names that JS could use to access fields
395-
> of the described WebAssembly objects.
396-
397444
```wasm
398445
;; counter.wasm
399446
(module
400447
(rec
401448
(type $counter (descriptor $counter.vtable (struct (field $val i32))))
402449
(type $counter.vtable (describes $counter (struct
450+
(field $proto (ref extern))
403451
(field $get (ref $get_t))
404452
(field $inc (ref $inc_t))
405-
))
453+
)))
406454
(type $get_t (func (param (ref null $counter)) (result i32)))
407455
(type $inc_t (func (param (ref null $counter))))
408456
)
409457
458+
(import "env" "counter.proto" (global $counter.proto (ref extern)))
459+
410460
(elem declare func $counter.get $counter.inc)
411461
412-
(global $counter.vtable (export "counter.vtable") (ref exact $counter.vtable)
462+
(global $counter.vtable (ref exact $counter.vtable)
413463
(struct.new $counter.vtable
464+
(global.get $counter.proto)
414465
(ref.func $counter.get)
415466
(ref.func $counter.inc)
416467
)
@@ -440,20 +491,32 @@ WebAssembly.
440491

441492
```js
442493
// counter.js
443-
var {module, instance} = await WebAssembly.instantiateStreaming(fetch('counter.wasm'));
444494

445-
WebAssembly.setDescriptorPrototype(instance.exports['counter.vtable'], {
446-
get: function() { instance.exports['counter.get'](this); },
447-
inc: function() { return instance.exports['counter.get'](this); }
495+
var counterProto = {};
496+
497+
var counterOpts = new WebAssembly.DescriptorOptions({
498+
prototype: counterProto
448499
});
449500

501+
var {module, instance} = await WebAssembly.instantiateStreaming(fetch('counter.wasm'), {
502+
env: {
503+
"counter.proto": counterOpts
504+
}
505+
});
506+
507+
counterProto.get = function() { return instance.exports['counter.get'](this); };
508+
counterProto.inc = function() { instance.exports['counter.inc'](this); };
509+
450510
var counter = instance.exports['counter'];
451511

452512
console.log(counter.get()); // 0
453513
counter.inc();
454514
console.log(counter.get()); // 1
455515
```
456516

517+
> Note: Other API designs are also possible.
518+
> See the discussion at https://github.com/WebAssembly/custom-rtts/issues/2.
519+
457520
## Declarative Prototype Initialization
458521

459522
TODO

0 commit comments

Comments
 (0)