madonoharu / tsify

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

Entire Wasm instance is invalidated if serde impl from macro crashes on deserialization step #47

Open CinchBlue opened 7 months ago

CinchBlue commented 7 months ago

Here is a minimal example.

Make a new project. Use these files:

lib.rs:

use std::sync::Arc;

use js_sys::Promise;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::future_to_promise;

extern crate web_sys;

// A macro to provide `println!(..)`-style syntax for `console.log` logging.
#[macro_export]
macro_rules! console_log {
    ( $( $t:tt )* ) => {
        web_sys::console::log_1(&format!( $( $t )* ).into());
    }
}

#[wasm_bindgen]
pub struct MyApi {
    pub(crate) api: Arc<tokio::sync::RwLock<ApiImpl>>,
}

#[wasm_bindgen]
pub struct ApiImpl {
    pub(crate) name: String,
    pub(crate) some_stuff: Arc<tokio::sync::RwLock<u32>>,
}

#[wasm_bindgen]
impl ApiImpl {
    pub fn new(name: String) -> Self {
        Self {
            name,
            some_stuff: Arc::new(RwLock::new(0)),
        }
    }

    pub fn get_name(&self) -> String {
        self.name.clone()
    }

    pub async fn do_stuff(&self) -> u32 {
        let mut some_stuff = self.some_stuff.write().await;
        *some_stuff += 1;
        *some_stuff
    }

    pub async fn get_stuff(&self) -> u32 {
        let some_stuff = self.some_stuff.read().await;
        *some_stuff
    }

    pub fn add_game_to_cart(
        &mut self,
        game_item_dbid: Gid,
        // game_group_dbid: Option<Gid>,
        // source: PlayerActionSource,
    ) -> Result<String, String> {
        if game_item_dbid.data == 0 {
            Err("game_group_dbid is none".to_string())
        } else {
            Ok("ok".to_string())
        }
    }
}

#[wasm_bindgen]
impl MyApi {
    pub fn new(name: String) -> Self {
        Self {
            api: Arc::new(RwLock::new(ApiImpl::new(name))),
        }
    }

    pub async fn get_name(&self) -> String {
        let api = self.api.read().await;
        api.get_name()
    }

    pub async fn do_stuff(&self) -> u32 {
        let mut api = self.api.write().await;
        api.do_stuff().await
    }

    pub async fn get_stuff(&self) -> u32 {
        let api = self.api.read().await;
        console_log!("get_stuff");
        api.get_stuff().await
    }

    pub fn add_game_to_cart(
        &mut self,
        game_item_dbid: Gid,
        // game_group_dbid: Option<Gid>,
        // source: PlayerActionSource,
    ) -> Promise {
        console_log!("add_game_to_cart");
        let api = self.api.clone();
        console_log!("add_game_to_cart");
        future_to_promise(async move {
            let mut api = api.write().await;
            let result = api
                .add_game_to_cart(game_item_dbid) //, game_group_dbid, source)
                .to_js_result()?;
            let serializer = serde_wasm_bindgen::Serializer::json_compatible();
            let result = result.serialize(&serializer);
            Ok(result?)
        })
    }
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, tsify::Tsify)]
#[tsify(from_wasm_abi)]
#[serde(tag = "kind", content = "payload")]
#[serde(rename_all = "snake_case")]
pub enum PlayerActionSource {
    Unknown,
    Aig,
    Navigation,
    Search { search_string: String },
}

#[derive(
    Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, Clone, serde::Serialize, serde::Deserialize,
)]
#[cfg_attr(
    any(target_arch = "wasm32", target_os = "macos"),
    derive(tsify::Tsify),
    tsify(from_wasm_abi, into_wasm_abi)
)]
pub struct Gid {
    pub data: u128,
}

use wasm_bindgen::JsValue;

pub type JsResult<T> = Result<T, JSError>;

pub trait ToJSResultTrait<T>: Sized {
    fn to_js_result(self) -> JsResult<T>;

