rustmemorywebassembly

Allocating/Deallocating Memory in Rust compiled to Webassembly


My Situation
I have been trying to make an IO game with parts of the client in Rust, compiled with wasm-pack to Webassembly, which I access through Javascript. Rust is used here as a libary, and gets called when packages need to be handled and the game-logic needs to be handled. For it to be able to handle the logic though, it need the context of all the entities and the map in the game, which can be a lot of data. Now functions that need the game context are called hundreds of times per second and I dont want to implement any batching logic and just want to avoid the problem entirely. That's why I decided to use the memory system in WebAssembly (https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Memory) to store the gamestate so I can retrieve it easily from Rust.

TLDR: I want to avoid passing the game context (Im making an IO Game, using JS and Rust as WASM using wasm-bindgen and wasm-pack) from Javascript to Rust and instead want to store it in memory.

Actual Problem
How do I now Allocate and Deallocate, Read from and Write to this Memory? My current implementation (below)

lib.rs

pub mod memory_operations {
    [...] Includes
    
    ///Write
    unsafe fn write_mem<T>(ptr: *mut u8, x: T) {
        let ptr = ptr as *mut T;
        unsafe {
            *ptr = x;
        }
    }
    
    /// READ
    unsafe fn read_mem<'a, T>(ptr: *mut u8) -> &'a mut T {
        let ptr = ptr as *mut T;
        unsafe {
            &mut *ptr
        }
    }

    /// Allocate
    unsafe fn allocate_memory<T>() -> MemorySpace {
        let size = std::mem::size_of::<T>();
        let mut memory = Vec::with_capacity(size);
        let ptr: *mut u8 = memory.as_mut_ptr();
        std::mem::forget(memory);
        MemorySpace::new(ptr as *mut T)
    }

    /// Deallocate without getting T back and without calling the deconstructor
    unsafe fn deallocate_memory_raw(memory: MemorySpace) {
        let buffer = unsafe {
            std::slice::from_raw_parts_mut(memory.ptr(), memory.size())
        };
        drop(buffer);
    }

    /// Deallocate a Layout in memory as a type T and get that type T back
    unsafe fn deallocate_memory_type<'a, T>(memory: MemorySpace) -> &'a mut T {
        let ptr = memory.ptr() as *mut T;
        println!("Dealloc Type");
        unsafe { &mut *ptr }
    }
    
    #[derive(Debug, Clone, Copy)]
    /// Simple Wrapper for Layout and pointer to pass it to JS
    pub struct MemorySpace {
        offset: *mut u8, 
        size: usize,
        align: usize,
    }

    impl MemorySpace {
        pub fn new<T: Sized>(offset: *mut T) -> MemorySpace {
            let layout = 
            unsafe { alloc::Layout::for_value_raw(offset) };

            let offset = offset as *mut u8;
            MemorySpace {offset, size: layout.size(), align: layout.align()}
        }

        pub fn ptr(&self) -> *mut u8 {self.offset}
        pub fn size(&self) -> usize {self.size}
        pub fn align(&self) -> usize {self.align}

        pub fn layout(&self) -> Result<Layout, LayoutError> {
            Layout::from_size_align(self.size, self.align)
        }
    }
}

This currently fails when writing, seems to just not allocate or to allocate and deallocate the memory. Im currently just testing this as a binary instead of WASM file so I can debug the problems premturely.

Question
Im wondering if this is the correct approach of accessing memory this when compiling to wasm (If you think this memory thing is dumb, let me know). If that is the case, how would I fix this code to run as a binary and as a wasm file?


Solution

  • You mention that you are building a JS/Rust game using wasm-bindgen and wasm-pack. Why not simply use wasm-bindgen to do the memory sharing as well? As documented here, it allows you to annotate structs and methods such that they can be passed to JS and have an object-like interface where the methods invoke the Rust methods. The object itself remains allocated in the WASM memory.

    If you want to do this yourself, for some reason, then at least have a look at what wasm-bindgen generates in the JS glue code. It creates a number of typed arrays, for example Uint8Array, that are backed by wasm.memory.buffer. Raw access to the WASM memory is then done by writing or reading this typed array. This is also what the glue methods do.

    Using your code, I was able to create a MemorySpace, pass it to a JS function, the JS function wrote some data, then Rust could read it back. I used #[wasm_bindgen] on MemorySpace and its impl block.

    cafce25 points out a problem in your deallocation method: dropping the result of std::slice::from_raw_parts_mut will not do anything, since the slice is not owning. You want to restore the full Vec using Vec::from_raw_parts, then drop that.

    I would also note that MemorySpace should deallocate in its Drop handler rather than with a separate deallocation method which you might forget to invoke. On a similar note, you should make sure that you retain a reference to a MemorySpace that you passed to JS as long as it is meant to be usable.