DEV Community

Sendil Kumar
Sendil Kumar

Posted on • Updated on • Originally published at sendilkumarn.com

Memory Model in WebAssembly

For JavaScript to execute, the JavaScript engine should download the resources. The JavaScript engine waits until the resources are downloaded. Once downloaded, the JavaScript engine parses. The parser converts the source code to byte code that JavaScript interpreter runs.

When a function is called multiple times. The baseline compiler (in v8) compiles the code. The compilation happens in the main thread. The compiler spends time for compilation. But the compiled code runs faster than the interpreted code. The compiled code is optimised by the optimising compiler.

When the function is called a lot more. The compiler marks the function and tries to optimise further. During this re-optimisation, the compiler assumes and produces even more optimised code. This optimisation takes a bit of time but the generated code is much faster.

The function is executed. Finally, the code is garbage collected.

Alt Text

WebAssembly is fast. 🚀

The JavaScript engine downloads the WebAssembly module. Once downloaded the WebAssembly module is decoded.

Decoding is faster than parsing.

Once decoded, the WebAssembly module is compiled and optimised. This step is fast because the module has already been compiled and optimised.

The module is finally executed.

Alt Text

Note: there is no separate garbage collection step. The WebAssembly module takes care of allocating and de-allocating the memory.


Check out my book on Rust and WebAssembly here


In the quest of speeding up WebAssembly execution, the browser vendors implemented streaming compilation. Streaming compilation enables JavaScript engines to compile and optimise the module while the WebAssembly module is still downloading. Unlike JavaScript, where the engines should wait until the file is completely downloaded. This speeds up the process.


JavaScript and WebAssembly are two different things at the browser level. Calling WebAssembly from JavaScript or vice versa is slow. (This holds good for calls between any two languages). This is because crossing boundaries has a cost attached to it.

The browser vendors (especially Firefox) are trying to reduce the cost of boundary-crossing. In fact, in Firefox the JavaScript to WebAssembly call is much faster than the non-inlined JavaScript to JavaScript calls.

But still, proper care should be given to the boundary-crossing while designing your application. They can be a major performance bottleneck for the application. In those cases, it is important to understand the memory model of the WebAssembly module.

Memory model in WebAssembly

The memory section of the WebAssembly module is a vector of linear memories.

Linear Memory Model

A linear memory model is a memory addressing technique in which the memory is organized in a single contagious address space. It is also known as Flat memory model.

While the linear memory model makes it easier to understand, program, and represent the memory.

They have huge disadvantages like

  • high execution time for rearranging elements
  • wastes a lot of memory area

Alt Text

The memory is a vector of raw bytes of uninterpreted data. They use resizable array buffers to hold the raw bytes of memory. JavaScript and WebAssembly can synchronously read and write into the memory.

We can allocate the memory using WebAssembly.memory() constructor from JavaScript.


Write some code ✍️

Passing from WebAssembly to JavaScript

Let us first see how to pass values through memory from WebAssembly Module (written with Rust) to JavaScript.

Create a new project using cargo.

$ cargo new --lib memory_world
Enter fullscreen mode Exit fullscreen mode

Once the project is successfully created. Open the project in your favourite editor. Let us edit the src/lib.rs with the following contents

#![no_std]

use core::panic::PanicInfo;
use core::slice::from_raw_parts_mut;

#[no_mangle]
fn memory_to_js() {
    let obj: &mut [u8];

    unsafe {
        obj = from_raw_parts_mut::<u8>(0 as *mut u8, 1);
    }

    obj[0] = 13;
}

#[panic_handler]
fn panic(_info: &PanicInfo) -> !{
    loop{}
}
Enter fullscreen mode Exit fullscreen mode

Add this to the Cargo.toml:

[lib]
crate-type = ["cdylib"]
Enter fullscreen mode Exit fullscreen mode

What is there?

The rust file starts with #![no_std]. The #![no_std] attribute instructs the rust compiler to fallback to core crate instead of std crate. The core crate is platform agnostic. The core crate is a smaller subset of the std crate. This reduces the binary size dramatically.

The function memory_to_js is annotated with #[no_mangle]. This function does not return any value, because it changes the value in the shared memory.

We define a mutable slice of type u8 and name it as obj. Then we use from_raw_parts_mut to create a u8 using a pointer and length. By default the memory starts at 0 and we just take 1 element.

We are accessing the raw memory so we wrap the calls inside the unsafe block. The generated slice from from_raw_parts_mut is mutable.

Finally, we assign 13 in the first index.

unsafe {
    obj = from_raw_parts_mut::<u8>(0 as *mut u8, 1);
}

obj[0] = 13;
Enter fullscreen mode Exit fullscreen mode

We have also defined a panic_handler to capture any panics and ignore it for now (do not do this in your production application).

Note that we are not using wasm_bindgen here.

In JavaScript, we load the WebAssembly module and access the memory straightaway from the module.

First, fetch and instantiate the WebAssembly module.

const bytes = await fetch("target/wasm32-unknown-unknown/debug/memory_world.wasm");
const response = await bytes.arrayBuffer();
const result = await WebAssembly.instantiate(response, {});
Enter fullscreen mode Exit fullscreen mode

The result object is the WebAssembly object that contains all the imported and exported functions. We call the exported memory_to_js function from the result.exports.

result.exports.memory_to_js();
Enter fullscreen mode Exit fullscreen mode

This calls the WebAssembly module's memory_to_js function and assigns the value in the shared memory.

The shared memory is exported by result.exports.memory.buffer object.