    fn to_js_result_msg(self, msg: &str) -> JsResult<T> {
        match self.to_js_result() {
            Ok(value) => Ok(value),
            Err(err) => Err(JSError {
                message: format!("{}: {}", msg, err.message),
            }),
        }
    }
}

impl<T> ToJSResultTrait<T> for Option<T> {
    fn to_js_result(self) -> JsResult<T> {
        match self {
            Some(value) => Ok(value),
            None => Err(JSError {
                message: "Option is None".to_string(),
            }),
        }
    }
}

impl<T> ToJSResultTrait<T> for Result<T, String> {
    fn to_js_result(self) -> JsResult<T> {
        match self {
            Ok(value) => Ok(value),
            Err(err) => Err(JSError { message: err }),
        }
    }
}

impl<T> ToJSResultTrait<T> for Result<T, reqwest::Error> {
    fn to_js_result(self) -> JsResult<T> {
        match self {
            Ok(value) => Ok(value),
            Err(err) => Err(JSError {
                message: err.to_string(),
            }),
        }
    }
}

impl From<JSError> for JsValue {
    fn from(error: JSError) -> JsValue {
        serde_wasm_bindgen::to_value(&error).unwrap()
    }
}

use wasm_bindgen::prelude::wasm_bindgen;

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, tsify::Tsify)]
#[tsify(from_wasm_abi)]
pub struct JSError {
    pub message: String,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, tsify::Tsify)]
#[tsify(from_wasm_abi)]
pub struct JSAnalyticItem {
    pub order_id: String,
    pub action_type: String,
    pub test_id: Option<i16>,
    pub object: Option<String>,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, tsify::Tsify)]
#[tsify(from_wasm_abi)]
#[serde(tag = "variant", content = "data")]
#[serde(rename_all = "snake_case")]
pub enum JSStreamStatus {
    Start,
    Retry(i8),
    GiveUp,
    Error(String),
}

Cargo.toml

[workspace]
resolver = "2"

[package]
name = "learn-rust-wasm"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
async-trait = "0.1.77"
cfg-if = "1.0.0"
futures = "0.3.30"
getrandom = {version = "0.2.8", features = ["js"] }
rand = "0.8.5"
reqwest = "0.11.23"
serde = "1.0.195"
serde-wasm-bindgen = "0.6.3"
serde_json = "1.0.111"
serde_yaml = "0.9.30"
tokio = { version = "1.35.1", features = ["macros", "rt", "sync"] }
tokio-stream = "0.1.14"
tracing = "0.1.40"
tsify = "0.4.5"
wasm-bindgen = "0.2.90"
wasm-bindgen-futures = "0.4.40"
console_error_panic_hook = { version = "0.1.6", optional = true }
wee_alloc = { version = "0.4.5", optional = true }
js-sys = "0.3.67"
tracing-wasm = "0.2.1"
wasm-streams = "0.4.0"
ws_stream_wasm = "0.7.4"
web-sys = {version = "0.3", features = [ "console", "ReadableStream", "BinaryType", "Blob", "ErrorEvent", "FileReader", "MessageEvent", "ProgressEvent", "WebSocket", ]}

.cargo/config.toml

[build]
target = "wasm32-unknown-unknown"
rustflags = ["--cfg", "tokio_unstable"]
rustdocflags = ["--cfg", "tokio_unstable"]

web/index.js

import { MyApi } from "learn_rust_wasm";

export async function debug_all() {

    // console.log('----------------- debug_wasm() -----------------');
    // await debug_wasm();
}

export function getApi() {
    let api = MyApi.new("lol");
    debugger;
    return api;
}

let x = getApi();

window.api = x;
console.log(await window.api.get_name());
console.log(await window.api.do_stuff());

web/bootstrap.js

// A dependency graph that contains any wasm must all be imported
// asynchronously. This `bootstrap.js` file does the single async import, so
// that no one else needs to worry about it again.
import("./index.js")
  .catch(e => console.error("Error importing `index.js`:", e));

