WASM: does anyone support multi-value WASM?

Viewed 117

Passing multiple values from WebAssembly to Javascript can be harder than it needs to be. Normally, I find that I have to:

  1. Setup memory shared between Javascript and WebAssembly.
  2. Do the WASM work to produce the values.
  3. Store the values somewhere in the shared memory.
  4. Output a memory pointer to Javascript.
  5. Have Javascript retrieve the values from shared memory.

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:

  1. Do the WASM work to produce the values.
  2. Output the values to Javascript.

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?

1 Answers

There is one: C.

TinyGo does not; Rust does not; AssemblyScript does not. I don't know whether Grain does; I can find no documentation either way.

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:

  1. -mmultivalue: tells Clang to enable multi-value support
  2. -Xclang -target-abi: tells Clang to target an application binary interface
  3. -Xclang experimental-mv: enables more multi-value stuff?
  4. -Oz: tells Clang to produce code aggressively optimized for size
  5. -s STANDALONE_WASM: tells Emscripten not to produce Javascript glue code that it normally produces
  6. -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.
  7. -Wl,--no-entry: tells Emscripten not to try to make a default entry in the WebAssembly
  8. hello.world.c: the input C file
  9. -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)
  )
 )

The situation with C being the only language capable of producing multi-value code is probably going to change soon. Rust seems to occasionally poke at a fix.

Related