user8866053
user8866053

Reputation:

Export a structure from C to wasm?

I have a header file with the following declarations:

typedef struct {
        double *vals;
        int len;
} List;

typedef struct {
        List *list;
        int sample;
} MarkedList;

I'm compiling a C file that includes these definitions and a function that takes a const *MarkedList with:

clang --target=wasm32 -nostdlib -Wl,--export-all -Wl,--no-entry src/filename.c -o target/res/filename.wasm

I'm loading that file with:

WebAssembly.instantiateStreaming(fetch('res/mean.wasm'), {})
.then(results => {
    console.log(results)
});

The console then shows me a wasm module with instance.exports containing the function I defined, but not the structures. How can I access the structures from my javascript results object?

Upvotes: 2

Views: 2044

Answers (2)

konsumer
konsumer

Reputation: 3493

This is a kinda old question, but I have been using what I call "magic classes" for plain (no emscripten) wasm, made with wasi-sdk (which includes a malloc/free) but there are a few malloc implementations for wasm, like "walloc", which is small & standalone.

Even with emscripten, it can be nice for exposing a JS interface that needs to pass structs to C functions. I used this style on raylib-wasm to expose the raylib API to web JS, using a familiar syntax.

The essential trick is to set all the fields to mess with the memory directly, using addresses.

Say we have a struct like this:

typedef Color {
  unsigned char r;
  unsigned char g;
  unsigned char b;
  unsigned char a;
}
// do your WASM init here, however you do that
// you need to expose malloc & memory, and should probably expose free
const api = {}
const { instance } = await WebAssembly.instantiate(wasmBytes, api)
const mod = instance.exports
const view = new DataView(mod.memory.buffer)

class Color {
  constructor(init={}, address) {
    this._size = 4 // bytes
    this._address = address || mod.malloc(this._size)
    for (const k of Object.keys(init)) {
      this[k] = init[k]
    }
  }
  get r() {
    return view.getUint8(this._address)
  }
  get g() {
    return view.getUint8(this._address + 1)
  }
  get b() {
    return view.getUint8(this._address + 2)
  }
  get a() {
    return view.getUint8(this._address + 3)
  }
  set r(v) {
    view.setUint8(this._address, v)
  }
  set g(v) {
    view.setUint8(this._address + 1, v)
  }
  set b(v) {
    view.setUint8(this._address + 2, v)
  }
  set a(v) {
    view.setUint8(this._address + 3, v)
  }
}

Now, you can do stuff like this:

// these do a fresh allocation
const BLACK = new Color()
const WHITE = new Color({ r: 255, g: 255, b: 255, a: 255 })

// this makes a color-object from an address
const c = new Color({}, address)

// you can get/set them just like a plain js object
console.log(BLACK.a)
BLACK.a = 255

// you can use them with your wasm by pulling _address
mod.clear_screen(BLACK._address)

// here is a wrapper, for nicer user-function
export function clear_screen(color) {
  mod.clear_screen(color._address)
}

// free the mem when you are done
mod.free(BLACK._address)

I wrote a library that does similar, for simple-to-use structs like this, here is an example:

import memhelpers from 'cmem_helpers'
const wasmBytes = '...'

const env = {
  // add your imported API here
}

const mod = (await WebAssembly.instantiate(wasmBytes, { env })).instance.exports

const { structClass, setString, getString } = memhelpers(mod.memory.buffer, mod.malloc)

const Color = structClass({
  r: 'Uint8',
  g: 'Uint8',
  b: 'Uint8',
  a: 'Uint8'
})

// usage is same as above
const BLACK = new Color({r: 0, g: 0, b: 0, a: 255})
mod.clear_screen(BLACK._address)

Additionally, setString/getString are useful lil utils for messing with C-strings over wasm, in a similar way, and not having to encode/decode and worry too much about string-length.

Upvotes: 1

user8866053
user8866053

Reputation:

I'v changed a lot of code in figuring out how to address this, but I'll try to write out a summary of my overall solutions as best I can. Firstly, I switched to emcc over clang so that standard library C features (like malloc) work in wasm (see How to implement "malloc" in Wasm). While it's a very heavy-handed solution, I believe LTO can cut down on some unused features.

Secondly, I've done my best to encapsulate the API of the List and MarkedList structs such that only useful functions that communicate with solely number types (including pointers) are exported to the JS, in compliance with the WebAssembly standard. So a List *malloc_list(void) and a void push_list(List *l, double v) make sense in this context, but anything with an API depending on the in-memory representation of a struct (for instance, to access an indivdual field) would be impossible.

So while there is no widely-applicable way to export a struct, there are workarounds that keep as much as possible in the C and as little as possible in the JS, and I was able to accomplish what I needed from the List and MarkedList structs.

Upvotes: 2

Related Questions