let api = import("learn_rust_wasm");

web/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Hello wasm-pack!</title>
  </head>
  <body>
    <noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
    <script src="./bootstrap.js"></script>
  </body>
</html>

web/package.json

{
  "name": "create-wasm-app",
  "version": "0.1.0",
  "description": "create an app to consume rust-generated wasm packages",
  "main": "index.js",
  "bin": {
    "create-wasm-app": ".bin/create-wasm-app.js"
  },
  "scripts": {
    "build": "webpack --config webpack.config.js",
    "start": "webpack-dev-server --mode development"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/rustwasm/create-wasm-app.git"
  },
  "keywords": [
    "webassembly",
    "wasm",
    "rust",
    "webpack"
  ],
  "author": "Ashley Williams <ashley666ashley@gmail.com>",
  "license": "(MIT OR Apache-2.0)",
  "bugs": {
    "url": "https://github.com/rustwasm/create-wasm-app/issues"
  },
  "homepage": "https://github.com/rustwasm/create-wasm-app#readme",
  "devDependencies": {
    "copy-webpack-plugin": "^5.0.0",
    "hello-wasm-pack": "^0.1.0",
    "webpack": "^5.89.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^4.15.1"
  },
  "dependencies": {
    "learn_rust_wasm": "file:../pkg"
  }
}

web/webpack.config.js

const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require('path');

module.exports = {
  entry: "./bootstrap.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bootstrap.js",
  },
  mode: "development",
  plugins: [
    new CopyWebpackPlugin(['index.html'])
  ],
  experiments: {
    asyncWebAssembly: true,
  },
  devtool: 'source-map'
};

then run:

wasm-pack build; (cd web && npm i && npm run start)

When you go to http://localhost:8080 and then run this, it will fail in this way:

>>> window.api.add_game_to_cart({})
Uncaught Error: missing field `data` at line 1 column 2
    __wbindgen_throw learn_rust_wasm_bg.js:537
    add_game_to_cart learn_rust_wasm_bg.js:384
    <anonymous> debugger eval code:1
[learn_rust_wasm_bg.js:537](webpack://create-wasm-app/pkg/learn_rust_wasm_bg.js)
    __wbindgen_throw learn_rust_wasm_bg.js:537
    <anonymous> 3456e6f9d69608745039.module.wasm:91904
    <anonymous> 3456e6f9d69608745039.module.wasm:65348
    add_game_to_cart learn_rust_wasm_bg.js:384
    <anonymous> debugger eval code:1
>>> window.api.add_game_to_cart({})
Uncaught Error: recursive use of an object detected which would lead to unsafe aliasing in rust
    __wbindgen_throw learn_rust_wasm_bg.js:537
    add_game_to_cart learn_rust_wasm_bg.js:384
    <anonymous> debugger eval code:1
[learn_rust_wasm_bg.js:537](webpack://create-wasm-app/pkg/learn_rust_wasm_bg.js)
>>> window.api.get_stuff({})
Uncaught Error: recursive use of an object detected which would lead to unsafe aliasing in rust
    __wbindgen_throw learn_rust_wasm_bg.js:537
    __wbg_adapter_22 learn_rust_wasm_bg.js:216
    real learn_rust_wasm_bg.js:201
    __wbg_queueMicrotask_118eeb525d584d9a learn_rust_wasm_bg.js:437
    __wbg_adapter_49 learn_rust_wasm_bg.js:227
    cb0 learn_rust_wasm_bg.js:501
    __wbg_new_1d93771b84541aa5 learn_rust_wasm_bg.js:506
    get_stuff learn_rust_wasm_bg.js:376
    <anonymous> debugger eval code:1
[learn_rust_wasm_bg.js:537](webpack://create-wasm-app/pkg/learn_rust_wasm_bg.js)
CinchBlue commented 7 months ago

The problem is that it will create a panic and the entire wasm instance will no longer be valid.

CinchBlue commented 7 months ago

See example repo: https://github.com/CinchBlue/wasm-pack-problem-example