mirror of
https://github.com/fluencelabs/wasm-bindgen
synced 2025-06-18 15:31:25 +00:00
Add support for #[wasm_bindgen]
on async fn
(#1754)
This commit adds support to attach `#[wasm_bindgen]` on an `async fn` which will change the return value into a `Promise` in JS. This in theory has the exact same semantics as an `async` function in JS where you call it with all the arguments, nothing happens and you get a promise back, and then later the promise actually resolves. This commit also adds a helper trait, `IntoJsResult`, to allow `async` functions with multiple kinds of return values instead of requiring everything to be `Result<JsValue, JsValue>`.
This commit is contained in:
@ -213,6 +213,7 @@ pub struct Function {
|
|||||||
pub ret: Option<syn::Type>,
|
pub ret: Option<syn::Type>,
|
||||||
pub rust_attrs: Vec<syn::Attribute>,
|
pub rust_attrs: Vec<syn::Attribute>,
|
||||||
pub rust_vis: syn::Visibility,
|
pub rust_vis: syn::Visibility,
|
||||||
|
pub r#async: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]
|
#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]
|
||||||
|
@ -442,13 +442,30 @@ impl TryToTokens for ast::Export {
|
|||||||
if let syn::Type::Reference(_) = syn_ret {
|
if let syn::Type::Reference(_) = syn_ret {
|
||||||
bail_span!(syn_ret, "cannot return a borrowed ref with #[wasm_bindgen]",)
|
bail_span!(syn_ret, "cannot return a borrowed ref with #[wasm_bindgen]",)
|
||||||
}
|
}
|
||||||
let ret_ty = quote! {
|
|
||||||
-> <#syn_ret as wasm_bindgen::convert::ReturnWasmAbi>::Abi
|
// For an `async` function we always run it through `future_to_promise`
|
||||||
};
|
// since we're returning a promise to JS, and this will implicitly
|
||||||
let convert_ret = quote! {
|
// require that the function returns a `Future<Output = Result<...>>`
|
||||||
<#syn_ret as wasm_bindgen::convert::ReturnWasmAbi>
|
let (ret_expr, projection) = if self.function.r#async {
|
||||||
::return_abi(#ret)
|
(
|
||||||
|
quote! {
|
||||||
|
wasm_bindgen_futures::future_to_promise(async {
|
||||||
|
wasm_bindgen::__rt::IntoJsResult::into_js_result(#ret.await)
|
||||||
|
}).into()
|
||||||
|
},
|
||||||
|
quote! {
|
||||||
|
<wasm_bindgen::JsValue as wasm_bindgen::convert::ReturnWasmAbi>
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
quote! { #ret },
|
||||||
|
quote! {
|
||||||
|
<#syn_ret as wasm_bindgen::convert::ReturnWasmAbi>
|
||||||
|
},
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
let convert_ret = quote! { #projection::return_abi(#ret_expr) };
|
||||||
let describe_ret = quote! {
|
let describe_ret = quote! {
|
||||||
<#syn_ret as WasmDescribe>::describe();
|
<#syn_ret as WasmDescribe>::describe();
|
||||||
};
|
};
|
||||||
@ -457,7 +474,7 @@ impl TryToTokens for ast::Export {
|
|||||||
|
|
||||||
let start_check = if self.start {
|
let start_check = if self.start {
|
||||||
quote! {
|
quote! {
|
||||||
const _ASSERT: fn() = || #ret_ty { loop {} };
|
const _ASSERT: fn() = || -> #projection::Abi { loop {} };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
quote! {}
|
quote! {}
|
||||||
@ -465,11 +482,13 @@ impl TryToTokens for ast::Export {
|
|||||||
|
|
||||||
(quote! {
|
(quote! {
|
||||||
#(#attrs)*
|
#(#attrs)*
|
||||||
#[export_name = #export_name]
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))]
|
#[cfg_attr(
|
||||||
|
all(target_arch = "wasm32", not(target_os = "emscripten")),
|
||||||
|
export_name = #export_name,
|
||||||
|
)]
|
||||||
#[allow(clippy::all)]
|
#[allow(clippy::all)]
|
||||||
pub extern "C" fn #generated_name(#(#args),*) #ret_ty {
|
pub extern "C" fn #generated_name(#(#args),*) -> #projection::Abi {
|
||||||
#start_check
|
#start_check
|
||||||
// Scope all local variables to be destroyed after we call the
|
// Scope all local variables to be destroyed after we call the
|
||||||
// function to ensure that `#convert_ret`, if it panics, doesn't
|
// function to ensure that `#convert_ret`, if it panics, doesn't
|
||||||
|
@ -713,6 +713,7 @@ fn function_from_decl(
|
|||||||
ret,
|
ret,
|
||||||
rust_attrs: attrs,
|
rust_attrs: attrs,
|
||||||
rust_vis: vis,
|
rust_vis: vis,
|
||||||
|
r#async: sig.asyncness.is_some(),
|
||||||
},
|
},
|
||||||
method_self,
|
method_self,
|
||||||
))
|
))
|
||||||
|
@ -26,3 +26,4 @@ quote = "1.0"
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
trybuild = "1.0"
|
trybuild = "1.0"
|
||||||
wasm-bindgen = { path = "../..", version = "0.2.50", features = ['strict-macro'] }
|
wasm-bindgen = { path = "../..", version = "0.2.50", features = ['strict-macro'] }
|
||||||
|
wasm-bindgen-futures = { path = "../futures", version = "0.4.0" }
|
||||||
|
39
crates/macro/ui-tests/async-errors.rs
Normal file
39
crates/macro/ui-tests/async-errors.rs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct MyType;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn good1() { loop {} }
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn good2() -> JsValue { loop {} }
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn good3() -> u32 { loop {} }
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn good4() -> MyType { loop {} }
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn good5() -> Result<(), JsValue> { loop {} }
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn good6() -> Result<JsValue, JsValue> { loop {} }
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn good7() -> Result<u32, JsValue> { loop {} }
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn good8() -> Result<MyType, JsValue> { loop {} }
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn good9() -> Result<MyType, u32> { loop {} }
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn good10() -> Result<MyType, MyType> { loop {} }
|
||||||
|
|
||||||
|
pub struct BadType;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn bad1() -> Result<(), ()> { loop {} }
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn bad2() -> Result<(), BadType> { loop {} }
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn bad3() -> BadType { loop {} }
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn bad4() -> Result<BadType, JsValue> { loop {} }
|
||||||
|
|
||||||
|
|
||||||
|
fn main() {}
|
50
crates/macro/ui-tests/async-errors.stderr
Normal file
50
crates/macro/ui-tests/async-errors.stderr
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
error[E0277]: the trait bound `std::result::Result<(), ()>: wasm_bindgen::__rt::IntoJsResult` is not satisfied
|
||||||
|
--> $DIR/async-errors.rs:29:1
|
||||||
|
|
|
||||||
|
29 | #[wasm_bindgen]
|
||||||
|
| ^^^^^^^^^^^^^^^ the trait `wasm_bindgen::__rt::IntoJsResult` is not implemented for `std::result::Result<(), ()>`
|
||||||
|
|
|
||||||
|
= help: the following implementations were found:
|
||||||
|
<std::result::Result<(), E> as wasm_bindgen::__rt::IntoJsResult>
|
||||||
|
<std::result::Result<T, E> as wasm_bindgen::__rt::IntoJsResult>
|
||||||
|
= note: required by `wasm_bindgen::__rt::IntoJsResult::into_js_result`
|
||||||
|
|
||||||
|
error[E0277]: the trait bound `std::result::Result<(), BadType>: wasm_bindgen::__rt::IntoJsResult` is not satisfied
|
||||||
|
--> $DIR/async-errors.rs:31:1
|
||||||
|
|
|
||||||
|
31 | #[wasm_bindgen]
|
||||||
|
| ^^^^^^^^^^^^^^^ the trait `wasm_bindgen::__rt::IntoJsResult` is not implemented for `std::result::Result<(), BadType>`
|
||||||
|
|
|
||||||
|
= help: the following implementations were found:
|
||||||
|
<std::result::Result<(), E> as wasm_bindgen::__rt::IntoJsResult>
|
||||||
|
<std::result::Result<T, E> as wasm_bindgen::__rt::IntoJsResult>
|
||||||
|
= note: required by `wasm_bindgen::__rt::IntoJsResult::into_js_result`
|
||||||
|
|
||||||
|
error[E0277]: the trait bound `wasm_bindgen::JsValue: std::convert::From<BadType>` is not satisfied
|
||||||
|
--> $DIR/async-errors.rs:33:1
|
||||||
|
|
|
||||||
|
33 | #[wasm_bindgen]
|
||||||
|
| ^^^^^^^^^^^^^^^ the trait `std::convert::From<BadType>` is not implemented for `wasm_bindgen::JsValue`
|
||||||
|
|
|
||||||
|
= help: the following implementations were found:
|
||||||
|
<wasm_bindgen::JsValue as std::convert::From<&'a T>>
|
||||||
|
<wasm_bindgen::JsValue as std::convert::From<&'a std::string::String>>
|
||||||
|
<wasm_bindgen::JsValue as std::convert::From<&'a str>>
|
||||||
|
<wasm_bindgen::JsValue as std::convert::From<MyType>>
|
||||||
|
and 62 others
|
||||||
|
= note: required because of the requirements on the impl of `std::convert::Into<wasm_bindgen::JsValue>` for `BadType`
|
||||||
|
= note: required because of the requirements on the impl of `wasm_bindgen::__rt::IntoJsResult` for `BadType`
|
||||||
|
= note: required by `wasm_bindgen::__rt::IntoJsResult::into_js_result`
|
||||||
|
|
||||||
|
error[E0277]: the trait bound `std::result::Result<BadType, wasm_bindgen::JsValue>: wasm_bindgen::__rt::IntoJsResult` is not satisfied
|
||||||
|
--> $DIR/async-errors.rs:35:1
|
||||||
|
|
|
||||||
|
35 | #[wasm_bindgen]
|
||||||
|
| ^^^^^^^^^^^^^^^ the trait `wasm_bindgen::__rt::IntoJsResult` is not implemented for `std::result::Result<BadType, wasm_bindgen::JsValue>`
|
||||||
|
|
|
||||||
|
= help: the following implementations were found:
|
||||||
|
<std::result::Result<(), E> as wasm_bindgen::__rt::IntoJsResult>
|
||||||
|
<std::result::Result<T, E> as wasm_bindgen::__rt::IntoJsResult>
|
||||||
|
= note: required by `wasm_bindgen::__rt::IntoJsResult::into_js_result`
|
||||||
|
|
||||||
|
For more information about this error, try `rustc --explain E0277`.
|
@ -310,6 +310,7 @@ impl<'src> FirstPassRecord<'src> {
|
|||||||
ret: ret.clone(),
|
ret: ret.clone(),
|
||||||
rust_attrs: vec![],
|
rust_attrs: vec![],
|
||||||
rust_vis: public(),
|
rust_vis: public(),
|
||||||
|
r#async: false,
|
||||||
},
|
},
|
||||||
rust_name: rust_ident(rust_name),
|
rust_name: rust_ident(rust_name),
|
||||||
js_ret: js_ret.clone(),
|
js_ret: js_ret.clone(),
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
use js_sys::Promise;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
use wasm_bindgen_futures::future_to_promise;
|
|
||||||
use wasm_bindgen_futures::JsFuture;
|
use wasm_bindgen_futures::JsFuture;
|
||||||
use web_sys::{Request, RequestInit, RequestMode, Response};
|
use web_sys::{Request, RequestInit, RequestMode, Response};
|
||||||
|
|
||||||
@ -35,11 +33,7 @@ pub struct Signature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn run() -> Promise {
|
pub async fn run() -> Result<JsValue, JsValue> {
|
||||||
future_to_promise(run_())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_() -> Result<JsValue, JsValue> {
|
|
||||||
let mut opts = RequestInit::new();
|
let mut opts = RequestInit::new();
|
||||||
opts.method("GET");
|
opts.method("GET");
|
||||||
opts.mode(RequestMode::Cors);
|
opts.mode(RequestMode::Cors);
|
||||||
|
@ -1,4 +1,76 @@
|
|||||||
# Converting Between JavaScript `Promise`s and Rust `Future`s
|
# Working with a JS `Promise` and a Rust `Future`
|
||||||
|
|
||||||
|
Many APIs on the web work with a `Promise`, such as an `async` function in JS.
|
||||||
|
Naturally you'll probably want to interoperate with them from Rust! To do that
|
||||||
|
you can use the `wasm-bindgen-futures` crate as well as Rust `async`
|
||||||
|
functions.
|
||||||
|
|
||||||
|
The first thing you might encounter is the need for working with a `Promise`.
|
||||||
|
For this you'll want to use [`js_sys::Promise`]. Once you've got one of those
|
||||||
|
values you can convert that value to `wasm_bindgen_futures::JsFuture`. This type
|
||||||
|
implements the `std::future::Future` trait which allows naturally using it in an
|
||||||
|
`async` function. For example:
|
||||||
|
|
||||||
|
[`js_sys::Promise`]: https://docs.rs/js-sys/*/js_sys/struct.Promise.html
|
||||||
|
|
||||||
|
```rust
|
||||||
|
async fn get_from_js() -> Result<JsValue, JsValue> {
|
||||||
|
let promise = js_sys::Promise::resolved(&42.into());
|
||||||
|
let result = wasm_bindgen_futures::JsFuture::from(promise).await?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we can see how converting a `Promise` to Rust creates a `impl Future<Output
|
||||||
|
= Result<JsValue, JsValue>>`. This corresponds to `then` and `catch` in JS where
|
||||||
|
a successful promise becomes `Ok` and an erroneous promise becomes `Err`.
|
||||||
|
|
||||||
|
Next up you'll probably want to export a Rust function to JS that returns a
|
||||||
|
promise. To do this you can use an `async` function and `#[wasm_bindgen]`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub async fn foo() {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When invoked from JS the `foo` function here will return a `Promise`, so you can
|
||||||
|
import this as:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { foo } from "my-module";
|
||||||
|
|
||||||
|
async function shim() {
|
||||||
|
const result = await foo();
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Return values of `async fn`
|
||||||
|
|
||||||
|
When using an `async fn` in Rust and exporting it to JS there's some
|
||||||
|
restrictions on the return type. The return value of an exported Rust function
|
||||||
|
will eventually become `Result<JsValue, JsValue>` where `Ok` turns into a
|
||||||
|
successfully resolved promise and `Err` is equivalent to throwing an exception.
|
||||||
|
|
||||||
|
The following types are supported as return types from an `async fn`:
|
||||||
|
|
||||||
|
* `()` - turns into a successful `undefined` in JS
|
||||||
|
* `T: Into<JsValue>` - turns into a successful JS value
|
||||||
|
* `Result<(), E: Into<JsValue>>` - if `Ok(())` turns into a successful
|
||||||
|
`undefined` and otherwise turns into a failed promise with `E` converted to a
|
||||||
|
JS value
|
||||||
|
* `Result<T: Into<JsValue>, E: Into<JsValue>>` - like the previous case except
|
||||||
|
both data payloads are converted into a `JsValue`.
|
||||||
|
|
||||||
|
Note that many types implement being converted into a `JsValue`, such as all
|
||||||
|
imported types via `#[wasm_bindgen]` (aka those in `js-sys` or `web-sys`),
|
||||||
|
primitives like `u32`, and all exported `#[wasm_bindgen]` types. In general,
|
||||||
|
you should be able to write code without having too many explicit conversions,
|
||||||
|
and the macro should take care of the rest!
|
||||||
|
|
||||||
|
## Using `wasm-bindgen-futures`
|
||||||
|
|
||||||
The `wasm-bindgen-futures` crate bridges the gap between JavaScript `Promise`s
|
The `wasm-bindgen-futures` crate bridges the gap between JavaScript `Promise`s
|
||||||
and Rust `Future`s. Its `JsFuture` type provides conversion from a JavaScript
|
and Rust `Future`s. Its `JsFuture` type provides conversion from a JavaScript
|
||||||
|
40
src/lib.rs
40
src/lib.rs
@ -822,6 +822,8 @@ pub fn function_table() -> JsValue {
|
|||||||
pub mod __rt {
|
pub mod __rt {
|
||||||
use core::cell::{Cell, UnsafeCell};
|
use core::cell::{Cell, UnsafeCell};
|
||||||
use core::ops::{Deref, DerefMut};
|
use core::ops::{Deref, DerefMut};
|
||||||
|
use crate::JsValue;
|
||||||
|
|
||||||
pub extern crate core;
|
pub extern crate core;
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
pub extern crate std;
|
pub extern crate std;
|
||||||
@ -1095,6 +1097,44 @@ pub mod __rt {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An internal helper trait for usage in `#[wasm_bindgen]` on `async`
|
||||||
|
/// functions to convert the return value of the function to
|
||||||
|
/// `Result<JsValue, JsValue>` which is what we'll return to JS (where an
|
||||||
|
/// error is a failed future).
|
||||||
|
pub trait IntoJsResult {
|
||||||
|
fn into_js_result(self) -> Result<JsValue, JsValue>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoJsResult for () {
|
||||||
|
fn into_js_result(self) -> Result<JsValue, JsValue> {
|
||||||
|
Ok(JsValue::undefined())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Into<JsValue>> IntoJsResult for T {
|
||||||
|
fn into_js_result(self) -> Result<JsValue, JsValue> {
|
||||||
|
Ok(self.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Into<JsValue>, E: Into<JsValue>> IntoJsResult for Result<T, E> {
|
||||||
|
fn into_js_result(self) -> Result<JsValue, JsValue> {
|
||||||
|
match self {
|
||||||
|
Ok(e) => Ok(e.into()),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Into<JsValue>> IntoJsResult for Result<(), E> {
|
||||||
|
fn into_js_result(self) -> Result<JsValue, JsValue> {
|
||||||
|
match self {
|
||||||
|
Ok(()) => Ok(JsValue::undefined()),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A wrapper type around slices and vectors for binding the `Uint8ClampedArray`
|
/// A wrapper type around slices and vectors for binding the `Uint8ClampedArray`
|
||||||
|
Reference in New Issue
Block a user