Skip to content

Commit 8861811

Browse files
authored
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>`.
1 parent 7fd6702 commit 8861811

File tree

10 files changed

+236
-18
lines changed

10 files changed

+236
-18
lines changed

crates/backend/src/ast.rs

+1
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ pub struct Function {
213213
pub ret: Option<syn::Type>,
214214
pub rust_attrs: Vec<syn::Attribute>,
215215
pub rust_vis: syn::Visibility,
216+
pub r#async: bool,
216217
}
217218

218219
#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]

crates/backend/src/codegen.rs

+29-10
Original file line numberDiff line numberDiff line change
@@ -442,13 +442,30 @@ impl TryToTokens for ast::Export {
442442
if let syn::Type::Reference(_) = syn_ret {
443443
bail_span!(syn_ret, "cannot return a borrowed ref with #[wasm_bindgen]",)
444444
}
445-
let ret_ty = quote! {
446-
-> <#syn_ret as wasm_bindgen::convert::ReturnWasmAbi>::Abi
447-
};
448-
let convert_ret = quote! {
449-
<#syn_ret as wasm_bindgen::convert::ReturnWasmAbi>
450-
::return_abi(#ret)
445+
446+
// For an `async` function we always run it through `future_to_promise`
447+
// since we're returning a promise to JS, and this will implicitly
448+
// require that the function returns a `Future<Output = Result<...>>`
449+
let (ret_expr, projection) = if self.function.r#async {
450+
(
451+
quote! {
452+
wasm_bindgen_futures::future_to_promise(async {
453+
wasm_bindgen::__rt::IntoJsResult::into_js_result(#ret.await)
454+
}).into()
455+
},
456+
quote! {
457+
<wasm_bindgen::JsValue as wasm_bindgen::convert::ReturnWasmAbi>
458+
},
459+
)
460+
} else {
461+
(
462+
quote! { #ret },
463+
quote! {
464+
<#syn_ret as wasm_bindgen::convert::ReturnWasmAbi>
465+
},
466+
)
451467
};
468+
let convert_ret = quote! { #projection::return_abi(#ret_expr) };
452469
let describe_ret = quote! {
453470
<#syn_ret as WasmDescribe>::describe();
454471
};
@@ -457,19 +474,21 @@ impl TryToTokens for ast::Export {
457474

458475
let start_check = if self.start {
459476
quote! {
460-
const _ASSERT: fn() = || #ret_ty { loop {} };
477+
const _ASSERT: fn() = || -> #projection::Abi { loop {} };
461478
}
462479
} else {
463480
quote! {}
464481
};
465482

466483
(quote! {
467484
#(#attrs)*
468-
#[export_name = #export_name]
469485
#[allow(non_snake_case)]
470-
#[cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))]
486+
#[cfg_attr(
487+
all(target_arch = "wasm32", not(target_os = "emscripten")),
488+
export_name = #export_name,
489+
)]
471490
#[allow(clippy::all)]
472-
pub extern "C" fn #generated_name(#(#args),*) #ret_ty {
491+
pub extern "C" fn #generated_name(#(#args),*) -> #projection::Abi {
473492
#start_check
474493
// Scope all local variables to be destroyed after we call the
475494
// function to ensure that `#convert_ret`, if it panics, doesn't

crates/macro-support/src/parser.rs

+1
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,7 @@ fn function_from_decl(
713713
ret,
714714
rust_attrs: attrs,
715715
rust_vis: vis,
716+
r#async: sig.asyncness.is_some(),
716717
},
717718
method_self,
718719
))

crates/macro/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ quote = "1.0"
2626
[dev-dependencies]
2727
trybuild = "1.0"
2828
wasm-bindgen = { path = "../..", version = "0.2.50", features = ['strict-macro'] }
29+
wasm-bindgen-futures = { path = "../futures", version = "0.4.0" }

