Passing multiple values from WebAssembly to Javascript can be harder than it needs to be. Normally, I find that I have to:
Multi-value is a feature of Wasm intended to make this easier, where multiple values can be passed directly from Wasm to JavaScript, eliminating the need to deal with pointers. The steps become:
For example:
(module
(func $multResult (export "multResult")
(result f64 f64)
f64.const 1
f64.const 2
)
)
We directly output 1 and 2.
I can use Rick Battagline's helpful functions to compile Wasm from WAT (with a slight fix to properly support the multi-value flag):
node ./bin/watwasm bugrepro.wat -o newoutput.wasm -O3 --multi-value
Turning the resulting Wasm compilation back into WAT, we get:
(module
(type $none_=>_f64_f64 (func (result f64 f64)))
(export "multResult" (func $0))
(func $0 (result f64 f64)
(tuple.make
(f64.const 1)
(f64.const 2)
)
)
)
That tuple.make command is the secret sauce that makes the function consumable directly in JavaScript. If I write this JavaScript:
const fs = require('fs');
const wasmBytes = fs.readFileSync('./newoutput.wasm');
WebAssembly.instantiate(wasmBytes)
.then(obj => obj.instance.exports)
.then(exported => exported.multResult())
.then(res => console.log(res));
I can see that [1,2] is returned by the Wasm function. Terrific.
I want to be able to do this with higher-level languages than WAT. Do any higher-level languages produce multi-value Wasm?
There are two: Rust and C. Rust seems to produce the smallest .wasm file so far.
TinyGo does not. AssemblyScript does not. Grain does not. SwiftWASM does not.
For Rust
The below example assumes that you intend to use WebAssembly in a browser. Compiling for WASI (using WebAssembly in an operating system) is slightly different; see this comment. The primary point of WebAssembly is to provide light, fast code in browsers, and how light/how fast depends on whether the WebAssembly goals align with the source language's priorities. The below example assumes that you want your WebAssembly maximally light. (See here for helpful information on how to reduce .wasm size in more complex scenarios.)
npm install -g binaryenwasm32-unknown-unknown using rustup target add wasm32-unknown-unknown. (WASI uses a different target.)cargo new, and add this code to Cargo.toml:[profile.release]
lto = true
opt-level = "z"
strip = "debuginfo"
[lib]
crate-type = ["cdylib"]
lib.rs and add the following:#[no_mangle]
fn flip(a: u32, b: u32) -> (u32, u32) {
(b, a)
}
Apply the following environment variable wherever you intend to run the cargo build command: RUSTFLAGS="-Ctarget-feature=+multivalue -Clink-args=-zstack-size=64000". (Implementation details depend on your operating system and command line program.)
Rundown of the flags:
-Ctarget-feature=+multivalue: tell Rust to enable the WASM multi-value feature
-Clink-args=-zstack-size=64000: tell Rust to use one page of WASM linear memory. (Rust defaults to 16 pages of memory. For a very simple use case like the example, this is wasteful, so reset the memory to use the minimum setting. Browsers allocate 1 page per WASM thread anyway.)
You're going to want a release version, not a debug version. In step 5 above, strip = "debuginfo" is stripping out the debug information. Besides, the point of the program is to run within the context of a browser, so any debugging/testing should be done by embedding the code within Javascript. Use this command to compile a .wasm file: cargo build --release --target=wasm32-unknown-unknown
Use the wasm-opt tool from Binaryen to squash the code more: wasm-opt -Oz -o [output file name].wasm [input file name].wasm
Using this page to convert the .wasm into .wat, here is the entire program:
(module
(type $t0 (func (param i32 i32) (result i32 i32)))
(func $flip (export "flip") (type $t0) (param $p0 i32) (param $p1 i32) (result i32 i32)
(local.get $p1)
(local.get $p0))
(memory $memory (export "memory") 1)
(global $__data_end (export "__data_end") i32 (i32.const 64000))
(global $__heap_base (export "__heap_base") i32 (i32.const 64000)))
Very clean. Not perfect, but just about. At just 239 bytes, the .wasm file is smaller than that produced in C.
For C
You have to use the Emscripten compiler in a special way. (This is apparently only otherwise mentioned anywhere in this Twitter entry.)
First, build the function in C. For example:
typedef struct _nums
{
int x;
int y;
} nums;
nums echo(int x, int y)
{
nums result = {x,y};
return result;
}
This defines a C struct containing two ints, then defines a function which echoes whatever is sent to it.
It can be compiled with Emscripten as follows:
emcc -mmultivalue -Xclang -target-abi -Xclang experimental-mv -Oz -s STANDALONE_WASM -s EXPORTED_FUNCTIONS="['_echo']" -Wl,--no-entry hello.world.c -o bob.wasm
Quick breakdown of the flags:
-mmultivalue: tells Clang to enable multi-value support-Xclang -target-abi: tells Clang to target an application binary interface-Xclang experimental-mv: enables more multi-value stuff?-Oz: tells Clang to produce code aggressively optimized for size-s STANDALONE_WASM: tells Emscripten not to produce Javascript glue code that it normally produces-s EXPORTED_FUNCTIONS="['_echo']": tells Emscripten not to optimize away the 'echo' function if it finds no reference to it. This allows you to export the code to Javascript from WebAssembly.-Wl,--no-entry: tells Emscripten not to try to make a default entry in the WebAssemblyhello.world.c: the input C file-o bob.wasm: output a file called 'bob.wasm'This results in this code:
(module
(type $none_=>_i32 (func (result i32)))
(type $i32_=>_none (func (param i32)))
(type $i32_=>_i32 (func (param i32) (result i32)))
(type $i32_i32_=>_i32_i32 (func (param i32 i32) (result i32 i32)))
(memory $0 256 256)
(table $0 1 1 funcref)
(global $global$0 (mut i32) (i32.const 5243920))
(export "memory" (memory $0))
(export "echo" (func $0))
(export "__indirect_function_table" (table $0))
(export "__errno_location" (func $4))
(export "stackSave" (func $1))
(export "stackRestore" (func $2))
(export "stackAlloc" (func $3))
(func $0 (param $0 i32) (param $1 i32) (result i32 i32)
(tuple.make
(local.get $0)
(local.get $1)
)
)
(func $1 (result i32)
(global.get $global$0)
)
(func $2 (param $0 i32)
(global.set $global$0
(local.get $0)
)
)
(func $3 (param $0 i32) (result i32)
(global.set $global$0
(local.tee $0
(i32.and
(i32.sub
(global.get $global$0)
(local.get $0)
)
(i32.const -16)
)
)
)
(local.get $0)
)
(func $4 (result i32)
(i32.const 1024)
)
)
That produces a lot of boilerplate. The important bits:
...
(export "echo" (func $0))
...
(func $0 (param $0 i32) (param $1 i32) (result i32 i32)
(tuple.make
(local.get $0)
(local.get $1)
)
)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With