Wasm Notes


Jun 21 2024 | Binary Data

Load data

Having a file .wasm file, we can decode it using some binary debug tools. An interest way to do it is analyzing the internal bits. It is as simple as read the file and write it’s content to a buffer.

We just need to create a method to print out the content of a .wasm file, the result could be copied directly in javascript to create a byte array as Int8Array to be able to create a WebAssembly instance.

fn wasm_printer(filename: &str) {
    let mut file = File::open(filename).expect("could not open the file");
    let metadata = fs::metadata(filename).expect("could not read metadata from file");
    let mut buffer = vec![0; metadata.len() as usize];
    file.read(&mut buffer).expect("error writing file content into buffer");
    println!("{:?}", buffer);
}

We can translate the meaning of each bit reading the Spec of it’s Binary Format

    [0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 7, 10, 1, 6, 97, 100, 100, 84, 119, 111, 0, 0, 10, 9, 1, 7, 0, 32, 0, 32, 1, 106, 11, 0, 10, 4, 110, 97, 109, 101, 2, 3, 1, 0, 0]
async function load() {
  const byteArray = new Int8Array([
    0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0,
    7, 10, 1, 6, 97, 100, 100, 84, 119, 111, 0, 0, 10, 9, 1, 7, 0, 32, 0, 32, 1,
    106, 11, 0, 10, 4, 110, 97, 109, 101, 2, 3, 1, 0, 0,
  ]);
  const wasm = await WebAssembly.instantiate(byteArray.buffer);
  const addTwo = wasm.instance.exports.addTwo;
  console.log(addTwo(1, 10));
}

Or we can just simply load it the classic way using fetch.

async function load() {
+  const response = await fetch("test.wasm");
+  const buffer = await response.arrayBuffer();
-  const byteArray = new Int8Array([
-    0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0,
-    7, 10, 1, 6, 97, 100, 100, 84, 119, 111, 0, 0, 10, 9, 1, 7, 0, 32, 0, 32, 1,
-    106, 11, 0, 10, 4, 110, 97, 109, 101, 2, 3, 1, 0, 0,
-  ]);
+  const wasm = await WebAssembly.instantiate(buffer);
-  const wasm = await WebAssembly.instantiate(byteArray.buffer);
  const addTwo = wasm.instance.exports.addTwo;
  console.log(addTwo(1, 10));
}

Import from wasm

If we need to call some methods from a mdoule inside wasm, we can do it pretty simple. Imagine our module exports some methods.

const module = {
  public: {
    hi: () => console.log("Hi from module"),
    bye: () => console.log("Bye! from module"),
  },
};

Pass this module to wasm instance.

- const wasm = await WebAssembly.instantiate(buffer);
+ const wasm = await WebAssembly.instantiate(buffer, module);

We need to import our exposed methods public -> hi & public -> bye

  (import "public" "hi" (func $hi))
  (import "public" "bye" (func $bye))

Then we can invoke them like:

    call $hi
    call $bye

Sharing memory

Wasm to Javascript

First let’s add some memory in wasm file.

  • 1 means a page, meaning around 64Kb
  • data used to load some data into memory
(module
  (import "public" "hi" (func $hi))
  (import "public" "bye" (func $bye))
+ (memory $mem 1)
+ (data (i32.const 0) "hi")
  (func (export "addTwo") (param i32 i32) (result i32)
    call $hi
    call $bye
    local.get 0
    local.get 1
    i32.add)
+ (export "mem" (memory $mem))
)

Now lets retrieve this 2 bits string hi in javascript.

async function load() {
    const  module = {
        public: {
            hi: () => console.log("Hi from module"),
            bye: () => console.log("Bye! from module"),
        }
    };

    const response = await fetch("memory.wasm");
    const buffer = await response.arrayBuffer();

    const wasm = await WebAssembly.instantiate(buffer, module);
    const addTwo = wasm.instance.exports.addTwo;
+   const wasmMemory = wasm.instance.exports.mem;
+   const memoryArray = new Uint8Array(wasmMemory.array, 0, 2);

+   const wasmText = new TextDecoder().decode(memoryArray);
    console.log(addTwo(1,10))
+   console.log(wasmText)
}

Javascript to Wasm

To test how we can set some memory from javascript into wasm memory let’s define some memory in javacript file, then instantiate wasm file and fill this memory from it.

+   const memory = WebAssembly.Memory({initial: 1}); //-> here memory is empty
    const  module = {
        public: {
+           mem: memory,
            hi: () => console.log("Hi from module"),
            bye: () => console.log("Bye! from module"),
        }
    };
    const response = await fetch("memjs.wasm");
    const buffer = await response.arrayBuffer();

    const wasm = await WebAssembly.instantiate(buffer, module);
    //-> after instantiate, memory mutated with "hi" from wasm file
    const addTwo = wasm.instance.exports.addTwo;
-   const wasmMemory = wasm.instance.exports.mem;
-   const memoryArray = new Uint8Array(wasmMemory.array, 0, 2);
+   const memoryArray = new Uint8Array(memory.buffer, 0, 2);
    const wasmText = new TextDecoder().decode(memoryArray);
    console.log(addTwo(1,10))
    console.log(wasmText)
(module
  (import "public" "hi" (func $hi))
  (import "public" "bye" (func $bye))
- (memory $mem 1)
+ (memory (import "public" "mem") 1) //-> here, memory is changed by the imported one, then "hi" is set to this memory slot.
  (data (i32.const 0) "hi")
  (func (export "addTwo") (param i32 i32) (result i32)
    call $hi
    call $bye
    local.get 0
    local.get 1
    i32.add)
- (export "mem" (memory $mem))
)