Skip to content

Conversation

@alexcrichton
Copy link
Contributor

This commit adds support for generating bindings for dictionaries defined in
WebIDL. Dictionaries are associative arrays which are simply objects in JS with
named keys and some values. In Rust given a dictionary like:

dictionary Foo {
    long field;
};

we'll generate a struct like:

pub struct Foo {
    obj: js_sys::Object,
}

impl Foo {
    pub fn new() -> Foo { /* make a blank object */ }

    pub fn field(&mut self, val: i32) -> &mut Self {
        // set the field using `js_sys::Reflect`
    }
}

// plus a bunch of AsRef, From, and wasm abi impls

At the same time this adds support for partial dictionaries and dictionary
inheritance. All dictionary fields are optional by default and hence only have
builder-style setters, but dictionaries can also have required fields. Required
fields are exposed as arguments to the new constructor.

Closes #241

Copy link
Contributor

@ohanar ohanar left a comment

Choose a reason for hiding this comment

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

Mostly looks good. The main thing that I'm concerned about is exposing the JS Object that is being used under the hood, I'm not sure that is necessarily forward compatible with host binding support. I would remove the conversion impls there, and add getters so that dictionaries are usable as a return type (getters should take into account default values).

}
}
}
dst[start..].sort_by_key(|f| f.name.clone());
Copy link
Contributor

Choose a reason for hiding this comment

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

This is sorting by the snake case identifier, rather than the original identifier, which could produce the wrong result in edge cases:

dictionary Example {
    boolean fooBaz;
    boolean foo_bar;
};

That is correctly ordered per the spec, but this line will reorder it to [foo_bar, foo_baz].

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point! I'll add a comment because I think this is largely good enough for now, it's basically just generating the order of required fields for the constructor in any case.

&JsValue::from(val),
);
self
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Without corresponding getters, if a dictionary is returned, it is clumsy to use (have to upcast it to an Object, which I have concerns about).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True! The limitation to not providing getters right now is that there isn't a great way to compile them in and generate code which works. I think we'll definitely want to add getters though! (this is just an initial implementation)

}

// interop w/ Object
impl From<#name> for Object {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think there is any guarantee that we could do this with the host bindings proposal. In particular, I don't see any mention that WebIDL dictionaries have to be ECMAScript Objects. (This is really more of a concern for the AsRef and AsMut impls than the conversion impl, but I think we should implement all or none.)