const memObj = new UInt8Array(result.exports.memory.buffer, 0).slice(0, 1);
console.log(memObj[0]); // 13
Enter fullscreen mode Exit fullscreen mode

The memory is accessed via load and store binary instructions. These binary instructions are accessed with the offset and the alignment. The alignment is in base 2 logarithmic representation.

Note: WebAssembly currently provides only 32-bit address ranges. In future, WebAssembly may provide 64-bit address range.


Passing from JavaScript to WebAssembly

We have seen how to share memory between JavaScript and WebAssembly, by creating the memory in Rust. Now it is time to create a memory in JavaScript and use it inside Rust.

The memory in the JavaScript land has no way to tell the WebAssembly land what to allocate and when to free them. Being type, WebAssembly expects explicit type information. We need to tell the WebAssembly land how to allocate the memory and then how to free them.

To create the memory via JavaScript, use the WebAssembly.Memory() constructor.

The memory constructor takes in an object to set the defaults. They are

  • initial - The initial size of the memory
  • maximum - The maximum size of the memory (Optional)
  • shared - to denote whether to use the shared memory

The unit for initial and maximum is (WebAssembly) pages. Each page holds up to 64KB.


Write some code ✍️

Initialise the memory,

const memory = new WebAssembly.Memory({initial: 10, maximum: 100});
Enter fullscreen mode Exit fullscreen mode

The memory is initialized with WebAssembly.Memory() constructor with an initial value of 10 pages and a maximum value of 100 pages. This translates to 640KB and 6.4MB initial and maximum respectively.

const bytes = await fetch("target/wasm32-unknown-unknown/debug/memory_world.wasm");
const response = await bytes.arrayBuffer();
const instance = await WebAssembly.instantiate(response, { js: { mem: memory } });
Enter fullscreen mode Exit fullscreen mode

We fetch the WebAssembly Module and instantiate them. But while instantiating we pass in the memory object.

const s = new Set([1, 2, 3]);
let jsArr = Uint8Array.from(s);
Enter fullscreen mode Exit fullscreen mode

We create a typedArray (UInt8Array) with values 1, 2, and 3.

const len = jsArr.length;
let wasmArrPtr = instance.exports.malloc(length);
Enter fullscreen mode Exit fullscreen mode

WebAssembly modules will not have any clue about the size of the objects that are created in the memory. WebAssembly needs to allocate memory. We have to manually write the allocation and freeing of memory. In this step, we send the length of the array and allocate that memory. This will give us a pointer to the location of the memory.

let wasmArr = new Uint8Array(instance.exports.memory.buffer, wasmArrPtr, len);
Enter fullscreen mode Exit fullscreen mode

We then create a new typedArray with the buffer (total available memory), memory offset (wasmAttrPtr), and the length of the memory.

wasmArr.set(jsArr);
Enter fullscreen mode Exit fullscreen mode

We finally set the locally created typedArray (jsArr) into the typedArray wasmArrPtr.

const sum = instance.exports.accumulate(wasmArrPtr, len); // -> 7
console.log(sum);
Enter fullscreen mode Exit fullscreen mode

We are sending the pointer (to memory) and length to WebAssembly module. In the WebAssembly module, we fetch the value from the memory and use them.

In the Rust, the malloc and accumulate functions are as follows:

use std::alloc::{alloc, dealloc,  Layout};
use std::mem;

#[no_mangle]
fn malloc(size: usize) -> *mut u8 {
    let align = std::mem::align_of::<usize>();
    if let Ok(layout) = Layout::from_size_align(size, align) {
        unsafe {
            if layout.size() > 0 {
                let ptr = alloc(layout);
                if !ptr.is_null() {
                    return ptr
                }
            } else {
                return align as *mut u8
            }
        }
    }
    std::process::abort
}
Enter fullscreen mode Exit fullscreen mode

Given the size, the malloc function allocates a block of memory.

#[no_mangle]
fn accumulate(data: *mut u8, len: usize) -> i32 {
    let y = unsafe { std::slice::from_raw_parts(data as *const u8, len) };
    let mut sum = 0;
    for i in 0..len {
        sum = sum + y[i];
    }
    sum as i32
}
Enter fullscreen mode Exit fullscreen mode

The accumulate function takes in the shared array and the size (len). It then recovers the data from the shared memory. Then runs through the data and returns the sum of all the elements passed in the data.

If you have enjoyed the post, then you might like my book on Rust and WebAssembly. Check them out here



👇 Repo 👇

GitHub logo sendilkumarn / rustwasm-memory-model

Sharing memory between WebAssembly and JavaScript with Rust


Interested to explore further

WebAssembly Memory using JavaScript API at here

Memory access in the WebAssembly is safer check at here

Check out more about from_raw_parts_mut at here

Check out more about TypedArray here


🐦 Twitter // 💻 GitHub // ✍️ Blog // 🔶 hackernews

If you like this article, please leave a like or a comment. ❤️


Top comments (4)

Collapse
 
funnymania profile image
funnymania

Awesome Sendil. Thank you. I recently did a demonstration on the speed improvements of rust wasm crypto vs JS in the browser. I used wasm_bindgen, but it's good to see the memory model in action

Collapse
 
sendilkumarn profile image
Sendil Kumar

Thanks glad you liked it. Yeah wasm crypto is something very exciting, did you achieve any results here? I am also experimenting something in line :)

Collapse
 
funnymania profile image
funnymania

Sure, you can check the repo.

github.com/funnymania/wasm-rhymes-...

The rust-wasm module was around 6 times faster, however on a less sizable input than BTC, as it is cumbersome to work within javascript' s number limitations