Repository navigation
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.
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
|
|
||
| | 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!
There was a problem hiding this comment.
I think the naming will be bikeshedded a bit, it's not great. The justification for it though is that it's just pulling the globalThis from the JS environment. I don't think we'll want to use wasm:browser or wasm:dom because this JS-API could be implemented on Node or other non-browser environments that have a JS global object.
There was a problem hiding this comment.
I don't think naming it something like wasm:dom would preclude non-browser environments form implementing it, I think actually it would make it clearer that they were bundling support for a DOM-like environment.
I think my main thought here that there is a distinction between the web context and most non-browser-specific objects/operations on globalThis. I think it would be useful to be able to avoiding including DOM globals for a component that did not use them, and include other globals ?
There was a problem hiding this comment.
I don't think naming it something like wasm:dom would preclude non-browser environments form implementing it, I think actually it would make it clearer that they were bundling support for a DOM-like environment
I might be misunderstanding something, but I don't think this is bundling a DOM-like environment. Importing from wasm:js/global is just equivalent to passing the JS globalThis binding to the import. Which lets you import anything that JS exposes on globalThis which could be DOM objects or it might not be.
There was a problem hiding this comment.
Ah I get what you're saying -- what I was thinking of this in terms of a WIT interface is that the Element resource for example would have to be present, and for non-browser platforms it's not present at all. This is what I was trying to represent with the idea of a wasm:dom -- unless I'm misunderstanding, what is present on globalThis varies by platform, and I wanted to capture that difference.
Is the idea that platforms that don't have Element (for example) just fill in constructors that fail/etc?
I think of the capabilities provided by globalThis in the browser context as a WIT world rather than an interface.
There was a problem hiding this comment.
Is the idea that platforms that don't have Element (for example) just fill in constructors that fail/etc?
I think if you have a component that requires DOM interfaces and you want to run it in a JS environment that doesn't have them, then you'll need to polyfill them. Or maybe with optional imports of some kinds the code can detect they're not available.
I think of the capabilities provided by globalThis in the browser context as a WIT world rather than an interface.
In an ESM context, it seems like you sometimes want to import things from globalThis, but sometimes you may want to import from a different ESM. If globalThis was mapped to the top-level world then I don't think you'd be able to represent that.
There was a problem hiding this comment.
I think if you have a component that requires DOM interfaces and you want to run it in a JS environment that doesn't have them, then you'll need to polyfill them. Or maybe with optional imports of some kinds the code can detect they're not available.
Sure that's certainly a viable approach -- I had the same thought about the optional imports, but not sure when that will land, and the problem there is that some of the globals will be missing but not all of them, IIRC optional imports were at the interface level.
In an ESM context, it seems like you sometimes want to import things from globalThis, but sometimes you may want to import from a different ESM. If globalThis was mapped to the top-level world then I don't think you'd be able to represent that.
I was thinking more conceptually than literally here -- something like this:
world browser {
export dom;
export crypto;
export errors;
// more exports here
}The exports "add up" to the globalThis provided by the browser, but as far as specification goes they are separate.
That said, I can definitely see how this wouldn't map well/take additional automation to realize in a WebIDL <-> WIT sense.
|
Just pushed some fixes to address a bunch of comments. I've not gotten to some of the larger semantic reworks yet though. |
lukewagner
left a comment
There was a problem hiding this comment.
Looking great, just a bunch of small nits and suggestions but one question to consider in the final comment.
|
|
||
| #### 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 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. |
There was a problem hiding this comment.
Technically, the per-call-lender-list only exists in the spec for import calls made by a component. If you're calling from the host, there's no handle list because the host doesn't even pass a handle (it passes the rep value). Instead, when the host calls a component export, there is this OnResolve callback that is called with the component export returns its value and, at that moment, you know all borrowed handles have been dropped. So I think what you could say here is that when you make a call into a component, you accumulate all the guest-resource-instance objects (passed to ToComponentValue) into a fresh set S, and then you say that this set S is considered strongly-reachable until the CM spec's OnResolve callback is called.
There was a problem hiding this comment.
(I might be misunderstanding something)
The point of the JS-API lender list here is for when:
- Component exports resource type 'A' (guest resource class)
- JS receives an own instance of 'A' named 'x' (guest resource instance)
- JS calls exported function and passes 'x' to a
borrow<A> - Component now has a handle table entry for 'x' in a borrowed state
- JS still has a reference to 'x' instance and can call dispose on it
In this case, the call to the exported function increments a lend count on the 'x' instance, and dispose checks it to ensure that the JS instance 'x' wasn't lent out.
| 1. Set |jsValue|.[[Rep]] to **empty**. | ||
| 1. Unregister |jsValue| from the [guest resource `FinalizationRegistry`](#guest-resource-finalizationregistry). |
There was a problem hiding this comment.
Separately, in Component-to-Component-via-JS scenarios, I believe the proposal is to not detach/transfer the JS resource handle object (and if JS calls Symbol.dispose, it's JS's "fault" for doing so). But here, there is a detach/transfer going on when passing an own handle, and the circumstances for the difference are pretty subtle. It seems like we should maybe do the same thing in both cases (I think the detach/transfer).
There was a problem hiding this comment.
Yeah, there is an asymmetry here. This guest case is a component receiving one of it's own resources back (and so it has a way to do a transfer). Whereas the host case currently doesn't have a standard way to do that (unless we special case Component-to-Component-via-JS). I'm not sure what to do about it, will think more on it.
lukewagner
left a comment
There was a problem hiding this comment.
A few more small comments. With these, I'm done with a closer read and overall everything looks good to merge once these remaining comments are addressed. Nice work!
| 1. Let |callKind|, |receiverRule| and |paramOffset| be determined by |importName|: | ||
| 1. *Constructor*: `Construct`, no receiver, offset 0. | ||
| 1. *Method* with `scope` "method": `Call`, receiver is component argument 0 (the `borrow` self parameter), offset 1. | ||
| 1. Otherwise: `Call`, receiver is |staticReceiver|, offset 0. |
There was a problem hiding this comment.
I would think that, if we're not making a [method] call, the receiver would be undefined (and you could delete all the |staticReceiver| plumbing here and above). E.g., Object.getOwnPropertyDescriptor(CSS, 'highlights').get.call(undefined) succeeds. (Also, in read a scope of imports in the non-member accessor case, it seems like importsObject can't be the right staticReceiver in any case.)
|
Did a pass to do some fixes. There was just one comment I didn't have time to address yet, and a couple questions back. |
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.