let name = &self.name;
let ty = &self.ty;
(quote! {
pub fn #name(&mut self, val: #ty) -> &mut Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

You use pub here, even though the Dictionary might not have a public visibility.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True! Dictionaries are only generated by WebIDL though so they'll always be pub anyway. I think I'll remove the visibility field there and just hardcode everything to public

impl<'src> FirstPass<'src, ()> for weedle::PartialDictionaryDefinition<'src> {
fn first_pass(&'src self, record: &mut FirstPassRecord<'src>, (): ()) -> Result<()> {
record.dictionaries.entry(self.identifier.0)
.or_insert_with(Default::default)
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe or_default() here (and above)?

impl<'a> OptionIntoWasmAbi for &'a #name {
fn none() -> Self::Abi { <&'a Object>::none() }
}
impl<'a> OptionFromWasmAbi for #name {
Copy link
Contributor

Choose a reason for hiding this comment

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

Unused lifetime.

#name { obj: Object::from_abi(abi, extra) }
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Missing RefFromWasmAbi.

@eminence
Copy link
Contributor

eminence commented Aug 14, 2018

One places that uses a Dictionary is AudioContext.webidl:

dictionary AudioContextOptions {
             float        sampleRate = 0;
};

[Pref="dom.webaudio.enabled",
 Constructor(optional AudioContextOptions contextOptions)]
interface AudioContext : BaseAudioContext {
 //...
}

While the AudioContextOptions dictionary gets correctly generated, and a no-arg constructor is generated for the AudioContext struct, there is no AudioContext constructor that accepts an AudioContextOptions struct. Is this expected? (edit: expected given the state of this PR. That is, are the other known issues that would prevent the other constructor from being generated?)

}
}
for d in self.dictionaries.iter() {
d.to_tokens(tokens);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is ToTokens not implemented for &[T] or something? It seems like we shouldn't need to write this loop out by hand.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nah it's not currently implemented because I think it's ambiguous whether you want bracket delimiters or everything just concatenated together

.map(|f| &f.ty)
.collect::<Vec<_>>();
let required_names2 = required_names;
let required_names3 = required_names;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this still necessary even though required_names is a reference (and therefore is Copy)? I thought the trick of making a thing a reference usually fixed the ownership issues with quasi quoting...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately yeah, it has to do with the mystery of the quote! macro, although I'm not quite sure why. If the same variable name is used below it generates a bunch of scary compiler errors.

@alexcrichton
Copy link
Contributor Author

Thanks for taking a look @ohanar! It's a good point that I don't think we quite know how this is going to map to the host bindings proposal. We could definitely be more conservative and expose none of the internal details about Object.

I'm not sure what the best route here is. I don't think we know how this is gonna end up with host bindings (these functions may just not ever be bound with host bindings?). In any case though I think it's definitely the case that we're going to publish multiple major versions of web-sys. There's a lot of API design decisions we're only guessing about and we're definitely going to get some of them wrong. In that sense I'd probably be in favor of exposing the impls today with the assumption that we may very well reverse this decision in the future if a better binding strategy comes about.

@alexcrichton
Copy link
Contributor Author

@eminence turned out to be a bug, should be fixed now!

// Note that these map to `f64` because JS doesn't really have a
// native type for u64, so WebIDL surely just means a JS number
IdlType::LongLong => Some(ident_ty(raw_ident("f64"))),
IdlType::UnsignedLongLong => Some(ident_ty(raw_ident("f64"))),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a pretty subtle update in this PR, but lots of dictionaries apparently have long long specified in them. It's unclear what WebIDL could mean other than f64 for JS because 64-bit integers aren't possible, so these two now map to f64

Copy link
Contributor

@ohanar ohanar Aug 14, 2018

Choose a reason for hiding this comment

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

From the spec:

The long long type is a signed integer type that has values in the range [−9223372036854775808, 9223372036854775807].

In other words, WebIDL says a long long is a 64-bit integer. Presumably with host-bindings this won't be an issue, it is however an issue with current day shims. I don't think we should change all the signatures of methods just because of modern day limitations.

@ohanar
Copy link
Contributor

ohanar commented Aug 14, 2018

@alexcrichton My vote would be to be on the more conservative side. I don't see that much value added by having the ability to convert to an Object, so the cost/value ratio doesn't really add up for me.

@alexcrichton
Copy link
Contributor Author

@ohanar makes sense yeah, but the JS object under the hood is currently what's powering dictionaries-in-dictionaries, so I'm not sure we can avoid exposing this detail without removing that functionality? I don't want to be too conservative at the loss of functionality in the sense that we have tons of time to iterate on this crate.

The most recent commit should also avoid modifying the WebIDL and manually skips unimplemented dictionaries and i64/u64 in dictionaries.

@ohanar
Copy link
Contributor

ohanar commented Aug 15, 2018

Well, really conversion to a JSValue is needed to power dictionaries-in-dictionaries, not conversion to js_sys::Object (which was my main complaint).

@alexcrichton
Copy link
Contributor Author

@ohanar oh interesting, can you expand on why JsValue is ok but Object isn't? I figured that we'd want to remove both to be conservative but having a conversion to JsValue seems like it'd be natural to have a conversion to Object as well (in that adding Object isn't really that much more of a promise in the API, the conversion to JsValue is already the big one)

@eminence
Copy link
Contributor

With the latest fix, I confirm that the missing web audio constructors are now available, and that I was able to successfully get a sine-wave demo working! This obviously isn't a rigorous test of every dictionary, but I'm very happy that this particular one is working 😃 🎶

This commit adds support for generating bindings for dictionaries defined in
WebIDL. Dictionaries are associative arrays which are simply objects in JS with
named keys and some values. In Rust given a dictionary like:

    dictionary Foo {
        long field;
    };

we'll generate a struct like:

    pub struct Foo {
        obj: js_sys::Object,
    }

    impl Foo {
        pub fn new() -> Foo { /* make a blank object */ }

        pub fn field(&mut self, val: i32) -> &mut Self {
            // set the field using `js_sys::Reflect`
        }
    }

    // plus a bunch of AsRef, From, and wasm abi impls

At the same time this adds support for partial dictionaries and dictionary
inheritance. All dictionary fields are optional by default and hence only have
builder-style setters, but dictionaries can also have required fields. Required
fields are exposed as arguments to the `new` constructor.

Closes wasm-bindgen#241
@alexcrichton
Copy link
Contributor Author

I've gone ahead and removed the asref/asmut/from implementations for Object

@ohanar
Copy link
Contributor

ohanar commented Aug 16, 2018

So I guess my concern is that Objects are much more general maps than dictionaries (which are more akin to structs), and I would be a bit surprised if they were re-used for host bindings, just out of concern of efficiency. On the other hand, I would be less surprised if dictionaries were still opaque data structures, so they would presumably be a subtype of anyref, which will likely be the underlying type for a JsValue in the future.

This is obviously me making guesses, but I generally want to take as conservative of an approach as reasonably possible, just so we can minimize potential breakage in the future.

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.

4 participants