-
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
Proposal to support single inheritance and casting in wasm-bindgen
#2
Conversation
My very un-expert two cents... I like requiring explicit casting 'cause explicit is good. Using Don't re-export the traits in prelude, one can always import them oneself as necessary. We can always change that later.
|
A few first thoughts:
|
Thanks for the RFC @fitzgen! I think may be important here to define in this RFC what role we think these traits and/or mechanisms will have on ecosystems API. On one end of the spectrum we could say that we want the solution here to be as ergonomic as possible, setting the precedent for how the entire ecosystem uses JS objects and casts. On the other end we could say that we only want to support the ability to connect types between their parent and child classes, but don't care about ergonomics at all. I personally feel that we should probably lean towards the latter end of the spectrum, mainly empowering users here rather than defining ecosystem-wide conventions about how they're expected to be used. The Along those lines I think I'd like to propose another alternative to this RFC. I feel that the The alternative I'm thinking of looks like this: pub unsafe trait AllJsValues {
fn from_js_value(js: JsValue) -> Self where Self: Sized;
fn into_js_value(self) -> JsValue where Self: Sized;
fn cast<U>(self) -> U
where U: AllJsValues + Sized, Self: Sized
{
U::from_js_value(self.into_js_value())
}
fn cast_ref<U>(&self) -> &U
where U: AllJsValues
{
unsafe { &*(self as *const Self as *const U) }
}
fn cast_mut<U>(&mut self) -> &mut U
where U: AllJsValues
{
unsafe { &mut *(self as *mut Self as *mut U) }
}
} An implementation of this trait could be generated by We could also be even more "extreme" and purely provide accessors for the In any case I'm personally a fan of sticking to the lower end of the spectrum of "not providing too much abstraction", but I'm curious what others think as well! |
This is a really good insight! My one fear is that we lose the type inference that allows chaining like derived.upcast().base_method(); and instead are forced to write temporaries and help out type inference: let base: MyBase = derived.upcast();
base.base_method(); I wonder if we can separate facts about inheritance relationships from the methods that do the casting so that we can maintain ergonomics. I think turbo-fishing is the best we could do syntax/ergonomics-wise with non-linear upcasting: derived.upcast::<MyBase>().base_method(); This would require trait definitions like pub unsafe trait Extends<T>: Sized + Into<T> {}
pub trait Upcast {
fn upcast<T>(self) -> T
where
Self: Extends<T>;
fn upcast_ref<T>(&self) -> &T
where
Self: Extends<T>;
fn upcast_mut<T>(&mut self) -> &mut T
where
Self: Extends<T>;
}
impl<U> Upcast for U {
#[inline]
fn upcast<T>(self) -> T
where
Self: Extends<T>,
{
self.into()
}
#[inline]
fn upcast_ref<T>(&self) -> &T
where
Self: Extends<T>,
{
unsafe { std::mem::transmute(self) }
}
#[inline]
fn upcast_mut<T>(&mut self) -> &mut T
where
Self: Extends<T>,
{
unsafe { std::mem::transmute(self) }
}
} So basically it comes down to whether non-linear upcasting is worth having to turbofish in method chaining like Thoughts? |
Agreed that our number one goal is empowering users to leverage APIs that make use of inheritance, that ergonomics is a nice-to-have, and that setting conventions for all crates built on top of However, I would like to disentangle type safety from ergonomics.
On the other hand, Web APIs/WebIDL are type safe, and we shouldn't lose that property when writing raw, thin bindings to them. I'm happy to include So looking at the problem once again from the perspective of
then I don't think |
Excellent points @fitzgen! I think it's worth digging into the type safety point here. I personally think we should be wary, though, of ascribing type safety to the synthetic types we're attaching to JS objects. We've got WebIDL for the web, but how sure are we that it's 100% exactly what all browsers accept? Are we sure that there aren't at least a few corner cases of some "duck typed" APIs or otherwise instances of accepting more than one type for compatibility's sake? Additionally, while WebIDL may be typesafe I'd imagine that the NPM ecosystem at large likely isn't in the sense that some types could be ascribed but at least in Typescript what I've seen is a lot of interfaces rather than classes (like a trait object instead of a concrete type) and there's not a great way to express that I think? I think what I'm trying to say is that it feels to me like an already-lost uphill battle to ascribe types to the JS ecosystem with 100% fidelity in the sense that we can use |
"extends" is intuitive for people with java or ES6 experience, and it also aligns pyo3 with wasm-bindgen (see rustwasm/rfcs#2)
This doesn't seem to match with
If we aren't going to trust our input (webidl / proc-macro attributes) and the types described therein, then I don't think we should be returning typed values from imported functions nor trusting whether an imported function may throw or not. In this world where we don't trust JS types, we should apply the "be conservative in what you send, be liberal in what you accept" principal to all interactions with JS:
FWIW, I don't think this would be a bad world to live in. But it is very different from what we are doing now.
The whole concept of |
Hm so I definitely don't mean to say that we should distrust all the webidl sources. Nor do I think it's a lost cause to attempt to add types here and there to the web. Mainly though what I'm trying to say is that we shouldn't be striving for perfection. That may be a bit of a nebulous concept in my head though, but to me I do think there's a lot of value in adding types somewhere in the ecosystem and I think putting it at the This feels similar to the standard library's I guess what I'm getting at is that a perhaps even more conservative implementation of this RFC (to the end of "expose all runtime capabilities") is something like: #[wasm_bindgen]
extern {
type Foo;
}
// becomes ...
struct Foo(JsValue);
impl AsRef<JsValue> for Foo { ... }
impl AsMut<JsValue> for Foo { ... }
impl From<JsValue> for Foo { ... }
impl AsRef<Foo> for JsValue { ... }
impl AsMut<Foo> for JsValue { ... }
impl From<Foo> for JsValue { ... } That way you should be able to convert in every direction as well as making a super-general API take something like |
That's basically what stdweb does. Here's how you define a new JS type with stdweb: #[derive(Clone, PartialEq, Eq, ReferenceType)]
#[reference(instance_of = "RegExp")]
pub struct RegExp(Reference); The above code gets expanded (by the derive macros) into something like this: pub struct RegExp(Reference);
impl AsRef<Reference> for RegExp { ... }
impl From<RegExp> for Reference { ... }
impl TryFrom<Reference> for RegExp { ... }
impl TryFrom<Value> for RegExp { ... }
impl JsSerialize for RegExp { ... }
impl InstanceOf for RegExp { ... }
This works out well in practice with stdweb, so I agree with your idea of making everything generic. Speaking of inheritance, stdweb solves that with two systems:
If you look at the methods for the traits, you'll see that they usually accept generic arguments ( In other words, they accept anything that implements those traits, so you rarely need to use |
I think we are in agreement here.
I think the I am not against unchecked dynamic casts, and I am not against marking them safe
However, I think that these particular implementations are a little too much of If a static upcast can be used, then it should be preferred over an unchecked If a static upcast cannot be used, then one should have a small road bump to Ultimately, I just want these implementations to be of different traits that are // Unchecked (but still safe) conversions.
impl UncheckedFrom<JsValue> for Foo { ... }
impl UncheckedAsRef<Foo> for JsValue { ... }
impl UncheckedAsMut<Foo> for JsValue { ... }
// Checked conversions using `instanceof`
impl TryFrom<JsValue> for Foo { ... }
impl<'a> TryFrom<&'a JsValue> for &'a Foo { ... }
impl<'a> TryFrom<&'a mut JsValue> for &'a mut Foo { ... }
I'm assuming these
I alluded to this in the original RFC text. I was hoping that we could do this I do think this general approach is the best way to get nicest ergonomics we can |
@fitzgen that sounds good to me yeah! It also looks suspiciously similar to the current RFC so I'm trying to reread my original response and the RFC to figure out the main differences... I think it basically just comes down to the details of how we expect these to be used and exactly how we're setting up the traits? For example these traits probably aren't suitable for "use everywhere as the base abstraction". An example of that you can't necessarily take Ok so to may be put my thoughts a bit more clearly, I think I'm worried that the RFC as-written isn't achieving the goal of "expose possibility, even if it's not the most ergonomic". Now I think it can with a few tweaks though! Perhaps something like: // in wasm_bindgen crate
pub unsafe trait InstanceOf {
fn is_instance_of(val: &JsValue) -> bool;
fn unchecked_from_js(val: JsValue) -> Self;
fn unchecked_from_js_ref(val: &JsValue) -> &Self;
fn unchecked_from_js_mut(val: &mut JsValue) -> &mut Self;
}
impl JsValue {
pub fn instanceof<T>(&self) -> bool {
T::is_instance_of(self)
}
pub fn try_into<T>(self) -> Result<T, Self>
where T: InstanceOf
{
if self.instanceof::<T>() { Ok(self.unchecked_into()) } else { Err(self) }
}
pub fn try_ref<T>(&self) -> Option<&T>
where T: InstanceOf
{
if self.instanceof::<T>() { Some(self.unchecked_ref()) } else { None }
}
pub fn try_mut<T>(&mut self) -> Option<&mut T>
where T: InstanceOf
{
if self.instanceof::<T>() { Some(self.unchecked_mut()) } else { None }
}
pub fn unchecked_into<T>(self) -> T where T: InstanceOf {
T::unchecked_from_js(self)
}
pub fn unchecked_ref<T>(&self) -> &T where T: InstanceOf {
T::unchecked_from_js_ref(self)
}
pub fn unchecked_mut<T>(&mut self) -> &mut T where T: InstanceOf {
T::unchecked_from_js_mut(self)
}
// pub fn from<T: Into<Self>>(t: T) -> JsValue { t.into() } // provided by the Rust prelude
pub fn from_ref<T: AsRef<Self>>(t: &T) -> &JsValue { t.as_ref() }
pub fn from_mut<T: AsMut<Self>>(t: &mut T) -> &mut JsValue { t.as_mut() }
}
// and then in your crate
#[wasm_bindgen]
type Foo;
#[wasm_bindgen(extends = Foo)]
type Bar;
// generates
impl InstanceOf for Foo {
fn is_instance_of(val: &JsValue) -> bool { /* shell out to JS shim */ }
fn unchecked_from_js(val: JsValue) -> Self { /* do the constructor */ }
fn unchecked_from_js_ref(val: &JsValue) -> &Self { /* do the weird transmute */ }
fn unchecked_from_js_mut(val: &mut JsValue) -> &mut Self { /* do the weird transmute */ }
}
impl From<Foo> for JsValue { ... }
impl AsRef<JsValue> for Foo { ... }
impl AsMut<JsValue> for Foo { ... }
impl InstanceOf for Bar {
fn is_instance_of(val: &JsValue) -> bool { /* shell out to JS shim */ }
fn unchecked_from_js(val: JsValue) -> Self { /* do the constructor */ }
fn unchecked_from_js_ref(val: &JsValue) -> &Self { /* do the weird transmute */ }
fn unchecked_from_js_mut(val: &mut JsValue) -> &mut Self { /* do the weird transmute */ }
}
impl From<Bar> for Foo { /* use unchecked casts in InstanceOf */ }
impl AsRef<Foo> for Bar { /* use unchecked casts in InstanceOf */ }
impl AsMut<Foo> for Bar { /* use unchecked casts in InstanceOf */ }
impl From<Bar> for JsValue { ... }
impl AsRef<JsValue> for Bar { ... }
impl AsMut<JsValue> for Bar { ... } ... Hm so actually that's very similar to my earlier comment, but in writing it up I've realized a few things:
I'm curious if you agree w/ these constraints? I think it fits what we've been saying here pretty well so far, but want to make sure I didn't miss something! Now on a totally unrelated note from functionality when we get to API-design business I'm personally wary to lean too heavily on traits. I find they can often serve as a barrier to understanding because you're always feeling like you should be using a trait somewhere or are otherwise too aggressively implementing traits on things that aren't used all that often (or bemoaning a missing blanket impl or lack of specialization or something like that). The ergonomics also aren't too great as you have to import them, but |
Yes! This is the crux of it. |
The one question still unanswered is whether we will generate #[wasm_bindgen]
extern {
type Base;
#[wasm_bindgen(extends = Base)]
type Derived;
#[wasm_bindgen(extends = Derived)]
type DoubleDerived;
} I think for the proc-macro we can only do one level due to technical reasons (for example if the definitions are split across For WebIDL we could possibly do the whole thing. |
Agreed!
Also agreed! To bridge the gap we could perhaps allow multiple |
SGTM! Will update the RFC text tomorrow. |
* Use `From` and `As{Ref,Mut}` instead of custom upcasting trait. Allows upcasting multiple steps at a time and is more familiar. * Introduce arbitrary unchecked casting escape hatch. * Some refactoring of dynamic casts (and unchecked casts) into the `JsCast` trait.
439fec7
to
6c9b1cf
Compare
RFC text updated! @alexcrichton @icefoxen @ohanar what do y'all think? |
@Pauan interested in your feedback on the updated RFC as well! Sorry I forgot to tag you in that last comment :-p |
Looks great to me, thanks for updating @fitzgen! Do we need a split between the two traits still? I think it'd be possible to have only one trait for these operations, right? |
This would require implementing Thoughts? |
Oh I think that's fine, it just means that the "instanceof" check there always returns |
Overall, I'm pretty happy with the proposal, there are just a couple small comments I have:
|
Yes, the On the other hand, the And it's always possible to use
Sure, I did see that. But since stdweb has already tackled this problem, I wanted to explain the solution they came up with, since it acts as evidence that the general idea presented in this RFC is both sound and practical.
Sure! I'll reply individually to parts of the RFC:
We should consider marking the
Why the separation between
I'm fine with this. In almost every case there will be
I view manual casting as a very advanced thing which should rarely be needed: users should use |
One use case I have is to implement Custom Elements in Rust. In this situation, the Rust struct needs to inherit from (for instance) HTMLElement and implement the custom elements lifecycle callbacks (see details at https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#Using_the_lifecycle_callbacks). It's unclear to me how this RFC would allow that since it seems focused on exposing the JS inheritance chain to Rust when importing JS but not when exporting. |
@fabricedesre I think that's covered by rustwasm/wasm-bindgen#210 which isn't addressed by this RFC, although the components in this RFC should be hopefully adding support for it in the future! |
And rename `try_{into,ref,mut}` to `dyn_{into,ref,mut}` to be future compatible with `TryFrom`.
New commit merged We implement Renamed |
I'd like to now propose that we enter the Final Comment Period for this RFC. Aside: we might want to eventually make a bindings/lib/something team that handles RFCs that are related to wasm-bindgen's output and design, but since such a team does not exist at the moment, I will ask @rustwasm/core to sign off. Disposition: merge @rustwasm/core members to sign off: |
All @rustwasm/core team members have signed off. Entering the 7 day ✨ Final Comment Period ✨! |
A sample implementation of the current state of this RFC is at rustwasm/wasm-bindgen#640 |
FCP complete! Thanks everyone for the discussion :) I think we ended up in a better place than where we started! |
Rendered