Conversation
|
I have a few dumb questions
|
|
Apologies for the delay, I was rewriting most of the explainer for a new approach. I'm less convinced now that specifying this in terms of WebIDL is the right approach and so I'm switching back to this being a JS-API. I believe we can get good performance for web API calls by designing value conversions such that JS script cannot intercept them when a web API is imported directly. The new approach attempts this, and it's not as bad as I thought it would be. It's not quite ready for review but is closer now. I have answers to your questions, but they're in terms of the discarded draft so I'm not sure it's relevant anymore. |
|
@eqrion so, something that i've run into as a Rust WebAssembly enjoyer is that even though on paper Rust compiled to webassembly should be faster than javascript, in practice they're about the same speed, and in fact Rust can be slower compared to javascript when it comes to using apis like WebGPU or WebGL2. will this new api fix this? |
|
@ValorZard Performance is going to be very workload dependent. The biggest thing this could provide is to lower the overhead for calling from Rust into a web API. But it's still possible to have bottlenecks in your rust code that make it slower than JS. JS engines are marvelous things and are not an easy target to beat consistently. |
|
I think this is ready for review now. Tagging Luke for review. cc @bvisness @guybedford @vados-cosmonic. Feel free to look at this if you're interested, but no worries if not. |
lukewagner
left a comment
There was a problem hiding this comment.
Great work; exciting to see this all in detail! There's a lot and so I didn't get to read all of it and only skimmed some parts, but here's a first pass of comments/questions.
| instance.exports.greet(); // TypeError | ||
| ``` | ||
|
|
||
| Passing too few arguments is a `TypeError`. Extra arguments are ignored. |
There was a problem hiding this comment.
Would it make sense to allow trailing optional parameters to be omitted?
| | `list<u8>` | `Uint8Array` | | ||
| | `list<T>`, `list<T, N>`, `tuple<T, U>` | Array | |
There was a problem hiding this comment.
Would we want the other fixed-width integers to also turn into their respective typed arrays? And perhaps also the list<T, N> variants too?
| import { run } from "./component.wasm"; | ||
|
|
||
| // calls the default export of `https://esm.unpkg.com/slugify@1.6.6` | ||
| run(); |
There was a problem hiding this comment.
Since run() has no params/results, maybe convert it to use a core start function and have the first demo just use <script type="module" src="./component.wasm"> (mentioning that this .html+.wasm is a complete web app without any JS code). And then maybe as a second example add an export with params/results and show what it looks like to import and call from JS like you're doing here. And then to complete the ESM high-level picture, maybe show a component that imports some name without an external-id and use an import-map to map it to a URL, and then mention that this is one way that non-browser APIs like WASI can be polyfilled on the Web.
|
|
||
| ### Importing from the JS global | ||
|
|
||
| The example above still needs someone to write `{ element: Element }`. A component can skip that and take its imports straight from the global object by importing `wasm:js/global`: |
There was a problem hiding this comment.
Pretty cool that you only need one fixed JS built-in and the rest is done with instance type projects; great idea!
|
|
||
| ### Greeter: exporting a function | ||
|
|
||
| Let's start with a component that imports nothing: |
There was a problem hiding this comment.
Technically this is the type of a component (it'd fail validation as a standalone .wat, but would pass if you wrapped it with (component (type ...))). Since the convention is used (effectively, since the types are mostly all we care about) throughout the explainer, it might be good to clarify that here or in the introductory text above.
| 1. Perform `DefinePropertyOrThrow`(|target|, `JSName`(|e|), PropertyDescriptor { [[Value]]: |func|, [[Writable]]: **true**, [[Enumerable]]: **false**, [[Configurable]]: **true** }). | ||
| 1. Return |constructor|. | ||
|
|
||
| A method named `constructor` and a static named `prototype` are rejected because they would unexpectedly change JS class semantics. |
There was a problem hiding this comment.
Along the same lines, I think perhaps arguments, caller, name and length might conflict with predefined class names. I wonder if, rather than throwing a type-error (which might actually be a problem for some of these), these methods are silently omitted but can still be found via their full original plainname on the exports object (just like interfacename exports), and thus these methods/statics/constructors are really just syntactic sugar.
|
|
||
| #### Conversions for guest resources | ||
|
|
||
| The *current lender list* is a per-call spec state. `invoke a component function` establishes it for a JS-to-component call. Each instance lowered as a `borrow` during that call has its [[LendCount]] incremented and is appended to the list, which protects it from being dropped while lent. When the call returns, every [[LendCount]] in the list is decremented. |
There was a problem hiding this comment.
| The *current lender list* is a per-call spec state. `invoke a component function` establishes it for a JS-to-component call. Each instance lowered as a `borrow` during that call has its [[LendCount]] incremented and is appended to the list, which protects it from being dropped while lent. When the call returns, every [[LendCount]] in the list is decremented. | |
| The *current lender list* is a per-call spec state. `invoke a component function` establishes it for a JS-to-component call. Each guest resource instance lowered as a `borrow` during that call has its [[LendCount]] incremented and is appended to the list, which protects it from being dropped while lent. When the call returns, every [[LendCount]] in the list is decremented. |
|
|
||
| Converting the same JS value to a host resource type yields fresh handle indices. There is no canonicalization of indices. | ||
|
|
||
| ### Guest resource types (i.e. exported) |
There was a problem hiding this comment.
IIUC, Guest resource types can be imported via the JS API by other components, in which case they are treated as Host resource types. It might be worth adding a short section mentioning this... somewhere.
| To `brand check` given a JS value |jsValue| and an Object |constructor|: | ||
| 1. If |constructor| is a WebIDL [interface object](https://webidl.spec.whatwg.org/#dfn-interface-object): | ||
| 1. Return **true** if and only if |jsValue| is a platform object that [implements](https://webidl.spec.whatwg.org/#implements) the interface |constructor| is the interface object of. | ||
| 1. If |constructor| has a [[ConstructorFunc]] internal slot (i.e. it is a [guest resource class](#guest-resource-classes)): |
There was a problem hiding this comment.
Would this apply not just to exported guest resource types but also imported host types that happen to be implemented by a previous component's exported resource type? If so, might be useful to mention that there are two ways this case applies.
| 1. Let |hostType| be |instance|.[[HostResourceTypes]][|abstractTypeKey|]. | ||
| 1. Assert: |rep| is a host resource value whose [[Type]] is |hostType|. | ||
| 1. Return |rep|.[[JSValue]]. | ||
| - `ToComponentValue(jsValue, own<R> | borrow<R>)`: |
There was a problem hiding this comment.
If the host value is really a Guest resource instance (returned from some other component's export), it seems like ownership needs to be transferred (in the form of emptying the [[Rep]] of the Guest resource instance) so that there is not a double drop.
vados-cosmonic
left a comment
There was a problem hiding this comment.
This is a great start, took a pass at the explainer and reference pages and they look good so far, mostly nits/organizational (and emphasizing easy readability of the explainer) and some questions about conversions and WebIDL layout
| @@ -0,0 +1,306 @@ | |||
| # WebAssembly Components JS-API Explainer | |||
|
|
|||
| This explainer describes how WebAssembly Components (hereafter 'components') can be used from JS. | |||
There was a problem hiding this comment.
Do we want to be more explicit about what we mean when we say "from JS"? It will likely be a while until we have full implementation support for all places people run JS.
I'm generally thinking of something like "can be used on runtimes that support the Component Model JS-API" ?
| ### Greeter: exporting a function | ||
|
|
||
| Let's start with a component that imports nothing: | ||
|
|
There was a problem hiding this comment.
| ### Greeter: exporting a function | |
| Let's start with a component that imports nothing: | |
| ### Components that export | |
| Let's start with a `greeter` component that imports nothing, but exports functionality: | |
The gist of this suggestion is that I think the "greeter" example is kind of a side-effect of doing the explanation of export-only components here, so figure that should take precedence as the title
BTW, we might also want to start with/use the WIT for the component actually rather that the WAT
|
|
||
| ```js |
There was a problem hiding this comment.
Just to make things painfully clear, I think right now WebAssembly.instantiate does not support WAT as input, so we might want to have a transition sentence here that notes that the WAT is converted to a WebAssembly's binary. That said, if Luke's note about being clear the WAT is a fragment is taken up then this sentence could look different/be unnecessary of course.
Also, if we switch to WIT instead of WAT for the initial description, we could lead with something like "Given a WebAssembly binary that implements the WIT above", etc.
| instance.exports.greet("world"); // "hello, world" | ||
| ``` | ||
|
|
||
| `exports` holds one property per export and `greet` is an ordinary JS function. Component names are kebab-case and JS names are [camelCase](./JS-Reference.md#names), so an export named `greet-loudly` would be `greetLoudly`. |
There was a problem hiding this comment.
| `exports` holds one property per export and `greet` is an ordinary JS function. Component names are kebab-case and JS names are [camelCase](./JS-Reference.md#names), so an export named `greet-loudly` would be `greetLoudly`. | |
| The `exports` property of an instance holds one property per export and `greet` is an ordinary JS function. | |
| Component export names are kebab-case and JS names are [camelCase](./JS-Reference.md#names), so an export named `greet-loudly` would be `greetLoudly`. |
| ``` | ||
|
|
||
| Passing too few arguments is a `TypeError`. Extra arguments are ignored. | ||
|
|
There was a problem hiding this comment.
I wonder if we should add some explanation around interfaces as exports, since they introduce a kind of nesting here. instance.exports.<iface> versus instance.exports.<fn>
| | *Plain* | `^(?<name><label>)$` | `query-selector` | | ||
| | *Property* | `^\[(?<accessor>get\|set)\](?<name><label>)$` | `[get]inner-HTML` | | ||
| | *Constructor* | `^\[constructor\](?<resource><label>)$` | `[constructor]element` | | ||
| | *Member* | `^\[(?<scope>method\|static)\](?:\[(?<accessor>get\|set)\])?(?<resource><label>)\.(?<name><label>)$` | `[method][set]element.inner-HTML` | |
There was a problem hiding this comment.
Should we add a note after this about component model uniqueness here?
Unrelated to this section for example [method]foo.bar and [static]foo.bar actually would be an issue I think -- might be worth mentioning that the names are subject to those upstream rules.
|
|
||
| While walking, the algorithm recognizes the pattern of a resource type import accompanied by *Constructor* and *Member* function imports naming it. A resource type import is read first and looks for a constructor (see [resource types](#resource-types)). Those function imports then read from the constructor and its prototype directly. This allows the common case of importing a class to be satisfied by just passing the constructor. | ||
|
|
||
| Passing an exported component definition to a component import via the JS-API/ESM-integration is treated as if the import were a JS value. There is no "direct linking" that bypasses going through JS semantics. This is different from core wasm, where exported functions are linked directly when imported and have stricter type checks. This is intentional to prevent the implementation detail of how a JS function was implemented from leaking. Components can still be nested and directly linked inside a single component binary. |
There was a problem hiding this comment.
Will it be ever possible to change this in the future? i.e. should we have something like "there is not currently" any direct linking?
|
|
||
| The component JS-API can provide builtins to imports just as the core JS-API does. | ||
|
|
||
| Builtin imports are opt-in via the `builtins` field of `WebAssemblyCompileOptions` when used in the JS-API. [ESM-integration](#webassembly-esm-integration) enables all builtins by default. |
There was a problem hiding this comment.
OK I don't think this matches the WebIDL right? We should probably have a component-specific compile options object if I'm reading this right
|
|
||
| | Specifier | Resolves to | | ||
| |---|---| | ||
| | `wasm:js/global` | [the global object](#the-global-object) | |
There was a problem hiding this comment.
I do wonder if this should actually be wasm:browser/wasm:dom or something (or maybe split out into multiple packages/interfaces), given that some browser JS globals are not JS globals, and what not, but that's a much wider discussion!
| |---|---|---| | ||
| | bare type, function, value | `import v from "JSSpecifier(decl)"` | the [default export](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-ImportedDefaultBinding) | | ||
| | instance | `import { a, b } from "JSSpecifier(decl)"` | one [named import](https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#prod-NamedImports) per export of the instance type whose name is an `interfacename` or matches *Plain*, named `JSName` of that export | | ||
| | core module, component | `import source M from "JSSpecifier(decl)"` | the module source, as a `Module` or `Component` | |
There was a problem hiding this comment.
Should we link to the source phase https://github.com/tc39/proposal-source-phase-imports proposal here?
I'm presenting on Mozilla's prototype implementation which is based off of this, so I wanted to have it publicly accessible somewhere. It needs a couple more iterations before it's ready for detailed review. High-level feedback is welcome though.