Skip to content

Write new JS-API explainer and reference - #686

Open
eqrion wants to merge 15 commits into
WebAssembly:mainfrom
eqrion:web
Open

eqrion wants to merge 15 commits into
WebAssembly:mainfrom
eqrion:web

Conversation

@eqrion

@eqrion eqrion commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

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.

@ValorZard

Copy link
Copy Markdown

I have a few dumb questions

  1. What parts of the Web APIs DON'T fall under WebIDL?
  2. When interacting with javascript types and functions through webidl, are those objects still garbage collected, or does the wasm component take full ownership of them. If I'm writing a rust WASM component for the web, and I get access to a Button, is that button garbage collected? And if so, how? Can I "free" that button in the Rust component and accidentally remove it from the browser somehow?

@eqrion

eqrion commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

@ValorZard

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 eqrion changed the title Initial design sketch for a web embedding to replace the JS-API Initial design sketch for an revised JS-API Aug 21, 2026
@ValorZard

Copy link
Copy Markdown

@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.
For example (and don't quote me on this), I think the Bevy game engine's web renderer is slower than three.js because three.js is natively written and optimized for javascript.

will this new api fix this?

@eqrion eqrion changed the title Initial design sketch for an revised JS-API Initial design sketch for a revised JS-API Aug 24, 2026
@eqrion

eqrion commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

@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.

@eqrion eqrion changed the title Initial design sketch for a revised JS-API Write new JS-API explainer and reference Sep 17, 2026
@eqrion
eqrion requested a review from lukewagner September 17, 2026 16:02
@eqrion

eqrion commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

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 lukewagner left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread design/mvp/JS-Explainer.md Outdated
instance.exports.greet(); // TypeError
```

Passing too few arguments is a `TypeError`. Extra arguments are ignored.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to allow trailing optional parameters to be omitted?

@bvisness bvisness Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect this already because I would expect them to be considered undefined.

Comment on lines +89 to +90
| `list<u8>` | `Uint8Array` |
| `list<T>`, `list<T, N>`, `tuple<T, U>` | Array |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are enough flavors of typed array that this would encompass literally all numeric types. Is that what we want?

I could see it being the case that list<N> is always returned to JS as a typed array, but that params of type list<N> would accept plain old JS arrays, using the same behavior as new <Type>Array([ ... ]) or equivalently TypedArray.from().

Still, it's a little funny to have some lists become TypedArrays and others become normal Arrays, but the benefits of using typed arrays for e.g. list<u8> seem so strong that it's probably the right decision.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think we could have ToJSValue always produce a typed array for scalars, but then allow ToComponentValue to still accept arrays.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have changed my mind and opened #731 instead.

import { run } from "./component.wasm";

// calls the default export of `https://esm.unpkg.com/slugify@1.6.6`
run();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread design/mvp/JS-Explainer.md
Comment thread design/mvp/JS-Explainer.md
Comment thread design/mvp/JS-Reference.md Outdated
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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread design/mvp/JS-Reference.md Outdated
Comment thread design/mvp/JS-Reference.md
Comment thread design/mvp/JS-Reference.md Outdated
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>)`:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. That would violate the 'no direct linking' direction where host resource imports are treated the same no matter whether it's a true JS class or implemented with a component. I'm not sure I understand the double drop case. The guest resource class instance will be a JS value that has a rep and a finalization entry. When imported as a host resource, it'll just hold that JS value to extend the lifetime so that it doesn't get finalized. If we have a 'Symbol.dispose' on guest resource classes (as suggested above) that trigger drops, that'll cancel the finalization registry and won't lead to a double drop.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was more thinking about the other direction: what if JS is given an owned handle, the object-handle-wrapper is passed into a component via own handle, and then JS calls Symbol.dispose on its object-handle-wrapper while the component is still holding its own handle (which it could then try to resource.drop, leading to double-drop, or use, leading to use-after-free -- or maybe both cases trap, but that's weird).

I wonder if, in the same way that the JS-API (might) detect-and-call Symbol.dispose, there's a Symbol.transfer (eventually... in the short term there could be a hard-coded list of object types, kinda like what the branding check does) so that when a JS object is passed into an owned handle, Symbol.transfer is called, if present, with the expectation that the original object gets nerfed. ArrayBuffer detachment (which literally has an ArrayBuffer.transfer()) would be another, non-wasm example.

So then the net result would be: if you have a JS object that wraps an owned handle and pass it into a component, your JS object is mutated to hold a null handle that throws if you do anything with it.

@vados-cosmonic vados-cosmonic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread design/mvp/JS-Explainer.md Outdated
Comment thread design/mvp/JS-Explainer.md Outdated
Comment thread design/mvp/JS-Explainer.md
Comment thread design/mvp/JS-Explainer.md Outdated
```

Passing too few arguments is a `TypeError`. Extra arguments are ignored.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that'd be good as a dedicated section on importing/exporting instances. I'll see about adding that. The explainer is definitely not complete, it's missing some areas like this.

Comment thread design/mvp/JS-Reference.md
Comment thread design/mvp/JS-Reference.md Outdated
Comment thread design/mvp/JS-Reference.md
Comment thread design/mvp/JS-Reference.md Outdated

| Specifier | Resolves to |
|---|---|
| `wasm:js/global` | [the global object](#the-global-object) |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ?

Comment thread design/mvp/JS-Reference.md Outdated
@eqrion

eqrion commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator Author

Just pushed some fixes to address a bunch of comments. I've not gotten to some of the larger semantic reworks yet though.

```

```js
const imports = { element: Element };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If type names are converted to PascalCase in JS, then why is this not { Element } (or equivalently { Element: Element })?

Comment on lines +89 to +90
| `list<u8>` | `Uint8Array` |
| `list<T>`, `list<T, N>`, `tuple<T, U>` | Array |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have changed my mind and opened #731 instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants