wasm-ffi

0.3.0 • Public • Published

wasm-ffi

A lightweight foreign function interface library for JavaScript & WebAssembly

Build Status Coverage Status

👉 Demo & Examples
🚥 Run the tests

wasm-ffi helps translate types across the JS ↔ WebAssembly boundary, including:

  • strings
  • arrays
  • structs
  • pointers
  • some Rust types (option, vector, string, enum, etc.)
  • combinations of the above

Heavily based on the ideas & syntax of node-ffi and emscripten (cwrap/ccall)

Contents

Why

WebAssembly only supports number types (i32, u32, f32, f64), so it can be clumsy to work with. What if you want to return a string? or pass an object? You can't, directly. You have to pass pointers to memory instead.

Each WebAssembly instance is backed by a memory object. Your module will use this buffer for memory, but you can also read & manipulate it from JavaScript. If you want to pass a string to WebAssembly, you need to write that string to memory, and then pass a pointer to it.

wasm-ffi wraps your WebAssembly functions and does this pointer conversion for you. It takes objects/strings and translates them into pointers for your function calls. It takes struct pointers and lets you use them like plain JS objects. It even handles the padding in structs so you don't have to do it yourself.

The goal here is to reduce friction and make WebAssembly easier to work with.


Example

If you had a WebAssembly interface like this:

  • make_todo(name, priority)*Todo
  • get_most_important()*Todo
  • mark_complete(*Todo)

You would use wasm-ffi like this:

import { Wrapper, Struct, Pointer, types } from 'wasm-ffi';
 
// define a new struct type: Todo
const Todo = new Struct({
  task_name: 'string', // (char *)
  priority:  'uint32',
  complete:  'bool',
  some_ptr:   types.pointer('bool'),
});
 
 
// wrap your WebAssembly function calls with this form:
// name: [return type, [argument types...]
const library = new Wrapper({
  // `make_todo` takes a string ptr and a number, returns a Todo ptr:
  make_todo: [Todo, ['string', 'number']]
  // `get_most_important` takes no arguments, returns a Todo ptr:
  get_most_important: [Todo]
  // `mark_complete` takes a Todo ptr, returns nothing:
  mark_complete: [null, [Todo]]
});
 
 
// fetch the module and instantiate it:
library.fetch('todo.wasm').then(() => {
  // use wrapped functions:
  const todo = library.make_todo('buy milk', 50);
 
  // the todo pointer gets wrapped into a useful object:
  console.log(`Is ${todo.task_name} complete?: ${todo.complete}`);
 
  // struct fields access wasm memory on the fly with getters & setters:
  todo.priority = 100;
 
  // you can dereference pointers to get their data:
  asset(todo.some_ptr.deref() === true);
 
  // you can make new structs from JS:
  const other = new Todo({
    task_name: 'Learn to make bagels',
    priority: 100,
    complete: false,
    some_ptr: new Pointer('bool', false),
  });
 
  // and pass them to WebAssembly:
  library.mark_complete(other);
 
  // then free them from memory:
  other.free();
});

Check the live examples for more.


Install

npm install wasm-ffi

Or, if you don't want to mess with webpack yet:

<script src="https://unpkg.com/wasm-ffi"></script>

Usage:

// imported as a module:
import { Wrapper, Struct, types, cwrap } from 'wasm-ffi';
 
// with require:
const ffi = require('wasm-ffi');
const Wrapper = ffi.Wrapper
 
// or when loaded from a <script> tag, use the `ffi` global:
const Wrapper = ffi.Wrapper
const Pointer = ffi.Pointer

Requirements

For some operations wasm-ffi needs to be able to allocate memory. It needs to coordinate this with your .wasm code so it doesn't mess your memory up.

If you do any of these...

  • pass a string from JS → WebAssembly (i.e., use a string as an argument)
  • create a struct instance using JS
  • create anything in JavaScript that you want to pass to WebAssebmly

you need two exported functions from your WebAssembly module:

  • allocate(size) → pointer
  • deallocate(pointer, size /* optional */)

(in rust)
(malloc/free in C)

wasm-ffi also expects to find WebAssembly memory at instance.exports.memory or imports.env.memory. If your module imports WebAssembly memory from a different namespace, you'll need to add it as an option in new Wrapper().


Memory management ♻️

WebAssembly has no garbage collection so you need to clean up after yourself. If you allocate anything in JS you need to free it when you're done. Here are two things you should know:

Strings & ArrayBuffers

wasm-ffi does some memory management for you. In wrapped functions, strings and arrays are allocated before the function call and automatically deallocated afterwards:

const library = new Wrapper({
  passString: [null, ['string']], // fn accepts pointer to a string
  passArray: [null, ['array']],   // fn accepts pointer to an array
});
// ...
 
// string is written to wasm memory...
library.passString('from JS');
// and then freed
 
// array is written to wasm memory...
library.passArray(new Uint8Array([1, 2, 3]));
// and then freed

If you want an ArrayBuffer or string to remain in wasm memory and not get automatically freed, you need to explicitly allocate / free it:

const library = new Wrapper({
  passString: [null, ['number']], // manual pointer to a string
  passArray: [null, ['number']],  // manual pointer to an array
});
// ...
 
// write string directly and pass pointer
const strPtr = library.utils.writeString('from JS');
library.passString(strPtr);
 
// write array directly and pass pointer
const arrPtr = library.utils.writeArray(new Uint8Array([1, 2, 3]));
library.passArray(arrPtr);
 
// deallocate
library.utils.free(strPtr);
library.utils.free(arrPtr);

Structs & Pointers

Struct and pointers created from JS are allocated and written when they are first used by a WebAssembly function.

// make a new struct type: Foo
const Foo = new Struct({
  bar: 'uint32',
});
 
// library with the function `useFoo`, which takes a `Foo` pointer:
const library = new Wrapper({
  useFoo: [null, [Foo]],
});
// ...
 
// create new Foo instance: (not yet allocated into wasm memory)
const foo = new Foo({
  bar: 1,
});
 
// the first time `foo` gets used in a function it will be allocated:
library.useFoo(foo);
 
// `foo.ref()` is now an address in wasm memory.
// This call uses that same reference:
library.useFoo(foo);
 
// free `foo` using JS:
foo.free();

Structs can also be directly allocated:

const foo = new Foo({ bar: 1 });
const ptr = library.utils.writeStruct(foo);
 
foo.free();

Documentation

new Wrapper(functions [, options])

  • functions - <object> Type signatures for WebAssembly functions you want to wrap
  • options - <object>
    • options.memory - <WebAssembly.Memory> (if not at instance.exports.memory)

Functions signatures take the format:

functionName: [returnType, [...argTypes]]

Valid argument types include:

  • 'number', 'string', 'array', 'bool' / 'boolean'
  • Struct instances
  • types.pointer(x)'s

Remember that WebAssembly only uses numbers, so strings and structs here are actually pointers to the data. Your WebAssembly functions should accept and return pointers.

const Foo = new Struct({
  bar: 'bool'
});
 
const library = new Wrapper({
  // library.getLength('taco') === 4
  getLength: ['number', ['string']],
  // library.matchStrings('queso', 'tortilla') === false
  matchStrings: ['bool', ['string', 'string']],
  // library.isFooBar(new Foo({ bar: true })) === true
  isFooBar: ['bool', [Foo]],
})

If there is no return type, use null, 'void', or exclude entirely:

const library = new Wrapper({
  noReturn: [null, ['string']],
  nothing: ['void', ['number']],
  nada: [],
})

Also, you can substitue a number type string (like 'uint32') for 'number' if you want it to more closely match your interface. This is purely cosmetic though--there aren't any checks to see if your inputs are in bounds.

.imports(fn|obj)

Add imports to your module. These are JS values that you can access from WebAssembly. You can provide a plain object or you can wrap functions like you would in the Wrapper constructor. This has to be called before fetching your wasm module.

Plain object:

library.imports({
  // the 'env' namespace is typically used by wasm compilers:
  env: {
    do_alert() {
      alert('called from webassembly!');
    }
  },
});
 

Use a callback to wrap imported functions. If they don't have a return type you can use the format:
wrap(type1, type2, ..., fn)

library.imports((wrap) => ({
  env: {
    // `alert_string` gets called with a string ptr:
    alert_string: wrap('string', (str) => {
      alert('WebAssembly just said: ' + str);
    }),
 
    // `log_version` gets a Foo ptr & a string ptr:
    log_version: wrap(Foo, 'string', (foo, name) => {
      console.log(foo.version, name)
    }),
 
    // not wrapped
    normal() {
      console.log('just normal');
    }
  },
}));

If your imported function has a return type you can wrap is using the same format as a Wrapper definitions:
wrap([return, [...types]], fn)

library.imports((wrap) => ({
  env: {
    // `get_value` is called with a string ptr & a number
    // It does some DOM stuff and returns another string ptr
    get_value: wrap(['string', ['string', 'number']], (id, n) => {
      return document.getElementById(id + n).value;
    }),
  },
}));

.fetch(url)

