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)]