Skip to content

Files

exploration

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
Apr 24, 2020
Apr 24, 2020
Apr 24, 2020
Apr 24, 2020
Apr 24, 2020
Apr 24, 2020
Apr 24, 2020
Apr 24, 2020

Exploring Sound, Polyglot FFI via Wasm

End Goal: Provide practical, sound interop between programming languages on top of WebAssembly.

Exploration Goal: What does WebAssembly currently provide for sound interop?

Index

  • demo*/
    • demo01 - Embed a WebAssembly inside a Rust program an run a trivial WebAssembly script (hello.wat).
    • demo02 - Compile a trivial addition function (i.e. (a: i32, b: i32) => a + b) in Zig to WebAssembly and run it via a JavaScript host runtime.
    • demo03 - Use the demo02 addition function, compile a trivial subtraction function (i.e. (a: i32, b: i32) => a - b) in Rust to WebAssembly, and compose them in a JavaScript host runtime.
    • demo04 - WIP 3-way w/ Zig, Lys, Rust
    • demo05 - Compile a simple stack implementation in Zig to WebAssembly and use it via a JavaScript host runtime.
    • demo06 - Consume the demo05 stack implementation with a Rust function compiled to WebAssembly and running on a JavaScript host runtime.
  • test01 - Trivial identify function in WebAssembly for testing signature types and runtime conversions.

Usage

Each of the demo* folders should be usable via make. Simply running make will build anything required using the appropriate toolchain (see [Dependencies]) and then run the demo.

Object Interop Vision

NOTE: this syntax was inspired by proposals that have not been accepted: GC (reference types, type imports, typed function references).

Simple ADT (no parameterized types)

Lib.wat (exported by some higher-level source language OR written by hand)

TODO: represent in a higher-level source language for better end-to-end understanding.

(module
  (type $Stack_i32 i32)
  (func $Stack_i32.constructor (result (ref $Stack_i32)) ...)
  (func $Stack_i32.destruct (param (ref $Stack_i32)) ...)
  (func $Stack_i32.push (param (ref $Stack_i32)) (param i32) ...)
  (func $Stack_i32.pop (param (ref $Stack_i32)) (result i32) ...)
  (export ...))

Consumer.rs

#[extern_adt]
impl Stack_i32 {
  fn constructor() -> Stack_i32;
  fn destruct(&self) -> Stack_i32;
  fn push(&self, i32); // NOTE: this could throw an error if insufficient memory is available
  fn pop(&self) -> i32;
}

Parameterized ADTs

Lib.wat

NOTE: This module will have to be reinstantiated for each unique Stack__T type.

(module
  (import "env" "Stack__T" (type $Stack__T))
  (type $Stack i32)
  (func $Stack.constructor (result (ref $Stack)) ...)
  (func $Stack.destruct (param (ref $Stack)) ...)
  (func $Stack.push (param (ref $Stack)) (param (ref $Stack__T)) ...)
  (func $Stack.pop (param (ref $Stack)) (result (ref $Stack__T)) ...)
  (export ...))

Consumer.rs

#[extern_adt]
impl Stack<T> {
  fn constructor() -> Stack<T>;
  fn destruct(&self) -> Stack<T>;
  fn push(&self, T); // NOTE: this could throw an error if insufficient memory is available
  fn pop(&self) -> T;
}

pub struct Person {
  age: u8,
  name: String,
}

TODO

  1. Define destructor naming convention.
    • Motivation: Rust has a Drop trait that will called (when available) when a reference exits scope. Having a standard convention for referring to destructors would allow this trait to be implemented automatically.
      • Alternative (Rust): Expand macro to optionally take the name of a destructor function with the signature (&self) -> nil.
    • Existing Work: GC proposal briefly mentions a possible extension Weak References and Finalization.
  2. Define name-mangling conventions so that names are usable by all source languages. * Motivation: Rust can't reference extern functions that have . in the name.
  3. Define host environment conventions for when to reinstantiate a module vs. when to reuse an existing one.
    • This is a performance optimization, the safe approach is to reinstantiate with every use.
  4. Define host environment conventions for namespacing modules.

Questions

  1. How should Generics/Paramaterized Types be handled?
  2. Where should ADT instances be stored?
    • Within Wasm Modules private memory?
      • Pro: Abstraction is easier to enforce.
      • Pro: Less work on the caller.
      • Con: Poor cache performance when working with instances from multiple libraries.
      • Con: If library uses GC and consumer doesn't, intermixes different memory management conventions.
    • The GC proposal appears to want to integrate ADTs with the GC system.
  3. How to propagate errors between languages?

Notes

  • Wasm Modules and abstract types are the primitives needed to implement this.
    • Wasm Modules enforced interface adherence and (mostly) hides underyling memory.
    • Abstrac types enable opaque object instance identification without having to refer to memory locations.
      • Could be circumvented with a mapping table, but abstract types are more convenient due to export/import capabilities and param references (to strengthen signature type checking). See Section 2.1 in my thesis on why mapping tables are insufficient.

Dependencies

Focused on configuration for a macOS development machine, but all toolchain componenets should be available on Linux and Windows as well.

  • Runtimes
    • wasm-interp (wabt | macos: brew)
    • node (node | macos: brew, fish: nvm)
  • Compilers
    • General Wasm/Wat (wabt | macos: brew)
    • CPP (emscripten | macos: emsdk)
      • Deprecated approach: brew install emscripten
        • Also need binaryen (macos: yarn)
        • Update ~/.emscripten to link to:
          • llvm from brew (LLVM_ROOT = /usr/local/Cellar/llvm/9.0.1/bin)
          • binaryen from yarn/npm global (BINARYEN_ROOT = /usr/local/bin)
        • FIX: unable to find wasm-emscripten-finalize when installed this way
    • Rust (rustup | macos: brew)
      • rustup target add wasm32-unknown-unknown
    • Zig (zig | macos: brew)