Skip to content

Instantly share code, notes, and snippets.

@tanishiking
Created April 22, 2024 07:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tanishiking/bb3627fc93dc5ce030506ab7f092d7b9 to your computer and use it in GitHub Desktop.
Save tanishiking/bb3627fc93dc5ce030506ab7f092d7b9 to your computer and use it in GitHub Desktop.

Why

WebAssembly interacts with the external environment through imported and exported functions. If it's a simple function that takes an integer and returns an integer, there is no problem.

However, when trying to use complex data types (such as strings or arrays), the limitation of WebAssembly 1.0 having only 4 types (i32, i64, f32, f64) becomes an issue.

To represent strings or user-defined types, we have to represent it using these 4 types, and there are no predefined rules on how to represent them in linear memory. The consumers of these modules need to understand how these data structures are laid out in linear memory and implement glue code accordingly.

The Component Model aims to solve these problems (how data is laid out in linear memory is implementation-dependent, and the implementation of glue code) by providing a standardized way to represent and interact with complex data types across different WebAssembly modules (components) and host environments.

Also, WASI preview 2 is defined based on Wasm Component Model.

What

WebAssembly Component Model defines

  • canonical ABI, a standarized way of how to represent the complex data structures on linear memory.
    • The Canonical ABI specifies, for each component function signature, a corresponding core function signature and the process for reading component-level values into and out of linear memory.

  • WIT
    • The Wasm Interface Type (WIT) format is an IDL to provide tooling for the WebAssembly Component Model in two primary ways:

    • WIT is a developer-friendly format to describe the imports and exports to a component. It is easy to read and write and provides the foundational basis for producing components from guest languages as well as consuming components in host languages.

    • WIT packages are the basis of sharing types and definitions in an ecosystem of components. Authors can import types from other WIT packages when generating a component, publish a WIT package representing a host embedding, or collaborate on a WIT definition of a shared set of APIs between platforms.

We can generate WIT bindings using, for example, wit-bindgen.

Guest

For example, for the WIT below,

// wit/hello.wit
default world hello {
    export run: func() -> string
}

Rust can the hello.wit in

// Generates the interface types and bindings for the WebAssembly component based on a
wit_bindgen::generate!("hello");
struct Component;

// implements the Hello interface for the Component struct. The Hello interface is defined 
impl Hello for Component {
    fn run() -> String {
        "Hello".to_string()
    }
}

// This macro provided by wit-bindgen generates the necessary code to export the Component struct as the implementation of the Hello interface.
export_hello!(Component);

which will be expanded to

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
pub trait Hello {
    fn run() -> wit_bindgen::rt::string::String;
}
#[doc(hidden)]
pub unsafe fn call_run<T: Hello>() -> i32 {
    #[allow(unused_imports)]
    use wit_bindgen::rt::{alloc, vec::Vec, string::String};
    let result0 = T::run();
    let ptr1 = _RET_AREA.0.as_mut_ptr() as i32;
    let vec2 = (result0.into_bytes()).into_boxed_slice();
    let ptr2 = vec2.as_ptr() as i32;
    let len2 = vec2.len() as i32;
    core::mem::forget(vec2);
    *((ptr1 + 4) as *mut i32) = len2;
    *((ptr1 + 0) as *mut i32) = ptr2;
    ptr1
}
#[doc(hidden)]
pub unsafe fn post_return_run<T: Hello>(arg0: i32) {
    wit_bindgen::rt::dealloc(
        *((arg0 + 0) as *const i32),
        (*((arg0 + 4) as *const i32)) as usize,
        1,
    );
}
#[allow(unused_imports)]
use wit_bindgen::rt::{alloc, vec::Vec, string::String};
#[repr(align(4))]
struct _RetArea([u8; 8]);
static mut _RET_AREA: _RetArea = _RetArea([0; 8]);
const _: &str = "default world hello {\n    export run: func() -> string\n}";
struct Component;
impl Hello for Component {
    fn run() -> String {
        "Hello".to_string()
    }
}
const _: () = {
    #[doc(hidden)]
    #[export_name = "run"]
    #[allow(non_snake_case)]
    unsafe extern "C" fn __export_hello_run() -> i32 {
        call_run::<Component>()
    }
    #[doc(hidden)]
    #[export_name = "cabi_post_run"]
    #[allow(non_snake_case)]
    unsafe extern "C" fn __post_return_hello_run(arg0: i32) {
        post_return_run::<Component>(arg0)
    }
};

wit-bindgen generates a glue code that follows a canonical ABI (We will need to implement this bindgen for Scala).

Host

In the README of wit-bindgen, Rust, JavaScript, and Python are introduced as supported Host Runtimes. For example, by running the following command with jco, it generates JavaScript code, d.ts, and wasm binary:

jco transpile hello-component.wasm -o hello
export function run(): string;
const instantiateCore = WebAssembly.instantiate;

let dv = new DataView(new ArrayBuffer());
const dataView = (mem) =>
  dv.buffer === mem.buffer ? dv : (dv = new DataView(mem.buffer));

const utf8Decoder = new TextDecoder();

const isNode =
  typeof process !== "undefined" && process.versions && process.versions.node;
let _fs;
async function fetchCompile(url) {
  if (isNode) {
    _fs = _fs || (await import("fs/promises"));
    return WebAssembly.compile(await _fs.readFile(url));
  }
  return fetch(url).then(WebAssembly.compileStreaming);
}

let exports0;
let memory0;
let postReturn0;

function run() {
  const ret = exports0.run();
  const ptr0 = dataView(memory0).getInt32(ret + 0, true);
  const len0 = dataView(memory0).getInt32(ret + 4, true);
  const result0 = utf8Decoder.decode(
    new Uint8Array(memory0.buffer, ptr0, len0)
  );
  postReturn0(ret);
  return result0;
}

export { run };

const $init = (async () => {
  const module0 = fetchCompile(
    new URL("./hello-component.core.wasm", import.meta.url)
  );
  ({ exports: exports0 } = await instantiateCore(await module0));
  memory0 = exports0.memory;
  postReturn0 = exports0.cabi_post_run;
})();

await $init;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment