tsify icon indicating copy to clipboard operation
tsify copied to clipboard

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

Open CinchBlue opened this issue 1 year ago • 3 comments

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 <[email protected]>",
  "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 avatar Jan 24 '24 00:01 CinchBlue

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

CinchBlue avatar Jan 24 '24 00:01 CinchBlue

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

CinchBlue avatar Jan 24 '24 00:01 CinchBlue