madonoharu / tsify

A library for generating TypeScript definitions from rust code.
Apache License 2.0
300 stars 41 forks source link

Trait `From<...>` is not implemented for `JsValue` when returning structs in async functions #24

Open jvanmalder opened 1 year ago

jvanmalder commented 1 year ago
Click to show Cargo.toml. ```toml [package] name = "asynctest" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib", "rlib"] [dependencies] tsify = "0.4.5" serde = { version = "1.0", features = ["derive"] } wasm-bindgen = { version = "0.2" } wasm-bindgen-futures = "0.4" [profile.release] # Tell `rustc` to optimize for small code size. opt-level = "s" ```

Using the following code, as in the example in the README, but when using an async function to return a struct:

use serde::{Deserialize, Serialize};
use tsify::Tsify;
use wasm_bindgen::prelude::*;

#[derive(Tsify, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Point {
    x: i32,
    y: i32,
}

#[wasm_bindgen]
pub async fn into_js() -> Point {
    Point { x: 0, y: 0 }
}

#[wasm_bindgen]
pub fn from_js(point: Point) {}

The compiler complains when executing wasm-pack build --target web --release:

[INFO]: Checking for the Wasm target...
[INFO]: Compiling to Wasm...
   Compiling asynctest v0.1.0
error[E0277]: the trait bound `JsValue: From<Point>` is not satisfied
  --> src/lib.rs:12:1
   |
12 | #[wasm_bindgen]
   | ^^^^^^^^^^^^^^^ the trait `From<Point>` is not implemented for `JsValue`
   |
   = help: the following other types implement trait `From<T>`:
             <JsValue as From<&'a T>>
             <JsValue as From<&'a std::string::String>>
             <JsValue as From<&'a str>>
             <JsValue as From<*const T>>
             <JsValue as From<*mut T>>
             <JsValue as From<JsError>>
             <JsValue as From<JsType>>
             <JsValue as From<bool>>
           and 81 others
   = note: required for `Point` to implement `Into<JsValue>`
   = note: required for `Point` to implement `IntoJsResult`
   = note: this error originates in the attribute macro `wasm_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
error: could not compile `tests` (lib) due to previous error
Error: Compiling your crate to WebAssembly failed
Caused by: failed to execute `cargo build`: exited with exit status: 101
  full command: "cargo" "build" "--lib" "--release" "--target" "wasm32-unknown-unknown"

Both using rustc version 1.67.1 (d5a82bbd2 2023-02-07) and 1.71.0-nightly (8b4b20836 2023-05-22). Saw a similar issue here but no real solution.

Edit: digging a bit deeper with cargo rustc --profile=check -- -Zunpretty=expanded revealed the following:

without async generates:

#[allow(dead_code)]
pub fn into_js() -> Point { Point { x: 0, y: 0 } }
#[automatically_derived]
const _: () =
    {
        pub unsafe extern "C" fn __wasm_bindgen_generated_into_js()
            -> <Point as wasm_bindgen::convert::ReturnWasmAbi>::Abi {
            let _ret = { let _ret = into_js(); _ret };
            <Point as wasm_bindgen::convert::ReturnWasmAbi>::return_abi(_ret)
        }
    };

whereas with async generates:

#[allow(dead_code)]
pub async fn into_js() -> Point { Point { x: 0, y: 0 } }
#[automatically_derived]
const _: () =
    {
        pub unsafe extern "C" fn __wasm_bindgen_generated_into_js()
            ->
                <wasm_bindgen::JsValue as
                wasm_bindgen::convert::ReturnWasmAbi>::Abi {
            let _ret =
                wasm_bindgen_futures::future_to_promise(async move
                            {
                            {
                                let _ret = into_js();
                                <Point as
                                        wasm_bindgen::__rt::IntoJsResult>::into_js_result(_ret.await)
                            }
                        }).into();
            <wasm_bindgen::JsValue as
                    wasm_bindgen::convert::ReturnWasmAbi>::return_abi(_ret)
        }
    };
sgantrim commented 1 year ago

This solved the async problem for me:

impl From<Point> for JsValue { fn from(value: Point) -> Self { serde_wasm_bindgen::to_value(&value).unwrap() } }

sgantrim commented 1 year ago

Messed around with this a little more to see if I could automate the trait implementation - otherwise, each Rust structure would have to individually provide the impl block. Turns out a derive macro works great.

use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(TsifyAsync)]
pub fn tsify_async_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate
    let ast = syn::parse(input).unwrap();

    // Build the trait implementation
    impl_tsify_async_macro(&ast)
}

fn impl_tsify_async_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let gen = quote! {
        impl From<#name> for JsValue {
            fn from(value: #name) -> Self {
                serde_wasm_bindgen::to_value(&value).unwrap()
            }
        }
    };
    gen.into()
}

From there we just have to add the macro as a dependency, then add TsifyAsync to the derive section of the struct.

#[derive(Debug, Serialize, Deserialize, Tsify, TsifyAsync)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Foo {
    pub text: String,
    pub yes_or_no: bool,
    pub magic_number: i32,
}