diff --git a/crates/backend/src/ast.rs b/crates/backend/src/ast.rs index c83894c7..a462a112 100644 --- a/crates/backend/src/ast.rs +++ b/crates/backend/src/ast.rs @@ -213,6 +213,7 @@ pub struct Function { pub ret: Option, pub rust_attrs: Vec, pub rust_vis: syn::Visibility, + pub r#async: bool, } #[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))] diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs index c930f5a7..3361c56b 100644 --- a/crates/backend/src/codegen.rs +++ b/crates/backend/src/codegen.rs @@ -442,13 +442,30 @@ impl TryToTokens for ast::Export { if let syn::Type::Reference(_) = syn_ret { bail_span!(syn_ret, "cannot return a borrowed ref with #[wasm_bindgen]",) } - let ret_ty = quote! { - -> <#syn_ret as wasm_bindgen::convert::ReturnWasmAbi>::Abi - }; - let convert_ret = quote! { - <#syn_ret as wasm_bindgen::convert::ReturnWasmAbi> - ::return_abi(#ret) + + // For an `async` function we always run it through `future_to_promise` + // since we're returning a promise to JS, and this will implicitly + // require that the function returns a `Future>` + let (ret_expr, projection) = if self.function.r#async { + ( + quote! { + wasm_bindgen_futures::future_to_promise(async { + wasm_bindgen::__rt::IntoJsResult::into_js_result(#ret.await) + }).into() + }, + quote! { + + }, + ) + } else { + ( + quote! { #ret }, + quote! { + <#syn_ret as wasm_bindgen::convert::ReturnWasmAbi> + }, + ) }; + let convert_ret = quote! { #projection::return_abi(#ret_expr) }; let describe_ret = quote! { <#syn_ret as WasmDescribe>::describe(); }; @@ -457,7 +474,7 @@ impl TryToTokens for ast::Export { let start_check = if self.start { quote! { - const _ASSERT: fn() = || #ret_ty { loop {} }; + const _ASSERT: fn() = || -> #projection::Abi { loop {} }; } } else { quote! {} @@ -465,11 +482,13 @@ impl TryToTokens for ast::Export { (quote! { #(#attrs)* - #[export_name = #export_name] #[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)] - pub extern "C" fn #generated_name(#(#args),*) #ret_ty { + pub extern "C" fn #generated_name(#(#args),*) -> #projection::Abi { #start_check // Scope all local variables to be destroyed after we call the // function to ensure that `#convert_ret`, if it panics, doesn't diff --git a/crates/macro-support/src/parser.rs b/crates/macro-support/src/parser.rs index 992cc17e..0271505b 100644 --- a/crates/macro-support/src/parser.rs +++ b/crates/macro-support/src/parser.rs @@ -713,6 +713,7 @@ fn function_from_decl( ret, rust_attrs: attrs, rust_vis: vis, + r#async: sig.asyncness.is_some(), }, method_self, )) diff --git a/crates/macro/Cargo.toml b/crates/macro/Cargo.toml index 3908c0b9..30c9c0be 100644 --- a/crates/macro/Cargo.toml +++ b/crates/macro/Cargo.toml @@ -26,3 +26,4 @@ quote = "1.0" [dev-dependencies] trybuild = "1.0" wasm-bindgen = { path = "../..", version = "0.2.50", features = ['strict-macro'] } +wasm-bindgen-futures = { path = "../futures", version = "0.4.0" } diff --git a/crates/macro/ui-tests/async-errors.rs b/crates/macro/ui-tests/async-errors.rs new file mode 100644 index 00000000..5c0016ce --- /dev/null +++ b/crates/macro/ui-tests/async-errors.rs @@ -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 { loop {} } +#[wasm_bindgen] +pub async fn good7() -> Result { loop {} } +#[wasm_bindgen] +pub async fn good8() -> Result { loop {} } +#[wasm_bindgen] +pub async fn good9() -> Result { loop {} } +#[wasm_bindgen] +pub async fn good10() -> Result { 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 { loop {} } + + +fn main() {} diff --git a/crates/macro/ui-tests/async-errors.stderr b/crates/macro/ui-tests/async-errors.stderr new file mode 100644 index 00000000..708bc569 --- /dev/null +++ b/crates/macro/ui-tests/async-errors.stderr @@ -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: + as wasm_bindgen::__rt::IntoJsResult> + 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: + as wasm_bindgen::__rt::IntoJsResult> + 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` is not satisfied + --> $DIR/async-errors.rs:33:1 + | +33 | #[wasm_bindgen] + | ^^^^^^^^^^^^^^^ the trait `std::convert::From` is not implemented for `wasm_bindgen::JsValue` + | + = help: the following implementations were found: + > + > + > + > + and 62 others + = note: required because of the requirements on the impl of `std::convert::Into` 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: 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` + | + = help: the following implementations were found: + as wasm_bindgen::__rt::IntoJsResult> + 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`. diff --git a/crates/webidl/src/util.rs b/crates/webidl/src/util.rs index 9c8dfc88..2170cd0f 100644 --- a/crates/webidl/src/util.rs +++ b/crates/webidl/src/util.rs @@ -310,6 +310,7 @@ impl<'src> FirstPassRecord<'src> { ret: ret.clone(), rust_attrs: vec![], rust_vis: public(), + r#async: false, }, rust_name: rust_ident(rust_name), js_ret: js_ret.clone(), diff --git a/examples/fetch/src/lib.rs b/examples/fetch/src/lib.rs index e65bdf37..3ef43ae7 100644 --- a/examples/fetch/src/lib.rs +++ b/examples/fetch/src/lib.rs @@ -1,8 +1,6 @@ -use js_sys::Promise; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; -use wasm_bindgen_futures::future_to_promise; use wasm_bindgen_futures::JsFuture; use web_sys::{Request, RequestInit, RequestMode, Response}; @@ -35,11 +33,7 @@ pub struct Signature { } #[wasm_bindgen] -pub fn run() -> Promise { - future_to_promise(run_()) -} - -async fn run_() -> Result { +pub async fn run() -> Result { let mut opts = RequestInit::new(); opts.method("GET"); opts.mode(RequestMode::Cors); diff --git a/guide/src/reference/js-promises-and-rust-futures.md b/guide/src/reference/js-promises-and-rust-futures.md index 3db5b60d..72ce692b 100644 --- a/guide/src/reference/js-promises-and-rust-futures.md +++ b/guide/src/reference/js-promises-and-rust-futures.md @@ -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 { + 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>`. 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` 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` - turns into a successful JS value +* `Result<(), E: Into>` - if `Ok(())` turns into a successful + `undefined` and otherwise turns into a failed promise with `E` converted to a + JS value +* `Result, E: Into>` - 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 and Rust `Future`s. Its `JsFuture` type provides conversion from a JavaScript diff --git a/src/lib.rs b/src/lib.rs index 7cbcb968..811fc145 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -822,6 +822,8 @@ pub fn function_table() -> JsValue { pub mod __rt { use core::cell::{Cell, UnsafeCell}; use core::ops::{Deref, DerefMut}; + use crate::JsValue; + pub extern crate core; #[cfg(feature = "std")] pub extern crate std; @@ -1095,6 +1097,44 @@ pub mod __rt { return ret; } } + + /// An internal helper trait for usage in `#[wasm_bindgen]` on `async` + /// functions to convert the return value of the function to + /// `Result` which is what we'll return to JS (where an + /// error is a failed future). + pub trait IntoJsResult { + fn into_js_result(self) -> Result; + } + + impl IntoJsResult for () { + fn into_js_result(self) -> Result { + Ok(JsValue::undefined()) + } + } + + impl> IntoJsResult for T { + fn into_js_result(self) -> Result { + Ok(self.into()) + } + } + + impl, E: Into> IntoJsResult for Result { + fn into_js_result(self) -> Result { + match self { + Ok(e) => Ok(e.into()), + Err(e) => Err(e.into()), + } + } + } + + impl> IntoJsResult for Result<(), E> { + fn into_js_result(self) -> Result { + match self { + Ok(()) => Ok(JsValue::undefined()), + Err(e) => Err(e.into()), + } + } + } } /// A wrapper type around slices and vectors for binding the `Uint8ClampedArray`