A helper method to fetch a .wasm module at a url and instantiate it.
Tries to use instantiateStreaming if supported.

library.fetch('my.wasm').then(() => {
  library.doThing();
});

.use(instance)

If you don't want to use .fetch you can instantiate the module yourself and tell your wrapped library to use it.

library.use(wasmInstance);
library.doThing();

.exports

Access to all WebAssembly instance exports, not just your wrapped functions. Same thing as instance.exports.

.utils

Some utility functions:

  • .readString(addr)string
  • .writeString(str)addr
  • .writeArray(arr)addr
  • .readStruct(addr, type)StructType
  • .writeStruct(struct)addr
  • .readPointer(addr, type)Pointer
  • .writePointer(pointer)addr
  • .allocate(value)addr
  • .free(value/addr)

cwrap(instance, fnName, returnType, argTypes)

Wraps a single function in a WebAssembly.Instance. Just like the emscripten cwrap. An alternative to using Wrapper.

const doStuff = cwrap(wasmInstance, 'doStuff', 'number', ['string', 'bool']);
const value = doStuff('one', true);

ccall(instance, fnName, returnType, argTypes, ...args)

Wraps and calls a single function in a WebAssembly.Instance. Just like the emscripten ccall. An alternative to using Wrapper.

const value = ccall(wasmInstance, 'doStuff', 'number', ['string', 'bool'], 'one', true);

new Struct(fields [, options])

Defines a new struct type and returns a new constructor.

Constructor can be used to create struct instances, or it can be used as an argument type / return type for functions. Struct fields should be specified in order. Structs can be composed of any of the primitive types like 'uint8', or they can be composed of other sub-structs, pointers, or arrays of types. Struct fields will be padded according to the usual C rules.

Struct instances are automatically allocated/written to memory the first time they are used in a WebAssembly function. They can also be explicitly allocated. If you create struct instance from JS (not just receive it some WebAssembly call), remember to free it somehow or you will leak!

// define a new struct type:
const Point = new Struct({
  x: 'uint32',
  y: 'uint32',
});

Structs can be composed of other structs and can include arrays of types.

// define another Struct type (with arrays)
const Coords = new Struct({
  points: [Point, 4], // an array of 4 `Point` types
});

new StructType(values)

Creates a new instance from that struct type

const p1 = new Point({
  x: 1,
  y: 2,
});
 
// read values
p1.x === 1;
p1.y === 2;
 
const library = new Wrapper({
  manipulate: [Point],
});
 
library.manipulate(p1);
 
// read changed values
p1.x === 5;
p1.y === 10;
 
// write values in wasm memory
p1.x = 50;
p1.y = 100;
  • .ref() - Returns the address of the struct in wasm memory
  • .free() - Free the struct from wasm memory, deallocating it. Be careful! ⚠️

types

Types have string aliases to make things more concise, so instead of using types.uint32 you can just put the string 'uint32' or 'u32'.

types aliases
types.uint8 uint8, u8, char, uchar
types.uint16 uint16, u16, ushort
types.uint32 uint32, u32, uint, ulong, size_t, usize
types.uint64* uint64, u64, ulonglong
types.int8 int8, i8, schar
types.int16 int16, i16, short
types.int32 int32, i32, int, long
types.int64* int64, i64, longlong
types.float f32
types.double f64
types.bool boolean

* note: JS doesn't have 64 bit integers. These types will return a 8 byte DataView. You can use decide if you want to down cast it to a u32 or use some other BigInt solution.

types.string

A pointer to a null-terminated string.
string fields in structs will hold StringPointer objects.

⚠️ Because strings are pointers you need to remember to free them!

const Foo = new Struct({
  str: 'string',
});
 
const foo = library.getStruct();
 
foo.str instanceof StringPointer === true;
foo.str.ref() === 0x45522; // some address in memory
 
// dereference the pointer to read string
foo.str.deref() === 'Hello!';
// or coerce to a string:
String(foo.str) === 'Hello!';
foo.str == 'Hello!';
 
// to change a struct field string you need to create and allocate a StringPointer:
const str = new StringPointer('Set to something else');
library.utils.allocate(str);
 
foo.str = str;

types.pointer(type)

A type that represents a pointer to another type. Note: pointers in WebAssembly are uint32's.

const HasPointer = new Struct({
  ptr: type.pointer('uint8'),
  normal: 'uint8',
});
 
const struct = library.getStruct();
 
// dereference to get value
struct.ptr.deref() === 3;
 
// to change an existing struct you need to create and allocate a new Pointer:
const p = new Pointer('uint8', 42);
library.utils.allocate(p);
 
