-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add RFC for Deref
in web-sys
#5
Conversation
This looks great to me, and I think it is the right set of trade offs given the choices we have. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this looks great, thanks! I generally approve, but I listed a few minor comments.
transitively. This is then used in the code generator to generate `AsRef` | ||
implementatiosn for `Element`. | ||
|
||
The code generation of `#[wasm_bindgen]` will be updated with the following |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a footgun. Perhaps it could be split into two separate attributes, like this?
#[wasm_bindgen(parent = Node, extends = Object)]
So that way it's crystal clear what's going on, and it's harder to make mistakes (and if somebody does make a mistake, such as using extends
without parent
, it can print a nice error message).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I'd be fine adding that in the future, but for now allowing multiple extends
while still implementing Deref
is a backwards-compatibility requirement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also think long term we don't want a new attribute, but rather only accepting one extends
attribute.
(and maybe adding a special attribute for generated AsRef
impls)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alexcrichton Is it a backwards compatibility requirement? The RFC itself says that web-sys will have to be changed so that it always puts the immediate parent first, so this requirement requires code changes. The same is true for non-web-sys bindings as well.
So I don't see how this enables backwards compatibility. In fact, it may even be a huge footgun: some existing code might put the wrong class first, but this doesn't give a warning or error, so they don't realize it.
If we instead required a new parent
attribute, we would give a nice error message if it's missing, and this lets people know that they need to change their code so that the immediate parent is correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes this would otherwise be a breaking change for wasm-bindgen
, which we're not doing at this time. web-sys
can be updated easily, other crates cannot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Somehow that still feels wrong. If there exists code where the first extends
isn't the immediate parent, then they will now get an automatic Deref
impl which is wrong (or at least unexpected).
We could instead tie Deref
to the parent
attribute, so that if code wants the Deref
impl they have to change to use parent
(existing code without parent
will keep the old behavior and not have a Deref
impl).
text/000-structural-and-deref.md
Outdated
|
||
An easy solution to this problem is to simply use `structural` everywhere, so... | ||
let's propose that! Consequently, this RFC proposes changing `#[wasm_bindgen]` | ||
to act as if all bindings are labeled as `structural`. This will not be a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically this is a breaking change, since somebody might be relying upon the non-structural behavior.
I think that's very unlikely though, so I'm personally fine with not treating it as a breaking change.
breaking change because the generated bindings will still have the same behavior | ||
as before, they'll just handle subclassing correctly! | ||
|
||
### Adding `#[wasm_bindgen(final)]` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like the name final
, because final
means "this method cannot be overridden", but we're using it to mean "this method can be overridden, but if it is overridden it will have subtly wrong behavior".
Maybe prototype
is a good alternative? Or perhaps static_prototype
to be even more specific.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we're using it to mean "this method can be overridden, but if it is overridden it will have subtly wrong behavior"
Well, I'd argue we're using it to mean "this method should not be overridden". Yes, there is a small distinction between that and "this method can not be overridden", in that we can't prevent the method from being overridden, but I don't think people will assume that we somehow can prevent it, since we're only providing bindings. So I don't really see the issue.
Either way, I think the distinction between the two meanings is small enough that we should reuse the well-known name "final" rather than trying to invent a completely new name for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Pauan I think "subtly wrong behavior" is too strong here, but rather final
indicates that this method, like final
indicates, will not be overridden and regardless of class passed in it'll call this precise method defined for this precise class. That's "subtly wrong" when applied incorrectly but it can also be applied correctly in which case it's not "subtly wrong".
In that sense I agree with @migi, the name final
invokes thoughts about the right concept, namely that calling this method will always call what's being defined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@migi but I don't think people will assume that we somehow can prevent it, since we're only providing bindings.
You would be surprised, especially when we're using well-established OOP terms.
@alexcrichton I think "subtly wrong behavior" is too strong here
"subtly wrong behavior" is correct. We're talking about the user expecting it to call Bar::some_method
but instead it calls Foo::some_method
. What would you call that if not "subtly wrong behavior"?
but rather final indicates that this method, like final indicates, will not be overridden
That's my point: with final
in wasm-bindgen, you can override the method. And this isn't hard to do either: it's possible for class hierarchies to be spread across multiple Rust crates, meaning that a downstream crate might override some methods on an upstream crate (unaware that they shouldn't do this, because of final
).
That's "subtly wrong" when applied incorrectly but it can also be applied correctly in which case it's not "subtly wrong".
That's why I specifically said if the method is overridden it will have subtly wrong behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Pauan I would not call it "subtly wrong behavior" because it simply is not. You'd have to go out of your way to write down final
which means you probably know what you're doing, so saying that you intended a subclass method I think is wrong because a manual annotation was added to say "I don't want this behavior".
If the final
modifier is applied blindly without understanding it then that results in the same misunderstanding from blindly applying anything without understanding it.
Also, no, you cannot override final
. When you say final
you're saying you're calling precisely the method indicated, no others. When calling the final
import function you'll always call that one, no overridden function. We're not defining JS classes in Rust so we have no control over how the JS hierarchy looks in JS, but the bindings are for the Rust hierarchy and defining what's happening in Rust.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, no, you cannot override final. When you say final you're saying you're calling precisely the method indicated, no others. When calling the final import function you'll always call that one, no overridden function
I am aware of that. You are right that it will always call Foo.prototype.someMethod
, but that's my point: that might cause subtle (and not so subtle!) problems. As an example, consider this code:
class Foo {
constructor() {
this.someProperty = "hi";
}
someMethod() {
return this.someProperty.slice(0, 0);
}
}
class Bar extends Foo {
constructor() {
super();
this.someProperty = null;
}
someMethod() {
return "";
}
}
new Bar().someMethod() // returns ""
Foo.prototype.someMethod.call(new Bar()) // errors
This is obviously contrived, but I'm sure similar situations do happen out in the wild.
In this case Bar
extends Foo
, so it has the same internal state (plus maybe some additional state). It can change its own state in such a way that Foo.prototype.someMethod
is now broken/wrong (but Bar.prototype.someMethod
is correct).
There's plenty of other situations as well: such as some code relying upon the fact that Bar.prototype.someMethod
is called (because it updates the internal state of Bar
), so calling Foo.prototype.someMethod
causes weird/broken behavior.
You might say "oh, but then you shouldn't use final
", and you're absolutely right. It's easy to see that in isolation, but I'm talking about a situation like this:
-
Somebody defines a
rust-foo
Rust crate which creates bindings for thenpm-foo
package.Since they're not overriding any methods on the classes (and perhaps due to some confusion with how
final
works in OOP languages), they mark the methods asfinal
. Looking at therust-foo
crate in isolation, this seems okay (since no methods are being overridden):#[wasm_bindgen(module = "npm-foo")] extern { type Foo; #[wasm_bindgen(method, final)] fn some_method(this: &Foo); }
-
Somebody else creates a
rust-bar
Rust crate which creates bindings for thenpm-bar
package. Thenpm-bar
package has some classes which extend the classes defined innpm-foo
, and so likewise therust-bar
crate puts in someextends
attributes (which point to the bindings created inrust-foo
):#[wasm_bindgen(module = "npm-bar")] extern { #[wasm_bindgen(extends = Foo)] type Bar; }
-
The classes in
npm-bar
override some methods, but the creator of therust-bar
crate is unaware of this (and even if they were aware, they cannot control therust-foo
crate, so they can't remove thefinal
attribute).
Basically, it's a situation where in isolation final
seems reasonable, but it ends up being wrong in practice.
Personally, I feel that the semantics of final
in wasm-bindgen are different enough (and the use case is niche enough) to warrant a different name than final
. But I'm not going to fight you on that.
In any case, the docs will need to be very clear about the following points:
-
Exactly what
final
does (and how it differs fromfinal
in OOP languages, in particular it does not prevent overriding the method). -
You almost never need
final
(it's strongly preferred to not use it). -
You should only use
final
if you can guarantee that the method will not be overridden by you or by anybody else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree about those last 3 bullet points: the docs should indeed say those things clearly.
But here's the thing: To misunderstand the final
keyword in the way you fear would happen, you basically have to not have read the docs on final
at all. To think that final
prevents overriding is a very different meaning from what final
actually does. But since the author of the rust-foo
binding has to explicitly write that final
attribute, I think it's fair to assume that he would have read the docs, at least partially.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean if we really want to emphasize the fact that you should be careful when marking a function "final" we could use the name unsafe-final
, but that's putting a lot of emphasis on an overall pretty minor thing.
text/000-structural-and-deref.md
Outdated
generate JS that looks like: | ||
|
||
```rust | ||
#[wasm_bindgen(method, structural)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason this is marked as structural
even though above it doesn't use structural
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a mistake
* Edge 42, `structural` is 15% faster | ||
* Safai 12, `structural` is 8% slower | ||
|
||
and these numbers look quite different! There's some strong data here showing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this is good data. It also aligns with my personal beliefs about JS engines: because structural
is much more common in JS than final
, it makes sense that the JS engines heavily optimize for structural
.
Oh, by the way, it might be worth pointing out that So regardless of what the default is, it won't affect |
Excellent write-up. It explains the situation as I see it very well. I agree with essentially all of it. I'm strongly in favor of making methods structural by default. I'm weakly in favor of |
@Pauan ah yes, that was my intention but I forgot to explicitly say it. When |
An article was just posted that is very relevant to the benchmarks in this RFC: Calls between JavaScript and WebAssembly are finally fast 🎉. A brief summary: Firefox just implemented some optimizations that allows wasm to call JIT-ed JavaScript directly, and vice versa, without going through a C++ "trampoline". The optimizations made wasm->JS calls about 2x faster, and both JS->wasm and wasm->built-ins about 10x faster. It may be worth re-running the Firefox benchmark using the latest Firefox nightly to see if these optimizations dramatically affect the performance trade-off between |
A good point! Some testing shows, however, that there's not much difference. On today's nightly Firefox build |
Bikeshedding the attribute name:
|
I'd like to now propose that we enter the Final Comment Period for this RFC. Disposition: merge @rustwasm/core members to sign off: I think the remaining issue of attribute names can be figured out during FCP, but if changing I'd lean towards the name of |
I am in favor of merging this RFC, but I (also) think it is worth bike shedding the name of the non-structural attribute a little more (and am also happy to do this in FCP time).
In C++ land, Also happy with |
Since the semantics of JS are weird (prototypal inheritance, etc.) I'm in favor of a JS-specific name. Many good options have been suggested, I don't have a strong opinion about them, but I do like Since it should be used rarely, it's okay if the name is weird, long, and unwieldy. |
I'm also in favor of this RFC. At least the technical aspects of it. As far as the name of the attribute goes, I don't care very much one way or the other (it's just a name after all), but I do want to present a few more points in favor of
For example, if a JavaScript class extends another one, you mark the imported type as Say (hypothetically) that a future EcmaScript version would add "final" as a keyword to mark methods as non-overridable (they almost did, in the (abandoned) ES4 proposal). Then any method that is
|
All @rustwasm/core team members have signed off. Entering the 7 day ✨ Final Comment Period ✨! |
Ok! That concludes the 7-day FCP, and I've gone ahead and merged this. It does seem like the naming is still somewhat up in the air, but I figure it's ok to go ahead and start getting to the implementation |
For those interested I have an implementation of this at rustwasm/wasm-bindgen#1019 where I've proposed renaming |
Implement rustwasm/rfcs#5, implement `Deref` for imports and `structural` by default
This proposal is a formal alternative to RFC 3 which has evolved over time through the comments, hopefully now all written down in one place!
Rendered