crates/macro/ui-tests/async-errors.rs

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use wasm_bindgen::prelude::*;
2+
3+
#[wasm_bindgen]
4+
pub struct MyType;
5+
6+
#[wasm_bindgen]
7+
pub async fn good1() { loop {} }
8+
#[wasm_bindgen]
9+
pub async fn good2() -> JsValue { loop {} }
10+
#[wasm_bindgen]
11+
pub async fn good3() -> u32 { loop {} }
12+
#[wasm_bindgen]
13+
pub async fn good4() -> MyType { loop {} }
14+
#[wasm_bindgen]
15+
pub async fn good5() -> Result<(), JsValue> { loop {} }
16+
#[wasm_bindgen]
17+
pub async fn good6() -> Result<JsValue, JsValue> { loop {} }
18+
#[wasm_bindgen]
19+
pub async fn good7() -> Result<u32, JsValue> { loop {} }
20+
#[wasm_bindgen]
21+
pub async fn good8() -> Result<MyType, JsValue> { loop {} }
22+
#[wasm_bindgen]
23+
pub async fn good9() -> Result<MyType, u32> { loop {} }
24+
#[wasm_bindgen]
25+
pub async fn good10() -> Result<MyType, MyType> { loop {} }
26+
27+
pub struct BadType;
28+
29+
#[wasm_bindgen]
30+
pub async fn bad1() -> Result<(), ()> { loop {} }
31+
#[wasm_bindgen]
32+
pub async fn bad2() -> Result<(), BadType> { loop {} }
33+
#[wasm_bindgen]
34+
pub async fn bad3() -> BadType { loop {} }
35+
#[wasm_bindgen]
36+
pub async fn bad4() -> Result<BadType, JsValue> { loop {} }
37+
38+
39+
fn main() {}
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
error[E0277]: the trait bound `std::result::Result<(), ()>: wasm_bindgen::__rt::IntoJsResult` is not satisfied
2+
--> $DIR/async-errors.rs:29:1
3+
|
4+
29 | #[wasm_bindgen]
5+
| ^^^^^^^^^^^^^^^ the trait `wasm_bindgen::__rt::IntoJsResult` is not implemented for `std::result::Result<(), ()>`
6+
|
7+
= help: the following implementations were found:
8+
<std::result::Result<(), E> as wasm_bindgen::__rt::IntoJsResult>
9+
<std::result::Result<T, E> as wasm_bindgen::__rt::IntoJsResult>
10+
= note: required by `wasm_bindgen::__rt::IntoJsResult::into_js_result`
11+
12+
error[E0277]: the trait bound `std::result::Result<(), BadType>: wasm_bindgen::__rt::IntoJsResult` is not satisfied
13+
--> $DIR/async-errors.rs:31:1
14+
|
15+
31 | #[wasm_bindgen]
16+
| ^^^^^^^^^^^^^^^ the trait `wasm_bindgen::__rt::IntoJsResult` is not implemented for `std::result::Result<(), BadType>`
17+
|
18+
= help: the following implementations were found:
19+
<std::result::Result<(), E> as wasm_bindgen::__rt::IntoJsResult>
20+
<std::result::Result<T, E> as wasm_bindgen::__rt::IntoJsResult>
21+
= note: required by `wasm_bindgen::__rt::IntoJsResult::into_js_result`
22+
23+
error[E0277]: the trait bound `wasm_bindgen::JsValue: std::convert::From<BadType>` is not satisfied
24+
--> $DIR/async-errors.rs:33:1
25+
|
26+
33 | #[wasm_bindgen]
27+
| ^^^^^^^^^^^^^^^ the trait `std::convert::From<BadType>` is not implemented for `wasm_bindgen::JsValue`
28+
|
29+
= help: the following implementations were found:
30+
<wasm_bindgen::JsValue as std::convert::From<&'a T>>
31+
<wasm_bindgen::JsValue as std::convert::From<&'a std::string::String>>
32+
<wasm_bindgen::JsValue as std::convert::From<&'a str>>
33+
<wasm_bindgen::JsValue as std::convert::From<MyType>>
34+
and 62 others
35+
= note: required because of the requirements on the impl of `std::convert::Into<wasm_bindgen::JsValue>` for `BadType`
36+
= note: required because of the requirements on the impl of `wasm_bindgen::__rt::IntoJsResult` for `BadType`
37+
= note: required by `wasm_bindgen::__rt::IntoJsResult::into_js_result`
38+
39+
error[E0277]: the trait bound `std::result::Result<BadType, wasm_bindgen::JsValue>: wasm_bindgen::__rt::IntoJsResult` is not satisfied
40+
--> $DIR/async-errors.rs:35:1
41+
|
42+
35 | #[wasm_bindgen]
43+
| ^^^^^^^^^^^^^^^ the trait `wasm_bindgen::__rt::IntoJsResult` is not implemented for `std::result::Result<BadType, wasm_bindgen::JsValue>`
44+
|
45+
= help: the following implementations were found:
46+
<std::result::Result<(), E> as wasm_bindgen::__rt::IntoJsResult>
47+
<std::result::Result<T, E> as wasm_bindgen::__rt::IntoJsResult>
48+
= note: required by `wasm_bindgen::__rt::IntoJsResult::into_js_result`
49+
50+
For more information about this error, try `rustc --explain E0277`.