struct.ptr = p;
 
// if your are creating a new struct it will allocate it for you:
const other = new HasPointer({
  ptr: new Pointer('uint8', 111),
  normal: 222,
});

new CustomType(size [, options])

  • size - <integer> Size in bytes
  • options - <object>
    • options.alignment - <integer> defaults to size
    • options.read - <function(DataView)> returns a DataView of lengths size by default
    • options.write - <function(DataView, value)> write value to DataView

Types with customizable sizes, alignments, and read/write methods.
Could be useful if you only care about part of a struct, and not the other fields.

// hack to down cast u64 -> u32
const Uint64 = new CustomType(8, {
  read(view) {
    return view.getUint32(0, true);
  },
 
  write(view, value) {
    return view.setUint32(0, value, true);
  },
});
 
const Has64 = new Struct({
  num: Uint64,
});
 
// struct instanceof Has64
const struct = library.getStruct();
 
// struct.ptr instanceof Pointer
struct.num === 1;
struct.num = 99;

new Pointer(type [, value])

Creates a new pointer to type, with optional value. If you don't give it an initial value you can set it later with .set().

const HasPointer = new Struct({
  ptr: type.pointer('uint32'),
});
 
const struct = new HasPointer({
  ptr: new Pointer('uint32', 42),
});
 
// struct and struct.ptr both get allocated here:
library.passStruct(struct);
// explicitly allocate a pointer:
const pointer = new Pointer('uint32', 42);
library.utils.allocate(pointer);
  • .ref() - returns pointer's address
  • .deref() - reads the data
  • .set(value) - sets pointer value
  • .free() - free the data from wasm memory

new StringPointer(str)

Used to write strings to wasm memory. Like a Pointer, but specifically for strings. StringPointerss are automatically allocated and written when they are used in a WebAssembly function. Can also be manually allocated.

// make a new StringPointer and allocate/write it to wasm memory
const str = new StringPointer('woo woo');
library.utils.allocate(str);
  • .ref() - returns string pointer's wasm address
  • .deref() - reads the string at pointer address
  • .free() - free string from wasm memory

AssemblyScript Types

Support types specifically for AssemblyScript modules.

assemblyscript.array(type)

Read the underlying array data by accessing the .values field.

const library = new Wrapper({
  return_array: [assemblyscript.array('u16')],
});
 
const arr = library.return_array();
arr.values === [1, 2, 3];
arr.map(x => 2 * x) === [2, 4, 6];

Create a new array of type with values:

const library = new Wrapper({
  give_array: [null, [assemblyscript.array('string')]],
});
 
const arr = new assemblyscript.array('string', ['a', 'b']);
library.give_array(arr);
 
// *or*
 
library.give_array(['a', 'b']);

Rust Types ⚠️

Experimental & implementation dependent types based on this cheat sheet of container types.

Be warned, they may not work in future versions of Rust!

These are sub classes of StuctType with pre-defined fields and maybe some methods. Remember to use #[repr(C)] on enums. Use rustc & -Z print-type-sizes if you need to debug discriminant/size/alignment issues.

rust.string

A Rust String container type. Basically: struct { ptr, cap, len } Read the underlying string data by accessing the .value field or coercing to a string.

const library = new Wrapper({
  return_rust_string: [rust.string],
});
 
const str = library.return_rust_string();
 
str.value === 'Hello from Rust';
String(str) === 'Hello from Rust';
str == 'Hello from Rust';

Creating a new string:

const library = new Wrapper({
  give_rust_string: [null, [rust.string]],
});
 
const str = new rust.string("Hello from JS");
library.give_rust_string(str);
 
// *or*
 
library.give_rust_string("Hello from JS");

rust.str

A Rust str container type. Like String, but without cap: struct { ptr, len } Read the underlying string data by accessing the .value field or coercing to a string.

const Foo = new Struct({
  str: rust.str,
});
 
const library = new Wrapper({
  get_foo: [Foo],
});
 
const foo = library.get_foo();
 
foo.str.value === 'Hello from Rust';
String(foo.str) === 'Hello from Rust';
foo.str == 'Hello from Rust';

Creating a new rust str.

const Foo = new Struct({
  str: rust.str,
});
 
const library = new Wrapper({
  give_foo: [null, [Foo]],
});
 
const foo = new Foo({
  str: new rust.str('Hello from JS')
  // *or*
  str: 'Hello from JS'
});
 
library.give_foo(foo);

rust.vector(type)

A Rust Vector container type. Like a String: struct { ptr, cap, len }, but based on a given type Read the underlying array data by accessing the .values field.

