Wasm-bindgen: Unable to use async functions on instance methods

Created on 9 Nov 2019  路  3Comments  路  Source: rustwasm/wasm-bindgen

Using the async keyword on normal functions works fine, but if I want to use it on a method for an object I get some compiler errors that I assume are lifetime related:

#[wasm_bindgen]
pub struct Worker {
    user: PouchDB,
    group: PouchDB,
}

#[wasm_bindgen]
impl Worker {
    // ...
    pub async fn process(&self, command: String) -> Result<JsValue, JsValue> {
        let output = match command.as_str() {
            "adapter" => { self.user.adapter() }
            "info" => {
                match JsFuture::from(self.user.info()).await {
                    Ok(resolved) => {
                        match resolved.into_serde::<Info>() {
                            Ok(val) => format!("{:?}", &val),
                            Err(_) => "Deserialize error".to_string(),
                        }
                    },
                    Err(_) => "Promise error".to_string(),
                }
            }
            _ => String::from("Unknown command")
        };
        Ok(JsValue::from(output))
    }
}

Compiler error:

error[E0597]: `me` does not live long enough
  --> src\lib.rs:34:1
   |
34 | #[wasm_bindgen]
   | ^^^^^^^^^^^^^^-
   | |             |
   | |             `me` dropped here while still borrowed
   | borrowed value does not live long enough
   | argument requires that `me` is borrowed for `'static`

I tried to implement the fix that the compiler suggests:

