From 98d7919615c052bfff44c6d23a255e893d6fe553 Mon Sep 17 00:00:00 2001 From: Rodrigo Rivas Costa <rodrigorivascosta@gmail.com> Date: Sun, 28 Jun 2020 13:32:16 +0200 Subject: [PATCH 1/2] Implement extern "C" async functions. It converts a JS Promise into a wasm_bindgen_futures::JsFuture that implements Future<Result<JsValue, JsValue>>. --- crates/backend/src/codegen.rs | 59 +++++++++++++++---- .../reference/js-promises-and-rust-futures.md | 25 ++++++++ tests/wasm/futures.js | 23 ++++++++ tests/wasm/futures.rs | 59 +++++++++++++++++-- 4 files changed, 151 insertions(+), 15 deletions(-) diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs index ad982ffa1a4..b1b0fb8e3e1 100644 --- a/crates/backend/src/codegen.rs +++ b/crates/backend/src/codegen.rs @@ -971,22 +971,52 @@ impl TryToTokens for ast::ImportFunction { ); } Some(ref ty) => { - abi_ret = quote! { - <#ty as wasm_bindgen::convert::FromWasmAbi>::Abi - }; - convert_ret = quote! { - <#ty as wasm_bindgen::convert::FromWasmAbi> - ::from_abi(#ret_ident) - }; + if self.function.r#async { + abi_ret = quote! { <js_sys::Promise as wasm_bindgen::convert::FromWasmAbi>::Abi }; + let future = quote! { + wasm_bindgen_futures::JsFuture::from( + <js_sys::Promise as wasm_bindgen::convert::FromWasmAbi> + ::from_abi(#ret_ident) + ).await + }; + convert_ret = if self.catch { + quote! { Ok(#future?) } + } else { + quote! { #future.expect("unexpected exception") } + }; + } else { + abi_ret = quote! { + <#ty as wasm_bindgen::convert::FromWasmAbi>::Abi + }; + convert_ret = quote! { + <#ty as wasm_bindgen::convert::FromWasmAbi> + ::from_abi(#ret_ident) + }; + } } None => { - abi_ret = quote! { () }; - convert_ret = quote! { () }; + if self.function.r#async { + abi_ret = quote! { <js_sys::Promise as wasm_bindgen::convert::FromWasmAbi>::Abi }; + let future = quote! { + wasm_bindgen_futures::JsFuture::from( + <js_sys::Promise as wasm_bindgen::convert::FromWasmAbi> + ::from_abi(#ret_ident) + ).await + }; + convert_ret = if self.catch { + quote! { #future?; Ok(()) } + } else { + quote! { #future.expect("uncaught exception"); } + }; + } else { + abi_ret = quote! { () }; + convert_ret = quote! { () }; + } } } let mut exceptional_ret = quote!(); - if self.catch { + if self.catch && !self.function.r#async { convert_ret = quote! { Ok(#convert_ret) }; exceptional_ret = quote! { wasm_bindgen::__rt::take_last_exception()?; @@ -1045,12 +1075,17 @@ impl TryToTokens for ast::ImportFunction { &self.rust_name, ); + let maybe_async = if self.function.r#async { + Some(quote!{async}) + } else { + None + }; let invocation = quote! { #(#attrs)* #[allow(bad_style)] #[doc = #doc_comment] #[allow(clippy::all)] - #vis fn #rust_name(#me #(#arguments),*) #ret { + #vis #maybe_async fn #rust_name(#me #(#arguments),*) #ret { #extern_fn unsafe { @@ -1096,6 +1131,8 @@ impl<'a> ToTokens for DescribeImport<'a> { let nargs = f.function.arguments.len() as u32; let inform_ret = match &f.js_ret { Some(ref t) => quote! { <#t as WasmDescribe>::describe(); }, + // async functions always return a JsValue, even if they say to return () + None if f.function.r#async => quote! { <JsValue as WasmDescribe>::describe(); }, None => quote! { <() as WasmDescribe>::describe(); }, }; diff --git a/guide/src/reference/js-promises-and-rust-futures.md b/guide/src/reference/js-promises-and-rust-futures.md index 4d740bdf251..1598a1e83ba 100644 --- a/guide/src/reference/js-promises-and-rust-futures.md +++ b/guide/src/reference/js-promises-and-rust-futures.md @@ -25,6 +25,31 @@ 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`. +You can also import a JS async function directly with a `extern "C"` block, and +the promise will be converted to a future automatically. For now the return type +must be `JsValue` or no return at all: + +```rust +#[wasm_bindgen] +extern "C" { + async fn async_func_1() -> JsValue; + async fn async_func_2(); +} +``` + +The `async` can be combined with the `catch` attribute to manage errors from the +JS promise: + +```rust +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(catch)] + async fn async_func_3() -> Result<JsValue, JsValue>; + #[wasm_bindgen(catch)] + async fn async_func_4() -> Result<(), JsValue>; +} +``` + 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]`: diff --git a/tests/wasm/futures.js b/tests/wasm/futures.js index 20cec9a89be..487a98a259f 100644 --- a/tests/wasm/futures.js +++ b/tests/wasm/futures.js @@ -14,3 +14,26 @@ exports.call_exports = async function() { assert.strictEqual(8, (await wasm.async_return_8()).val); await assert.rejects(wasm.async_throw(), /async message/); }; + +exports.call_promise = async function() { + return "ok"; +} + +exports.call_promise_ok = async function() { + return "ok"; +} + +exports.call_promise_err = async function() { + throw "error"; +} + +exports.call_promise_unit = async function() { + console.log("asdfasdf"); +} + +exports.call_promise_ok_unit = async function() { +} + +exports.call_promise_err_unit = async function() { + throw "error"; +} diff --git a/tests/wasm/futures.rs b/tests/wasm/futures.rs index 906dcf58f1b..a056f2d6024 100644 --- a/tests/wasm/futures.rs +++ b/tests/wasm/futures.rs @@ -3,14 +3,26 @@ use wasm_bindgen_test::*; #[wasm_bindgen(module = "tests/wasm/futures.js")] extern "C" { - fn call_exports() -> js_sys::Promise; + #[wasm_bindgen(catch)] + async fn call_exports() -> Result<JsValue, JsValue>; + + async fn call_promise() -> JsValue; + #[wasm_bindgen(catch)] + async fn call_promise_ok() -> Result<JsValue, JsValue>; + #[wasm_bindgen(catch)] + async fn call_promise_err() -> Result<JsValue, JsValue>; + + #[wasm_bindgen] + async fn call_promise_unit(); + #[wasm_bindgen(catch)] + async fn call_promise_ok_unit() -> Result<(), JsValue>; + #[wasm_bindgen(catch)] + async fn call_promise_err_unit() -> Result<(), JsValue>; } #[wasm_bindgen_test] async fn smoke() { - wasm_bindgen_futures::JsFuture::from(call_exports()) - .await - .unwrap(); + call_exports().await.unwrap(); } #[wasm_bindgen] @@ -70,3 +82,42 @@ pub async fn async_return_8() -> Result<AsyncCustomReturn, AsyncCustomReturn> { pub async fn async_throw() -> Result<(), js_sys::Error> { Err(js_sys::Error::new("async message")) } + +#[wasm_bindgen_test] +async fn test_promise() { + assert_eq!(call_promise().await.as_string(), Some(String::from("ok"))) +} + +#[wasm_bindgen_test] +async fn test_promise_ok() { + assert_eq!( + call_promise_ok().await.map(|j| j.as_string()), + Ok(Some(String::from("ok"))) + ) +} + +#[wasm_bindgen_test] +async fn test_promise_err() { + assert_eq!( + call_promise_err().await.map_err(|j| j.as_string()), + Err(Some(String::from("error"))) + ) +} + +#[wasm_bindgen_test] +async fn test_promise_unit() { + call_promise_unit().await +} + +#[wasm_bindgen_test] +async fn test_promise_ok_unit() { + call_promise_ok_unit().await.unwrap() +} + +#[wasm_bindgen_test] +async fn test_promise_err_unit() { + assert_eq!( + call_promise_err_unit().await.map_err(|j| j.as_string()), + Err::<(), _>(Some(String::from("error"))) + ) +} From 480be5a6c272b6d13144afbbf1dcb80b2ebd0044 Mon Sep 17 00:00:00 2001 From: Rodrigo Rivas Costa <rodrigorivascosta@gmail.com> Date: Mon, 29 Jun 2020 17:52:25 +0200 Subject: [PATCH 2/2] Run rustfmt. Add #[rustfmt::skip] to the tests/wasm/futures.rs because it removes the async from extern "C" blocks. --- crates/backend/src/codegen.rs | 8 +++++--- tests/wasm/futures.rs | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/backend/src/codegen.rs b/crates/backend/src/codegen.rs index b1b0fb8e3e1..1954c0f7f03 100644 --- a/crates/backend/src/codegen.rs +++ b/crates/backend/src/codegen.rs @@ -972,7 +972,8 @@ impl TryToTokens for ast::ImportFunction { } Some(ref ty) => { if self.function.r#async { - abi_ret = quote! { <js_sys::Promise as wasm_bindgen::convert::FromWasmAbi>::Abi }; + abi_ret = + quote! { <js_sys::Promise as wasm_bindgen::convert::FromWasmAbi>::Abi }; let future = quote! { wasm_bindgen_futures::JsFuture::from( <js_sys::Promise as wasm_bindgen::convert::FromWasmAbi> @@ -996,7 +997,8 @@ impl TryToTokens for ast::ImportFunction { } None => { if self.function.r#async { - abi_ret = quote! { <js_sys::Promise as wasm_bindgen::convert::FromWasmAbi>::Abi }; + abi_ret = + quote! { <js_sys::Promise as wasm_bindgen::convert::FromWasmAbi>::Abi }; let future = quote! { wasm_bindgen_futures::JsFuture::from( <js_sys::Promise as wasm_bindgen::convert::FromWasmAbi> @@ -1076,7 +1078,7 @@ impl TryToTokens for ast::ImportFunction { ); let maybe_async = if self.function.r#async { - Some(quote!{async}) + Some(quote! {async}) } else { None }; diff --git a/tests/wasm/futures.rs b/tests/wasm/futures.rs index a056f2d6024..47c32f69277 100644 --- a/tests/wasm/futures.rs +++ b/tests/wasm/futures.rs @@ -1,6 +1,7 @@ use wasm_bindgen::prelude::*; use wasm_bindgen_test::*; +#[rustfmt::skip] #[wasm_bindgen(module = "tests/wasm/futures.js")] extern "C" { #[wasm_bindgen(catch)]