0

When using a vector of #[wasm_bindgen] structs in javascript, is there a guarantee that the order of the struct's fields will be maintained so that bytes in wasm memory can be correctly interpreted in JS? I would like to be able to have a vector of structs in Rust and deterministically be able to reconstruct the structs on the JS side without having to serialize across the wasm boundary with serde. I have the following Rust code:

#[wasm_bindgen]
pub struct S {
    a: u8,
    b: u16,
}

#[wasm_bindgen]
pub struct Container {
    ss: Vec<S>,
}

#[wasm_bindgen]
impl Container {
    pub fn new() -> Self {
        let ss = (0..10_u8)
            .map(|i| S {
                a: i,
                b: (2 * i).into(),
            })
            .collect();
        Self { ss }
    }
    
    pub fn items_ptr(&self) -> *const S {
        self.ss.as_ptr()
    }

    pub fn item_size(&self) -> usize {
        std::mem::size_of::<S>()
    }

    pub fn buffer_len(&self) -> usize {
        self.ss.len() * self.item_size()
    }
}

Now, on the JS side, I have the following:

import { memory } from "rust_wasm/rust_wasm.wasm";
import * as wasm from "rust_wasm";

const container = wasm.Container.new();
const items = new Uint8Array(memory.buffer, container.items_ptr(), container.buffer_len());

function getItemBytes(n) {
    const itemSize = container.item_size();
    const start = n * itemSize;
    const end = start + itemSize;
    return items.slice(start, end);
}

With the above code, I can obtain the (four, due to alignment) bytes that comprise an S in JS. Now, my question: how do I interpret those bytes to reconstruct the S (the fields a and b) in JS? In theory, I'd like to be able to say something like the following:

const itemBytes = getItemBytes(3);
const a = itemBytes[0];
const b = itemBytes[1] << 8 + itemBytes[2]

But of course this relies on the fact that the layout in memory of an instance of S matches its definition, that u16 is big endian, etc. Is it possible to get Rust to enforce these properties so that I can safely interpret bytes in JS?

BallpointBen
  • 9,406
  • 1
  • 32
  • 62
  • I'm not sure why you wouldn't want to use serde, as it's really its job to do this kind of things, and it's definitively more clean. Besides that, I think you need something like `#[repr(C)]`. – jthulhu Aug 26 '22 at 16:53
  • Also you may be interested in [the nomicon](https://doc.rust-lang.org/nomicon/other-reprs.html). – jthulhu Aug 26 '22 at 16:55
  • @BlackBeans My reason is that it's much more expensive to send a stringified version of the struct across the wasm boundary. Especially in the event that the Container is mutable, in which case I can't send `items` across once and forever after access it in JS, but need to send every item accessed across. I think performance would be much better if, every time I updated the Container I reconstructed the `Uint8Array` with a new length and then indexed into it. – BallpointBen Aug 26 '22 at 17:24
  • As BlackBeans recommended, `#[repr(C)]` on that struct will guarantee that the fields occur in order. You will need to research field order, alignment, and padding in C structs. (For instance, in this case, field `b` will probably start at byte 2, not byte 1). – PitaJ Aug 26 '22 at 18:00
  • @BallpointBen your are mistaken on what serde does. Serde provides a framework for *any* kind of serialization / deserialization, no matter the format. This means that you can use it to produce JSON, but also binary blobs. Basically, it can do what you are trying, but better (because it doesn't require using an "inferior" representation such as `C`, which prevents Rust from optimizing the layout of the struct, among other reasons). – jthulhu Aug 26 '22 at 20:00
  • 2
    Basically, you're trying to reinvent the wheel. See for example [bincode](https://docs.rs/bincode/latest/bincode/). – jthulhu Aug 26 '22 at 20:01
  • It seems I may actually want https://github.com/cloudflare/serde-wasm-bindgen – BallpointBen Aug 26 '22 at 20:06

0 Answers0