Skip to content

Latest commit

 

History

History
174 lines (134 loc) · 5.77 KB

File metadata and controls

174 lines (134 loc) · 5.77 KB

Example #5 - Autogenerated wasm IT polyfill

This is a polyfill for Interface Types. It generates one additional wasm module per .itl input, and uses one shared JS loader as a runtime.


This example demonstrates how one might implement IT adapters in Wasm itself.

How does it work?

Each module has two parts: the core wasm module, and an adapter wasm module.

C++ user code

The core wasm module is built with no additional compiler support. The C++ here is taken directly from Example #2, using the C++ DSL.

Looking at fizz.cpp, we have two normal C++ functions, isFizz and fizzStr, which we export with a DSL annotation:

export {
    func isFizz(s32) -> u1;
    func fizzStr() -> string;
}

// This gets translated into the following C++ function declarations

__attribute__((export_name("isFizz"))) bool isFizz(int);
__attribute__((export_name("fizzStr"))) const char* fizzStr();

Which in turn become Wasm function exports.

In fizzbuzz.cpp, we have matching imports:

import "fizz" {
    func isFizz(s32) -> u1;
    func fizzStr() -> string;
}

// Which becomes

__attribute__((import_module("fizz"), import_name("isFizz"))) bool isFizz(int);
__attribute__((import_module("fizz"), import_name("fizzStr"))) const char* fizzStr();

Generated ITL

Then the C++ DSL generates an .itl file, which represents its IT definition.

(export
    (func _it_isFizz "isFizz" (param s32) (result u1)
        (as u1 (call isFizz
            (as i32 (local 0))))
    )
    (func _it_fizzStr "fizzStr" (param ) (result string)
        (call _it_cppToString (call fizzStr))
    )
)

The general pattern of what these functions do:

  1. translate the arguments from IT to C++
  2. call the internal function
  3. translate the return value from C++ back to IT

IT .wat

From the ITL, we can generate a JS or wasm wrapper. Here, we're choosing wasm.

We can transcribe those functions into wasm that looks like:

;; core wasm functions
(func $isFizz (param i32) (result i32)
    (call_indirect (param i32) (result i32)
        (local.get 0)
        (i32.const 0)) ;; 0 = isFizz function index
)
(func $fizzStr (param ) (result i32)
    (call_indirect (param ) (result i32)
         (i32.const 1)) ;; 1 = fizzStr function index
)

;; exported IT-wrapped versions
(func $_it_isFizz (export "isFizz") (param i32) (result i32)
    (local )
    (call $isFizz (local.get 0))
)
(func $_it_fizzStr (export "fizzStr") (param ) (result externref)
    (local )
    (call $_it_cppToString (call $fizzStr ))
)

The core wasm functions are called via a call_indirect. This is so we can support loading multiple wasm modules from a single ITL, possibly with circular dependencies. (This is overkill for the simple C++ case, but is a useful mechanism for more general ITL usage).

The IT-wrapped versions that are exported from this wrapper module use higher-level signatures. Specifically here, we convert string into externref so we can pass external objects by-reference across the boundary (JS strings in this case).

There's another important piece here, the "_it_init" function. This is called in order to initialize the wrapper module, by it_loader.js in this case. It looks like this:

(func (export "_it_init")
    (global.set $wasm (call $ref_to_i32 (call $_it_load_wasm (i32.const 7))))
    (call $_it_set_table_func (i32.const 0) (call $i32_to_ref (global.get $wasm)) (i32.const 21))
    (call $_it_set_table_func (i32.const 1) (call $i32_to_ref (global.get $wasm)) (i32.const 28))
    (call $_it_set_table_func (i32.const 2) (call $i32_to_ref (global.get $wasm)) (i32.const 36))
    (call $_it_set_table_func (i32.const 3) (call $i32_to_ref (global.get $wasm)) (i32.const 43))
    (call $_it_set_table_func (i32.const 4) (call $i32_to_ref (global.get $wasm)) (i32.const 54))
)

This loads the wasm module and stores a reference to it, accessible via a global. Then it populates the table of function references used by the call_indirects mentioned previously.

The first i32.const arguments refer to function indices. The second i32.consts refer to the wrapper module's data section, which holds the string data used to read from the core module's exports. They look like so:

(memory (export "_it_memory") 1)
(data (i32.const 36) "\06malloc")
(data (i32.const 28) "\07fizzStr")
(data (i32.const 43) "\0a_it_strlen")
(data (i32.const 7) "\0dout/fizz.wasm")
(data (i32.const 21) "\06isFizz")
(data (i32.const 54) "\13_it_writeStringTerm")
(data (i32.const 0) "\06memory")

Here, strings are represented as a length byte, followed by the payload bytes. This is similar to how strings are encoded in the wasm binary format itself. If strings with length > 255 bytes are needed, we can use LEBs here in the future.

Loader JS

There's one shared JS file that handles loading any ITL-derived wasm modules, it_loader.js. Its main purpose is to provide the key IT runtime functions needed:

  • string_len: reads the length of any IT strings
  • mem_to_string: reads a string out of wasm memory
  • string_to_mem: writes a string into wasm memory
  • load_wasm: loads and instantiates a wasm module (instance) from a filename
  • set_table_func: stores a function reference in the IT module's table
  • ref_to_i32: converts from an externref to an i32, to store in a mutable global
  • i32_to_ref: converts from i32 back to externref

Additionally, the loader is responsible for maintaining the JS side of the runtime (specifically managing interop lookup tables for refs), and loading the wasm module. This is done via the instantiate function, which takes the path to the IT-wrapped module (which in turn has the path to the core module), as well as any imports.

All in all, fizz.wasm can be loaded in NodeJS like so:

let loader = require('./it_loader.js');
let fizz = await loader.instantiate('out/it_fizz.wasm', {});