How to read a vec of structs in WASM memory from JS?

In JavaScript, I'm trying to loop over a vector of structs created in Rust without copying the data (serialize -> send -> deserialize). I followed the WASM Game of Life tutorial, but it only brushes over working with memory. I'm not able to figure out how to go from working with an exact set of u8s like they do, to working with an arbitrary set of structs where not all fields are u8s.

My project files are the same as the tutorial's, except the following.

Rust

use {std::ops, wasm_bindgen::prelude::*};

// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy)]
pub enum DiagnosticSeverity {
    Err = 1,
    Warn = 2,
    Info = 3,
    Hint = 4,
}

#[wasm_bindgen]
pub struct Diagnostic {
    range: ops::Range<usize>,
    message: String,
    severity: DiagnosticSeverity,
}
#[wasm_bindgen]
impl Diagnostic {
    pub fn new() -> Self {
        Diagnostic {
            // Randomly selected to see if I can
            // recognize them in JavaScript
            range: 30..544,
            message: "Test diagnostic".to_string(),
            severity: DiagnosticSeverity::Info,
        }
    }
    pub fn msg(&self) -> String {
        self.message.to_owned()
    }
    pub fn severity(&self) -> DiagnosticSeverity {
        self.severity
    }
}

#[wasm_bindgen]
pub struct DiagList {
    items: Vec<Diagnostic>,
}
#[wasm_bindgen]
impl DiagList {
    pub fn new() -> Self {
        Self { items: Vec::new() }
    }
    pub fn push(&mut self) {
        self.items.push(Diagnostic {
            // Get different values for testing purposes
            range: self.len()..self.len() + 3,
            message: format!("Diag#{}", self.len()),
            // In JS, I `push()` twice and want each to have a
            // different value for testing purposes
            severity: if self.len() % 2 == 0 {
                DiagnosticSeverity::Info
            } else {
                DiagnosticSeverity::Hint
            },
        });
    }
    pub fn items(&self) -> *const Diagnostic {
        self.items.as_ptr()
    }
    pub fn buffer_len(&self) -> usize {
        self.items.len() * std::mem::size_of::<Diagnostic>()
    }
    pub fn len(&self) -> usize {
        self.items.len()
    }
}

JavaScript (index.js)

import { DiagList, Diagnostic, DiagnosticSeverity } from "wasm_test/wasm_test";
import { memory } from 'wasm_test/wasm_test_bg'

const list = DiagList.new();
const listItemsPtr = list.items();

list.push()
list.push()
const listItems = new Uint8Array(memory.buffer, listItemsPtr, list.len());

console.log(listItems);

The above JavaScript will log [0, 0], which I guess are null pointers?

I'm also not sure if using list.len() is the right "length", so I also tried

const listItems = new Uint8Array(memory.buffer, listItemsPtr, list.buffer_len());

which logs the JavaScript equivalent of [0; 48]. I have no idea what this means or why I'm getting all zeros.

Is every struct's total bytes, from all its fields, condensed into a single array of bytes? So, the size of a Vec<Diagnostic> would be:
the size of a Vec + the sum of a Diagnostic's fields' sizes (their types + their data)?

How do you get an element in listItems at each index, and then how do you get the data in each field without creating a getter in Rust for every field or part of a field you want to read?

Rust types don't have a guarenteed layout unless you specify a repr attribute. If you want to access the fields without deserializing or using getters, they'll need to be #[repr(c)].

The layout of #[repr(c)] structs is explained in the reference

For most use cases you will be better off using getters, or serialization.

Ah, so even if I knew how to read the memory I would probably get the wrong data anyway (without #[repr(c)].. So, how would I iterate over the elements and call each element's getters if I have a getter to the vec?

You can't do much of anything with a Vec on it's own from JS. Here's one way you could do it

use wasm_bindgen::prelude::*;

struct Value {
    name: String,
}

#[wasm_bindgen]
pub struct ExportedVec(Vec<Value>);

#[wasm_bindgen]
impl ExportedVec {
    pub fn get_name(&self, index: usize) -> String {
        self.0[index].name.clone()
    }
}

#[wasm_bindgen]
pub fn make_vec() -> ExportedVec {
    ExportedVec(vec![
        Value { name: "one".into() },
        Value { name: "two".into() },
    ])
}

Then you can use it from JS like this (I'm not using a bundler, if you are your init code and import paths may look different)

import init, { make_vec } from './target/wasm-bindgen/main/sample.js';

async function run() {
    await init();

    let vec = make_vec();

    console.log(vec.get_name(0));
    console.log(vec.get_name(1));

    vec.free()
}

run();

Thanks, that works well.. It's just a bummer the data has to be copied anyway, and every data structure I have in a Rust program that outputs something I want to read in JS, I'd have to do all that every time..

Ah well.

Any kind of FFI generally involves some amount of copying since languages have different ways of representing things.

In that example the only data getting copied is the strings returned by get_name, the ExportedVec JS object wasm_bindgen creates only has a "pointer" into the wasm memory

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.