const library = new Wrapper({
  return_rust_vector: [rust.vector('u16')],
});
 
const vec = library.return_rust_vector();
vec.values === [1, 2, 3];
vec.map(x => 2 * x) === [2, 4, 6];

Create a new vector of type with values:

const library = new Wrapper({
  give_rust_vector: [null, [rust.vector('u16')]],
});
 
const vec = new rust.vector('u16', [1, 2, 3]);
library.give_rust_vector(vec);
 
// *or*
 
library.give_rust_vector([1, 2, 3]);

rust.slice(type)

A Rust slice container type. Like a Vector but with no cap: struct { ptr, len } Read the underlying array data by accessing the .values field.

const Foo = new Struct({
  slice: rust.slice('usize'),
});
 
const library = new Wrapper({
  get_foo: [Foo],
});
 
const foo = library.get_foo();
foo.slice.values === [1, 2, 3];
foo.slice.map(x => 2 * x) === [2, 4, 6];

Create a new slice of type with values:

const Foo = new Struct({
  slice: [null, [rust.slice('usize')]],
});
 
const library = new Wrapper({
  give_foo: [Foo],
});
 
const foo = new rust.slice('usize', [1, 2, 3]);
library.give_foo(foo);
 
// *or*
library.give_foo([1, 2, 3]);

rust.tuple(...types)

const library = new Wrapper({
  return_rust_tuple: [rust.tuple('u16', 'usize')],
});
 
const tup = library.return_rust_tuple();
tup[0] === 2;
tup[1] === 288;

Create a new tuple of given types with matching values:

const library = new Wrapper({
  give_rust_tuple: [null, [rust.tuple('u16', 'usize')]],
});
 
const tup = new rust.Tuple(['u16', 'usize'], [2, 288]);
library.give_rust_tuple(tup);
 
// *or*
 
library.give_rust_tuple([2, 288]);

rust.enum(variants [, tagSize])

A Rust enum is combination of a discriminant tag and a type. If you use #[repr(C)] on your enum the discriminant will be 4 bytes. Without #[repr(C)] it varies. rust.enum defaults to a tagSize of 4.

Read the data by accessing the .value property.

const VersionID = rust.enum({
  One: 'u16',
  Two: rust.string,
});
 
const library = new Wrapper({
  getVersionID: [VersionID],
});
 
const version = library.getVersionID();
console.log(version.value);

You can also create new enums from your definition like you would a struct:

const library = new Wrapper({
  giveVersionID: [null, [VersionID]],
});
 
const version = new VersionID({ One: 123 });
// *or*
const version = new VersionID({ Two: '123' });
 
library.giveVersionID(version);

RustEnums have two methods:

  • .is(type)bool
  • .match(arms)value
if (version.is('One')) {
  // version.value is a 'u16'
}
 
// kinda-sorta like matching:
// match arms can be functions or simply a value:
const value = version.match({
  One(number) {
    return String(number);
  },
 
  Two(string) {
    return string.value;
  }
 
  _: 'Bad version number',
});

rust.option(type [, isNonNullable[, tagSize]])

A Rust Option is like an enum, but with only two variants: some type, or none. If the given type is non-nullable, an optimization is applied and the discriminant tag is left out completely (a value of 0 means none in this case).

If your type is non-nullable, like a pointer, set isNonNullable to true.

const Foo = rust.enum({
  opt: rust.option('usize'),
});
 
const library = new Wrapper({
  get_foo: [Foo],
});
 
const foo.opt = library.get_foo();
console.log(foo.opt.value);

RustOption has methods that work like you would expect:

  • .isSome()
  • .isNone()
  • .expect(msg) - throws an error with msg if None
  • .unwrap() - throws an error if None
  • .unwrapOr(default)
  • .unwrapOrElse(fn)  

Create a new option of type with a value. Value can be an actual value or it can be undefined for none. You can use rust.some(type, value) and rust.none(type) for this purpose too.

const Foo = rust.enum({
  opt: rust.option('usize'),
});
 
const library = new Wrapper({
  give_foo: [Foo],
});
 
const foo = new Foo({
  opt: new rust.option('usize', 123);
});
 
// new rust.option('usize', 123) === new rust.some('usize', 123)
// new rust.option('usize') === new rust.none('usize')
 
library.give_foo(foo);

Tests

In the /tests directory.
Try em in your browser of choice, or run through node with:

npm run test

License

MIT

Package Sidebar

Install

npm i wasm-ffi

Weekly Downloads

7

Version

0.3.0

License

MIT

Unpacked Size

37.7 MB

Total Files

270

Last publish

Collaborators

  • demille