crates/webidl/src/util.rs

+1
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ impl<'src> FirstPassRecord<'src> {
310310
ret: ret.clone(),
311311
rust_attrs: vec![],
312312
rust_vis: public(),
313+
r#async: false,
313314
},
314315
rust_name: rust_ident(rust_name),
315316
js_ret: js_ret.clone(),

examples/fetch/src/lib.rs

+1-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
use js_sys::Promise;
21
use serde::{Deserialize, Serialize};
32
use wasm_bindgen::prelude::*;
43
use wasm_bindgen::JsCast;
5-
use wasm_bindgen_futures::future_to_promise;
64
use wasm_bindgen_futures::JsFuture;
75
use web_sys::{Request, RequestInit, RequestMode, Response};
86

@@ -35,11 +33,7 @@ pub struct Signature {
3533
}
3634

3735
#[wasm_bindgen]
38-
pub fn run() -> Promise {
39-
future_to_promise(run_())
40-
}
41-
42-
async fn run_() -> Result<JsValue, JsValue> {
36+
pub async fn run() -> Result<JsValue, JsValue> {
4337
let mut opts = RequestInit::new();
4438
opts.method("GET");
4539
opts.mode(RequestMode::Cors);

guide/src/reference/js-promises-and-rust-futures.md

+73-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,76 @@
1-
# Converting Between JavaScript `Promise`s and Rust `Future`s
1+
# Working with a JS `Promise` and a Rust `Future`
2+
3+
Many APIs on the web work with a `Promise`, such as an `async` function in JS.
4+
Naturally you'll probably want to interoperate with them from Rust! To do that
5+
you can use the `wasm-bindgen-futures` crate as well as Rust `async`
6+
functions.
7+
8+
The first thing you might encounter is the need for working with a `Promise`.
9+
For this you'll want to use [`js_sys::Promise`]. Once you've got one of those
10+
values you can convert that value to `wasm_bindgen_futures::JsFuture`. This type
11+
implements the `std::future::Future` trait which allows naturally using it in an
12+
`async` function. For example:
13+
14+
[`js_sys::Promise`]: https://docs.rs/js-sys/*/js_sys/struct.Promise.html
15+
16+
```rust
17+
async fn get_from_js() -> Result<JsValue, JsValue> {
18+
let promise = js_sys::Promise::resolved(&42.into());
19+
let result = wasm_bindgen_futures::JsFuture::from(promise).await?;
20+
Ok(result)
21+
}
22+
```
23+
24+
Here we can see how converting a `Promise` to Rust creates a `impl Future<Output
25+
= Result<JsValue, JsValue>>`. This corresponds to `then` and `catch` in JS where
26+
a successful promise becomes `Ok` and an erroneous promise becomes `Err`.
27+
28+
Next up you'll probably want to export a Rust function to JS that returns a
29+
promise. To do this you can use an `async` function and `#[wasm_bindgen]`:
30+
31+
```rust
32+
#[wasm_bindgen]
33+
pub async fn foo() {
34+
// ...
35+
}
36+
```
37+
38+
When invoked from JS the `foo` function here will return a `Promise`, so you can
39+
import this as:
40+
41+
```js
42+
import { foo } from "my-module";
43+
44+
async function shim() {
45+
const result = await foo();
46+
// ...
47+
}
48+
```
49+
50+
## Return values of `async fn`
51+
52+
When using an `async fn` in Rust and exporting it to JS there's some
53+
restrictions on the return type. The return value of an exported Rust function
54+
will eventually become `Result<JsValue, JsValue>` where `Ok` turns into a
55+
successfully resolved promise and `Err` is equivalent to throwing an exception.
56+
57+
The following types are supported as return types from an `async fn`:
58+
59+
* `()` - turns into a successful `undefined` in JS
60+
* `T: Into<JsValue>` - turns into a successful JS value
61+
* `Result<(), E: Into<JsValue>>` - if `Ok(())` turns into a successful
62+
`undefined` and otherwise turns into a failed promise with `E` converted to a
63+
JS value
64+
* `Result<T: Into<JsValue>, E: Into<JsValue>>` - like the previous case except
65+
both data payloads are converted into a `JsValue`.
66+
67+
Note that many types implement being converted into a `JsValue`, such as all
68+
imported types via `#[wasm_bindgen]` (aka those in `js-sys` or `web-sys`),
69+
primitives like `u32`, and all exported `#[wasm_bindgen]` types. In general,
70+
you should be able to write code without having too many explicit conversions,
71+
and the macro should take care of the rest!
72+
73+
## Using `wasm-bindgen-futures`
274

375
The `wasm-bindgen-futures` crate bridges the gap between JavaScript `Promise`s
476
and Rust `Future`s. Its `JsFuture` type provides conversion from a JavaScript

src/lib.rs

+40
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,8 @@ pub fn function_table() -> JsValue {
822822
pub mod __rt {
823823
use core::cell::{Cell, UnsafeCell};
824824
use core::ops::{Deref, DerefMut};
825+
use crate::JsValue;
826+
825827
pub extern crate core;
826828
#[cfg(feature = "std")]
827829
pub extern crate std;
@@ -1095,6 +1097,44 @@ pub mod __rt {
10951097
return ret;
10961098
}
10971099
}
1100+
1101+
/// An internal helper trait for usage in `#[wasm_bindgen]` on `async`
1102+
/// functions to convert the return value of the function to
1103+
/// `Result<JsValue, JsValue>` which is what we'll return to JS (where an
1104+
/// error is a failed future).
1105+
pub trait IntoJsResult {
1106+
fn into_js_result(self) -> Result<JsValue, JsValue>;
1107+
}
1108+
1109+
impl IntoJsResult for () {
1110+
fn into_js_result(self) -> Result<JsValue, JsValue> {
1111+
Ok(JsValue::undefined())
1112+
}
1113+
}
1114+
1115+
impl<T: Into<JsValue>> IntoJsResult for T {
1116+
fn into_js_result(self) -> Result<JsValue, JsValue> {
1117+
Ok(self.into())
1118+
}
1119+
}
1120+
1121+
impl<T: Into<JsValue>, E: Into<JsValue>> IntoJsResult for Result<T, E> {
1122+
fn into_js_result(self) -> Result<JsValue, JsValue> {
1123+
match self {
1124+
Ok(e) => Ok(e.into()),
1125+
Err(e) => Err(e.into()),
1126+
}
1127+
}
1128+
}
1129+
1130+
impl<E: Into<JsValue>> IntoJsResult for Result<(), E> {
1131+
fn into_js_result(self) -> Result<JsValue, JsValue> {
1132+
match self {
1133+
Ok(()) => Ok(JsValue::undefined()),
1134+
Err(e) => Err(e.into()),
1135+
}
1136+
}
1137+
}
10981138
}
10991139

11001140
/// A wrapper type around slices and vectors for binding the `Uint8ClampedArray`

0 commit comments

Comments
 (0)