pub async fn process(&'static self, command: String) -> Result<JsValue, JsValue>

But then I get the error that is already mentioned in the issue #1187:

error: it is currently not sound to use lifetimes in function signatures
  --> src\lib.rs:47:27
   |
47 |     pub async fn process(&'static self, command: String) -> Result<JsValue, JsValue> {
   |    

As for now the only workaround working for me is creating a normal async function, that consumes the object and returns it back at the end. The processed data is saved internally by the object.

#[wasm_bindgen]
pub struct Worker {
    user: PouchDB,
    group: PouchDB,
    output: String
}

pub async fn process(mut worker: Worker, command: String) -> Worker {
    let output = match command.as_str() {
        "adapter" => { worker.user.adapter() }
        "info" => {
            match JsFuture::from(worker.user.info()).await {
                Ok(resolved) => {
                    match resolved.into_serde::<Info>() {
                        Ok(val) => format!("{:?}", &val),
                        Err(_) => "Deserialize error".to_string(),
                    }
                },
                Err(_) => "Promise error".to_string(),
            }
        }
        _ => String::from("Unknown command")
    };
    worker.output = output;
    worker
}

Usage in JavaScript:

let worker = new wasm.Worker();
...
worker = await wasm.process(worker, data.msg);
let msg = worker.get_output();

Full workaround-code can be found here: https://github.com/U3W/EasyPass/blob/client-backend/WebService/src/main/rust/src/lib.rs

bug

Most helpful comment

This isn't a problem with wasm-bindgen, it's just how Futures work in general, as you can see here:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=7a198bf0c5b75f61f9da4576098fa13a

Because Futures run asynchronously, they always outlive the stack, so they must have the 'static lifetime. But &self is not static, it's stack allocated. You can see here for a more detailed technical explanation.

There are generally two workarounds:

  1. You can return impl Future and hoist all uses of self outside of the Future, like this:

    impl Worker {
       pub fn process(&self, command: String) -> impl Future<Output = Result<JsValue, JsValue>> {
           let info = JsFuture::from(self.user.info());
    
           async move {
               let output = match command.as_str() {
                   "adapter" => { self.user.adapter() }
                   "info" => {
                       match info.await {
                           Ok(resolved) => {
                               match resolved.into_serde::<Info>() {
                                   Ok(val) => format!("{:?}", &val),
                                   Err(_) => "Deserialize error".to_string(),
                               }
                           },
                           Err(_) => "Promise error".to_string(),
                       }
                   }
                   _ => String::from("Unknown command")
               };
               Ok(JsValue::from(output))
           }
       }
    }
    

    Notice how self.user.info() has been hoisted outside of the async Future.

  2. You can accept self by value rather than by reference (this is the workaround that you're using):

    impl Worker {
       pub async fn process(self, command: String) -> Result<JsValue, JsValue> {
           // ...
       }
    }
    

    But obviously then you can't use self after calling process. You can fix that by accepting Rc instead:

    impl Worker {
       pub async fn process(self: Rc<Self>, command: String) -> Result<JsValue, JsValue> {
           // ...
       }
    }
    

    And if you need to mutate self you can combine Rc with RefCell:

    impl Worker {
       pub async fn process(this: Rc<RefCell<Self>>, command: String) -> Result<JsValue, JsValue> {
           // ...
       }
    }
    

    Notice how we had to change it to use this rather than self. That's because Rust doesn't currently allow for using RefCell with self. That also means you would need to call it like Worker::process(worker, foo) rather than worker.process(foo)

All 3 comments

This isn't a problem with wasm-bindgen, it's just how Futures work in general, as you can see here:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=7a198bf0c5b75f61f9da4576098fa13a

Because Futures run asynchronously, they always outlive the stack, so they must have the 'static lifetime. But &self is not static, it's stack allocated. You can see here for a more detailed technical explanation.

There are generally two workarounds:

  1. You can return impl Future and hoist all uses of self outside of the Future, like this:

    impl Worker {
       pub fn process(&self, command: String) -> impl Future<Output = Result<JsValue, JsValue>> {
           let info = JsFuture::from(self.user.info());
    
           async move {
               let output = match command.as_str() {
                   "adapter" => { self.user.adapter() }
                   "info" => {
                       match info.await {
                           Ok(resolved) => {
                               match resolved.into_serde::<Info>() {
                                   Ok(val) => format!("{:?}", &val),
                                   Err(_) => "Deserialize error".to_string(),
                               }
                           },
                           Err(_) => "Promise error".to_string(),
                       }
                   }
                   _ => String::from("Unknown command")
               };
               Ok(JsValue::from(output))
           }
       }
    }
    

    Notice how self.user.info() has been hoisted outside of the async Future.

  2. You can accept self by value rather than by reference (this is the workaround that you're using):

    impl Worker {
       pub async fn process(self, command: String) -> Result<JsValue, JsValue> {
           // ...
       }
    }
    

    But obviously then you can't use self after calling process. You can fix that by accepting Rc instead:

    impl Worker {
       pub async fn process(self: Rc<Self>, command: String) -> Result<JsValue, JsValue> {
           // ...
       }
    }
    

    And if you need to mutate self you can combine Rc with RefCell:

    impl Worker {
       pub async fn process(this: Rc<RefCell<Self>>, command: String) -> Result<JsValue, JsValue> {
           // ...
       }
    }
    

    Notice how we had to change it to use this rather than self. That's because Rust doesn't currently allow for using RefCell with self. That also means you would need to call it like Worker::process(worker, foo) rather than worker.process(foo)

Thank you very much @Pauan for your detailed answer! I finally understand the problem.

I tried applying your workarounds and ran to some problems.

The first workaround gives me following error:

error[E0667]: `impl Trait` is not allowed in path parameters
  --> src\lib.rs:97:19
   |
97 | pub fn testo() -> impl Future<Output = Result<JsValue, JsValue>> {
   |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error[E0562]: `impl Trait` not allowed outside of function and inherent method return types
  --> src\lib.rs:97:19
   |
97 | pub fn testo() -> impl Future<Output = Result<JsValue, JsValue>> {
   |   

The second:

error[E0277]: the trait bound `std::rc::Rc<Worker>: wasm_bindgen::convert::traits::FromWasmAbi` is not satisfied
  --> src\lib.rs:37:1
   |
37 | #[wasm_bindgen]
   | ^^^^^^^^^^^^^^^ the trait `wasm_bindgen::convert::traits::FromWasmAbi` is not implemented for `std::rc::Rc<Worker>`

Fortunately, I found a solution after I checked again the wasm-bindgen-futures documentation. Instead of returning the rust future directly, it is converted to a JS promise first.

    pub fn process(&self, command: String) -> Promise {
        let info = JsFuture::from(self.user.info());
        let adapter = self.user.adapter();

        future_to_promise(async move {
            let output = match command.as_str() {
                "adapter" => adapter,
                "info" => {
                    match info.await {
                        Ok(resolved) => {
                            match resolved.into_serde::<Info>() {
                                Ok(val) => format!("{:?}", &val),
                                Err(_) => "Deserialize error".to_string(),
                            }
                        },
                        Err(_) => "Promise error".to_string(),
                    }
                }
                _ => String::from("Unknown command")
            };
            Ok(JsValue::from(output))
        })
    }

Sadly, its a bit more unconvient to write than an async function, but it works as intended.

I would appreciate if the async book has a chapter on this. I ran into the same issue.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

hunterlester picture hunterlester  路  4Comments

pustaczek picture pustaczek  路  3Comments

NateLing picture NateLing  路  3Comments

fitzgen picture fitzgen  路  3Comments

MarcAntoine-Arnaud picture MarcAntoine-Arnaud  路  3Comments