CG-08-26.md

September 24, 2025 · View on GitHub

WebAssembly logo

Agenda for the August 26 video call of WebAssembly's Community Group

  • Where: Virtual meeting
  • When: August 26, 16:00-17:00 UTC (9am-10am PDT, 18:00-19:00 CEST)
  • Location: link on W3C calendar or Google Calendar invitation

Registration

No registration is required for VC meetings. The meeting is open to CG members only.

Agenda items

  1. Opening
  2. Proposals and discussions
    1. Discussion: Configuring JS prototypes with a builtin function (#44, Thomas Lively, 45 minutes)
  3. Closure

Agenda items for future meetings

None

Meeting Notes

Attendees

  • Thomas Lively
  • Conrad Watt
  • Derek Schuff
  • Andrew Brown
  • Brendan Dahl
  • Chris Fallin
  • Chris Woods
  • Deepti Gandluri
  • Emanuel Ziegler
  • Erik Rose
  • Francis McCabe
  • Harry Forbes
  • Jakob Kummerow
  • Jeff Charles
  • Manos Koukoutos
  • Michael Ficarra
  • Sam Clegg
  • Luke Wagner
  • Robin Freyler
  • Ryan Hunt
  • Julien Pages
  • Sean Jensen-Grey
  • Andrew Brown
  • Harry Forbes
  • yijiahuang
  • Yuri Iozzelli
  • Yury Delendik
  • Matthias Liedtke
  • Richard Winterton
  • Zalim Bashorov
  • Bailey Hayes
  • Alex Crichton
  • Ricky Vetter
  • Paolo Severini
  • Heejin Ahn
  • Mendy Berger
  • Sergey Rubanov
  • Dan Gohman

Proposals and discussions

Discussion: Configuring JS prototypes with a builtin function (#44, Thomas Lively)

TL presenting slides

CW: do we want this to be the format that faces the use in JS? Or something more structured? It’s unusual to have a JS API that takes random bytes?

TL: True. This is the compromise to avoid a semantically meaningful custom section and still get good performance (and it’s good). If we made it more readable, performance would suffer. This can be highly optimized.

CW: So if it were e.g. JSON style you’d have to walk it.

TL: Yes, the engine can be optimized here for no allocations, C++ can go through the bytes and interpret in place. It's hard to get faster.

CW: Is there any precedent for this style of API?

JK: Conrad: it's not really a JS API. The byte array is generated by toolchains and consumed by engines, humans won't have to see/modify/understand it. In that sense it's no different from WebAssembly.compile(...) :)

CW: True, but we didn’t do that for wiring up imports and exports; we didn’t pick the most efficient representation for that.

TL: True, although these aren’t user-facing the way those are.

CW: Yeah, we should just understand how badly we’re breaking precedent here.

ZB (chat): What if we need to set in prototype something Symbol based (e.g. iterator)?

TL: Good question.

JK (chat): Zalim: you can always manually modify the prototype objects to account for special cases

TL: Methods with symbol names aren’t supported directly, you'd have to do it separately in JS.

CW: Since we have this binary format, how weird would it be to say, we have a custom section with those bytes, and if you have it, you call this and it reads them.

TL: That’s where we started :D

CW: My impression was the reason we didn’t want custom sections was that we were talking about more and more ambitious sections. But this seems pretty basic, maybe it’s not so bad to make it a custom section?

TL: The only other thing the original one did was provide a way to create the prototypes as well. Here the prototypes are always imported externrefs. There’s no special magic for allocating them. In the original design, you could say how to allocate them in the section. We could turn this into a custom section and it would be a little simpler than before, that would be ok. Marginally simpler.

Now that JK has implemented this, I actually like it better than a custom section. I won’t have to update tools at all to understand it, it’s just a function call, there’s no magic new ways to import and export things, it’s polyfillable. Yes it’s a weird function but I like the general structure of the solution. I think it’s simpler than what else we considered.

LW: Before we had this tension between, exporting things and following the rules, versus punching a hole in the abstraction. Here we’ve solved it with the externref.

TL: The way we got here was that you all argued that this would be simpler, and you were right, even though it’s a little weird in this way.

JK(chat): Luke: the hole-punching now happens under the hood: we recognize the function call and optimize it out and read the module bytes directly, without ever allocating the arrays :)

TL: Yeah that’s where the hole punching belongs.

TL Presenting counter example

MF (chat): What is the encoding used for those names?

TL: The same as any other name in the binary format: UTF-8. This would also not by default support invalid UTF-8.

JK (chat): Michael: regular Wasm way of encoding names, just like for imports and exports. Essentially vec(utf8_bytes)

LW: To understand the optimization: new_elem just makes a copy of the elem segment?

TL: Yes

LW: The result is a mutable array?

TL: Depends on the type of the array. Generally could go either way. In this case we call them mutable for consistency with string builtins.

LW: If you could make them immutable, would optimization be required? Or could you just pass a ref to the backing store of the elem segment?

TL: It’s possible? I’m not sure that optimization exists in practice.

LW: Independent of this particular configureAll, if you created a bunch of array literals, I believe that’s a common JS optimization, you can share and lazy clone them. So maybe a good optimization to have in general.

ML (chat): An element segment doesn't look like a WasmArray. So we can't do that without copy.

JK (chat) yeah, we'd need two different array representations, one that's backed by static sections and one that isn't. Doesn't seem likely any time soon. We can totally change the spec here to require immutable arrays, if anyone (or rather: a majority) prefers that

[missed conversation]

TL: continuing, JS side

Wrapping or stamping?

When a wasm function flows to JS today, there’s no receiver. Also if you try to call a wasm function with new, you can't. So we need 2 mechanisms: one to take the receiver as first param, and another to be callable with new. So you can wrap them, or stamp them as saying they have special behavior. Issue is that stamping is stateful, so the same Wasm function can be visible in different ways: exported, passed as reference, etc. so if you stamp one exported function as a method, the other ways that function becomes visible will also do that. You might not expect that. E.g. binaryen merges 2 functions with the same body. Already not totally sound because it changes identity on the JS side, but this makes it even more unsound because if they were exported separately and only one got stamped, then after the merging optimization, they are both stamped. Questionable how much of an issue this is, since it’s new functionality that didn’t work before, certainly with new. Less clear about receiver.

But stamping is faster.

Maybe we want implementor feedback on the performance?

Personally I lean toward the wrapping approach and hoping the performance is good enough, to avoid action at a distance:

CW: That's my initial opinion too.

LW: So if we have a reference to the function, and pass it out, but later if you to table.get and get the same reference, it also takes a receiver.

TL: Yes. In the JS API , whenever you do ToJSValue, it has a cache, and you get the same JS object out whenever it flows out to JS. so if it’s stamped you always get that.

RH: Can you stamp a function multiple times?

TL: We’d thought of this as a one-way path, once you stamp it you can’t go back. Details can be determined later.

CW: Where does the stamp happen in the current design?

TL: We’d be adding new JS APIs that do the wrapping/stamping manually. And the procedure of interpreting the data array would call the APIs. So whenever you configure a nonstatic method, it would use the API for taking the receiver and install it on the prototype.

LW: If the function passed to configureAll is exposed but never actually get-ed, would it still be wrapped?

TL: In the wrapping approach you would probably still have to allocate the wrapper since you don’t know if it could flow out in the future.

JK (chat): I agree that the statefulness is a bit concerning. That said, two arguments in support of it:

Having the same function exported in multiple ways having the same stamped behavior is likely what you want: if it's designed to be a method, it should always behave like a method.

Arguably if we had designed WasmGC before Wasm-Linear, the stamped behavior of exported function is what we would have started with, because it's pretty much what all OO languages with classes and methods want. That we need to discuss this change now is an artifact of the fact that Wasm-Linear came first.

JK: V8 creates the JS wrappers lazily, but if we create them there’s only one per function.

TL: Presumably if the JS wrapper had its own wrapper…

LW: I was imagining just a different wrapper. On the configureAll path it doesn’t cache, it always makes a different wrapper with the separate behavior for the same Wasm function.

TL: I think that's tricky in the current implementation because there’s a network of objects: JS wrapper, wasm function, etc and they all assume they are 1:1 or 1:0 if no JS is allocated yet. So it might be tricky to support multiple JS wrappers.

JK: Yeah it’s a good idea but might be hard to do. Functions in general are tricky because they are performance and security critical, and need small memory overhead and calling overhead, etc. so it’s hard to change the design.

TL: Certainly the easy path is to add a second layer of wrapping.

RH (chat): I would prefer the wrapping approach. I’m concerned about the ‘spooky action at a distance’ of stamping

JK: We could also consider static stamping on the export, then we would not have the problem of state change. Could be argued that it’s sufficiently useful for any OO language to have that distinction. We might have designed all exported functions to take a receiver if we had designed GC before linear. One way to solve it would be to say we export functions in the export section, and then add a “method-style” export.

CW: Does that end up looking like Luke’s solution where we export the same function in different ways?

JK: Would expect that most modules wouldn’t do that, but we'd have to specify what happens. I would expect most wasmGC modules would just export everything as methods. I don’t see the use case for exporting as both; we could consider making that an error in the first version.

TL: There’s also the problem of functions that aren't exported but still flow out via table or a reference. So it’s really a property of the function itself rather than the export.

RH: Would adding a bit to the export work if the export is of an imported function? That would still be doing a mutation.

TL: Again I guess this would have to be encoded in the function type? To avoid multiple wrappers for the same function. I think we should try the wrapping approach until we know there's a performance problem.

Do we need DescriptorOptions?

I think this will be generated by toolchains, and won’t be too hard to get right.

JK: anecdotal evidence, when Thomas crafted the example, he forgot that DescriptorOptions, it’s easier to reason about it this way.

TL: Would anyone object if I just made a PR to remove it?

CW: we can add them back again if we end up needing to?

TL: Yeah. We are just at phase 2.

CW: What if we go all the way to 4, can we add it back then?

TL: There would be code in the wild then. they would have to make it use the 2nd field instead. It’s not too hard to work around the lack of DescriptorOptions,

JK: It could check if the argument is a DescriptorOptions and have the behavior depend on that. We could add that back in when we add DescriptorOptions. It would add another check, but we could do it.

TL: Yeah e.g. if we wanted to configure ownProperties or something new, we could add it.

RH: If we only ever care about prototypes, then DO might be not necessary. But if we want to configure other things like own properties, etc, then DO seems useful.

TL: Yeah, we could add them for those cases

JK: Ryan: own properties are supported quite well by getters/setters with the current design

CW: We are effectively making a little mini binary format, we should try to design it well and align it with the wasm binary format, document, etc

TL: The only departure we have so far is for the parent index for descriptor chains, where we encode lack of index as -1. Normally we’d use a vector with a max size of 1 to encode optionalness… that’s just less efficient. We can bikeshed.

CW: Yeah just worried that we now have a new format, we should think about it as carefully as the original.

ZB: Questions (to think):

How could it interact with ESM integration? Can we "directly export class" to JS? Can we extend a JS/DOM class with this?

TL: Good question, I haven't thought about ESM integration. We should talk to Guy about that.

LW: Are the constructors created exposed to core wasm as an externref?

TL: Yes, the constructors are wrapped wasm functions installed on this externref that’s passed as tht 4th parameter. So written to an outparam.

LW: So it would show up on the export object

TL: So if you had imported a method for retrieving fields from a JS object, then you could get them from the out param and write them to a mutable exported global. A bit convoluted.

JK: It probably works to pass the JS global object as the constructor, and then everything would get installed as JS globals, which is probably what you’d want.

Zalim: You can install a JS class's prototype as the parent prototype, though I'll admit I'm not sure how useful that'd be.

TL presenting alternative methods

configureOne: we don’t think it’s needed at this point. If we had a user ask for it, we wouldn’t have so many open questions about how it would look

ZB: How about extending JS/DOM classes, is it possible?

TL: You could put the class in your prototype chain, would have to think more about the use case. Please file an issue so we can discuss more.

Closure