-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Enable passing String and custom struct boxed slices across ABI and implement TryFrom<JsValue> for custom structs #2734
Conversation
This is accomplished via conversion of the Strings to/from JsValues.
This was done by converting the structs to/from JsValues. It was necessary to change the way relevant traits (e.g. WasmDescribe, IntoWasmAbi etc.) are implemented. It's impossible to implement these for `Box<[#name]>` in codegen.rs because implementing traits on generic types is only allowed in the crate in which the trait is defined. Naively adding a blanket implementation on the wasm-bindgen side doesn't work either because it conflicts with the implementation for JsObjects. The solution was to create traits like VectorIntoWasmAbi etc. that are defined on the concrete type and contain the implementation for IntoWasmAbi etc. for vectors of that type. JsObjects are blanket implemented as before, and struct types are implemented in codegen.rs. Due to the way these traits are defined, Rust requires implementing types to be Sized, so they can't be used for the existing String implementations. Converting structs from JsValues was accomplished by adding an unwrap function to the generated JavaScript class, and calling that from Rust.
Thanks for the PR, but judging from the size here, the description, and the title, this seems like a significant change. Can you describe the change in some more detail at a high level and explain what's in the patch internally? Additionally I think it'd probably be good to ensure CI is green. |
@alexcrichton No worries Alex, and thank you for taking a look. My motivation for writing this PR was frustration with wasm-bindgen's limited ability to pass vector types ( There are a few traits that are necessary to implement to allow for bidirectional conversion of a type between Rust and JS:
These traits are blanket implemented for The basis of this PR is the use of the existing implementations of these traits for The JS side can just treat this the same way as any other The downside of this approach is that it involves a lot of calls between Rust and JS, which might be a performance bottleneck in some situations. At least it's no worse than the workarounds people are doing anyway. It can be optimised in future if anyone finds a more performant way of doing it. A naive approach would be to implement these traits with a blanket implementation on Implementing the needed traits for exported structs was a little more challenging. Firstly, it's impossible to implement e.g. Secondly, there was no good way to recover an instance of an exported struct from a Could you or maybe @chinedufn advise me on https://github.com/rustwasm/wasm-bindgen/blob/main/crates/shared/src/schema_hash_approval.rs ? The comment reads: // Whenever the lib.rs changes, the SCHEMA_FILE_HASH environment variable will change and the
// schema_version test below will fail.
// Proceed as follows:
//
// If the schema in this library has changed then:
// 1. Change this APPROVED_SCHEMA_FILE_HASH to the new hash.
//
// If the schema in this library has changed then:
// 1. Bump the version in `crates/shared/Cargo.toml`
// 2. Change the `SCHEMA_VERSION` in this library to this new Cargo.toml version I think the top option might be supposed to say 'not changed'? Should version numbers be bumped in general in PRs for this project? I assumed this would be done on master when it's time for a new release. Please let me know if anything needs to be clarified or if there are any amendments you would like. |
Any follow-ups on this PR? Would be a useful feature to have when working with wasm-bindgen. |
@abi I haven't been able to get clarification on the failing test I mentioned in my previous comment. @chinedufn @alexcrichton could either of you advise me on that? If you're too busy at the moment please just leave a comment so I know you've seen this and are aware that I need advice to continue. |
Hi! any follow-ups on this? this is a very nice/important feature that is missing in in bindgen, it would do great for this project for this to be reviewed |
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 looks great! I have a couple comments, and it needs to be rebased, but after that this looks good to merge. Some tests would also be good.
I'm happy to handle rebasing and such if @sinking-point is no longer interested.
let js_vals: Box::<[JsValue]> = self | ||
.into_vec() | ||
.into_iter() | ||
.map(|x| x.into()) | ||
.collect(); |
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.
It feels kind of wrong to be throwing away the initial allocation and making a new one here, but it works for an MVP and can be optimised later if necessary.
I have some ideas how this can be avoided by doing more work in JS, but I have no idea if that'd actually end up being faster, so let's just go with this for now.
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.
Exactly my thinking, let's not let perfect be the enemy of good.
|
||
js_vals | ||
.into_iter() | ||
.filter_map(|x| x.try_into().ok()) |
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.
It feels like a bad idea to silently throw away array elements that fail to be converted; I'd prefer if this used unwrap_throw
to propagate the error instead.
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.
Agreed. No idea why I did this tbh. I thought I made it so it would panic.
/// Enables blanket implementations of `WasmDescribe`, `IntoWasmAbi`, | ||
/// `FromWasmAbi` and `OptionIntoWasmAbi` functionality on boxed slices of | ||
/// types which can be converted to and from `JsValue` without conflicting | ||
/// implementations of those traits. | ||
/// | ||
/// Implementing these traits directly with blanket implementations would | ||
/// be much more elegant, but unfortunately that's impossible because it | ||
/// conflicts with the implementations for `Box<[T]> where T: JsObject`. | ||
pub trait JsValueVector { | ||
type ToAbi; | ||
type FromAbi; | ||
|
||
fn describe(); | ||
fn into_abi(self) -> Self::ToAbi; | ||
fn none() -> Self::ToAbi; | ||
unsafe fn from_abi(js: Self::FromAbi) -> Self; | ||
} |
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 trait doesn't seem super necessary; you could replace it with a bunch of regular generic functions instead, like this:
pub fn js_value_vector_into_abi<T>(vector: Box<[T]>) -> <Box<[JsValue]> as IntoWasmAbi>::Abi
where
T: Into<JsValue>,
{
let js_vals: Box<[JsValue]> = vector.into_vec().into_iter().map(|x| x.into()).collect();
IntoWasmAbi::into_abi(js_vals)
}
pub unsafe fn js_value_vector_from_abi<T>(js: <Box<[JsValue]> as FromWasmAbi>::Abi) -> Box<[T]>
where
T: TryFrom<JsValue>,
<T as TryFrom<JsValue>>::Error: core::fmt::Debug,
{
let js_vals = <Vec<JsValue> as FromWasmAbi>::from_abi(js);
js_vals
.into_iter()
.map(|x| x.try_into().unwrap_throw())
.collect()
}
// `describe` and `none` are so simple I don't really see the point in putting
// functions for them here, but you can if you want.
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.
You may be right. I don't really have the context anymore to properly defend my decisions. Maybe I did it this way simply because I thought it was neater. I think it's nice to have it bundled up into a trait like this, so that if anyone wants to implement the same functionality for another type, they know exactly what they have to do. But if you would rather have generic functions instead, I don't mind.
|
||
#[allow(clippy::all)] | ||
impl wasm_bindgen::__rt::core::convert::TryFrom<wasm_bindgen::JsValue> for #name { | ||
type Error = (); |
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.
type Error = (); | |
type Error = JsValue; |
It's not great practice to use ()
as an error type. Normally I'd probably suggest creating a new error type for this, but all of wasm-bindgen
's existing TryFrom<JsValue>
impls use JsValue
for their error type and so I think it's a better idea to keep things consistent.
Thanks for the review @Liamolucko . I did this work last year because I wanted this feature for a project I was working on at the time. However, it's no longer a priority for me. I have other work to do and I don't forsee myself making time for this in the foreseeable future. Please feel free to pick up where I left off. I had accepted, painfully, that this work had gone to waste, so I'm very grateful that you're rescuing it. Thank you. |
Was replaced by #3554. |
Closes:
#111
#2452
#168
#2231
The vectors are converted across the ABI by converting their elements to/from
JsValue
s.It was necessary to change the way relevant traits (e.g.
WasmDescribe
,IntoWasmAbi
etc.) are implemented. It's impossible to implement these forBox<[#name]>
in codegen.rs because implementing traits on generic types is only allowed in the crate in which the trait is defined. Naively adding a blanket implementation on the wasm-bindgen side doesn't work either because it conflicts with the implementation forJsObject
s. The solution was to create traits likeVectorIntoWasmAbi
etc. that are implemented on the concrete type and contain the implementation forIntoWasmAbi
etc. for vectors of that type.JsObject
s are blanket implemented as before, and struct types are implemented in codegen.rs. Due to the way these traits are defined, Rust requires implementing types to beSized
, so they can't be used for theString
implementations.Converting structs from JsValues was accomplished by adding an unwrap function to the generated JavaScript class, and calling that from Rust.