Search code examples
rustwebassemblyserdewasm-bindgen

How to pass an array of objects to WebAssembly and convert it to a vector of structs with wasm-bindgen?


It's possible to pass an array of integers like this:

const js = import("./webassembly_rust");
let array_nums = [1,2,3,4,5,6,7,8,9];

js.then(js => {
  js.test( array_nums );
}); 

to WebAssembly and save it in a vector like this:

extern crate serde_json;
extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

#[macro_use]
extern crate serde_derive;

#[wasm_bindgen]
pub fn test(array: JsValue) {
    let elements: Vec<u32> = array.into_serde().unwrap();
}

It's also possible to pass a single object like this:

const js = import("./webassembly_rust");
let jsObject = {name: "hello world", id: "99", parent_id: "11"};

js.then(js => {
  js.test( jsObject );
}); 

to WebAssembly and save it as a Element struct like this:

extern crate serde_json;
extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

#[macro_use]
extern crate serde_derive;

#[derive(Serialize, Deserialize)]
pub struct Element {
    name: String,
    id: String,
    parent_id: String,
}

#[wasm_bindgen]
pub fn test(js_object: &JsValue) {
    let element: Element = js_object.into_serde().unwrap();
}

The next thing I tried is to pass an array of objects like this:

const js = import("./webassembly_rust");
let arrayOfObjects = [
  {name: "hello world", id: "99", parent_id: "88"},
  {name: "hello world2", id: "88", parent_id: "12"},
  {name: "hello world3", id: "77", parent_id: "88"}
]

js.then(js => {
  js.test( arrayOfObjects );
}); 

to WebAssembly and save it as a vector of Element structs like this:

extern crate serde_json;
extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

#[macro_use]
extern crate serde_derive;

#[derive(Serialize, Deserialize)]
pub struct Element {
    name: String,
    id: String,
    parent_id: String,
}

#[wasm_bindgen]
pub fn test(js_objects: &JsValue) {
    let elements: Vec<Element> = js_objects.into_serde().unwrap();
}

This compiles, but when I run this code I get the error:

func $__rust_start_panic (param i32) (result i32)
  unreachable
  unreachable
end

screenshot_promise_rejection_error

Passing an array of objects filled with numbers like this:

const js = import("./webassembly_rust");
let arrayOfNumObjects = [
    {name: 1, id: 2, parent_id: 3 },
    {name: 1, id: 2, parent_id: 3 },
    {name: 1, id: 2, parent_id: 3 }
]

js.then(js => {
  js.test( arrayOfNumObjects );
}); 

to WebAssembly is possible when the Element struct contains only u32 values.

extern crate serde_json;
extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;

#[macro_use]
extern crate serde_derive;

#[derive(Serialize, Deserialize)]
pub struct Element {
    name: u32,
    id: u32,
    parent_id: u32,
}

#[wasm_bindgen]
pub fn test(js_objects: &JsValue) {
    let elements: Vec<Element> = js_objects.into_serde().unwrap();
}

It seems like the problem is caused by the String type in the Element struct.

What did I do wrong?

I found the following articles, but I can't find a solution for my problem in them:

  • Serializing and Deserializing Arbitrary Data Into and From JsValue with Serde

    This explains how to convert a JavaScript object to a struct, but not how to convert an array of objects to a vector of structs.

  • js_sys crate

    This crate allows to use JavaScript types like arrays or objects in Rust, but this is not what I want. I want to convert JavaScript values into their Rust counterparts. This crate only allows to use JavaScript inline in Rust, as far as I understand... and this isn't as fast as using just Rust.

  • github issue


Solution

  • Follow the instructions to get a basic Rust / WASM setup, then add support for arbitrary data via Serde.

    I've changed your code to return a number and print out that number, just to see that it's working.

    Cargo.toml

    [package]
    name = "ww"
    version = "0.1.0"
    authors = ["An Devloper <an.devloper@example.com>"]
    edition = "2018"
    
    [lib]
    crate-type = ["cdylib"]
    
    [dependencies]
    wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
    serde_json = "1.0.32"
    serde_derive = "1.0.80"
    serde = "1.0.80"
    

    src/lib.rs

    extern crate serde_json;
    extern crate wasm_bindgen;
    
    use wasm_bindgen::prelude::*;
    
    #[macro_use]
    extern crate serde_derive;
    
    #[derive(Serialize, Deserialize)]
    pub struct Element {
        name: String,
        id: String,
        parent_id: String,
    }
    
    #[wasm_bindgen]
    pub fn test(js_objects: &JsValue) -> i32 {
        let elements: Vec<Element> = js_objects.into_serde().unwrap();
        elements
            .iter()
            .map(|e| {
                let id = e.id.parse::<i32>().unwrap_or(0);
                let parent_id = e.parent_id.parse::<i32>().unwrap_or(0);
                id + parent_id
            })
            .sum()
    }
    

    index.js

    const js = import("./ww");
    
    let arrayOfObjects = [
      { name: "hello world", id: "99", parent_id: "88" },
      { name: "hello world2", id: "88", parent_id: "12" },
      { name: "hello world3", id: "77", parent_id: "88" },
    ]
    
    js.then(js => {
      const sum = js.test(arrayOfObjects);
      console.log(sum);
    });
    

    package.json

    {
      "scripts": {
        "serve": "webpack-dev-server"
      },
      "devDependencies": {
        "html-webpack-plugin": "^3.2.0",
        "webpack": "^4.0.1",
        "webpack-cli": "^3.1.1",
        "webpack-dev-server": "^3.1.0"
      }
    }
    

    webpack.config.js

    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    
    module.exports = {
      entry: "./index.js",
      output: {
        path: path.resolve(__dirname, "dist"),
        filename: "index.js",
      },
      plugins: [
        new HtmlWebpackPlugin({
          title: "Getting started with WASM"
        })
      ],
      mode: "development"
    };
    

    Then run:

    # once
    npm install
    # every time the code changes
    cargo +nightly build --target wasm32-unknown-unknown
    wasm-bindgen target/wasm32-unknown-unknown/debug/*.wasm --out-dir .
    npm run serve
    

    Visit the page in your WASM-enabled browser.


    Observant readers will note that I didn't do anything different from the OP. That's because this code already worked as-is. Every time you change your Rust code, make sure that you:

    1. Build your Rust code
    2. Re-run wasm-bindgen