Using WebAssembly modules from C

Most often we think of WebAssembly as a compile target for languages like C, but it's also possible to convert a WebAssembly binary compiled from any language to C. And then it's the question: why would you do that?

In the WebAssembly Music project, music is generated from a WebAssembly module, written in AssemblyScript. Let's say you want to use that music in a game written in C targeting a native binary, then the best way would be to convert the music WASM module to C.

The WebAssembly Binary Toolkit (wabt) contains a handy tool called wasm2c. It's usage is straightforward. For example take the song provided from here, and click the export button in the upper right. You'll get a choice to export a Self executable WASI module (that can be played directly in a WASI enabled webassembly runtime) or a Library module that can be used from javascript, other WASM modules or C.

Select Library module and generate the WASM module, and when done you'll get a file named song.wasm which you then can convert to C:

wasm2c song.wasm -o song.c

And then you get a c source file song.c and a header file song.h. Now compiling these directly with a C compiler doesnt't do much, since there's no main entry point, so we need to write one.

For now we'll just use the cross-platform library libsoundio for playing sound when our final compiled program is executed. And so our main file will look like this:

// we'll include the song WASM module here:

#include "song.h"
#include <soundio/soundio.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

u32 samplebuffer; // a pointer to the samplebuffer in the WASM module memory
int samplebufferpos = 128; // keep track on how many frames we've rendered

/**
 * Callback from the audio system whenever audio data should be rendered
 */
static void write_callback(struct SoundIoOutStream *outstream,
        int frame_count_min, int frame_count_max)
{
    const struct SoundIoChannelLayout *layout = &outstream->layout;

    struct SoundIoChannelArea *areas;
    int frames_left = frame_count_max;
    int err;

    while (frames_left > 0) {
        int frame_count = frames_left;

        if ((err = soundio_outstream_begin_write(outstream, &areas, &frame_count))) {
            fprintf(stderr, "%s\n", soundio_strerror(err));
            exit(1);
        }

        if (!frame_count)
            break;

        for (int frame = 0; frame < frame_count; frame += 1) {
            if(samplebufferpos == 128) {
                // Call into the WebAssembly module to render Audio data
                Z_fillSampleBufferZ_vv();
                samplebufferpos = 0;
            }
            // Copy data from the WASM module sample buffer to the audio output buffer
            float left = *((float*)(Z_memory->data + samplebuffer + (samplebufferpos) *4 ));
            float right = *((float*)(Z_memory->data + samplebuffer + 128 * 4 + (samplebufferpos) *4 ));
            
            float *leftptr = (float*)(areas[0].ptr + areas[0].step * frame);
            float *rightptr = (float*)(areas[1].ptr + areas[1].step * frame);
            *leftptr = left;
            *rightptr = right;            

            samplebufferpos++;
        }
        
        if ((err = soundio_outstream_end_write(outstream))) {
            fprintf(stderr, "%s\n", soundio_strerror(err));
            exit(1);
        }

        frames_left -= frame_count;
    }
}

int main(int argc, char **argv) {
    int err;
    struct SoundIo *soundio = soundio_create();
    if (!soundio) {
        fprintf(stderr, "out of memory\n");
        return 1;
    }

    if ((err = soundio_connect(soundio))) {
        fprintf(stderr, "error connecting: %s", soundio_strerror(err));
        return 1;
    }

    soundio_flush_events(soundio);

    int default_out_device_index = soundio_default_output_device_index(soundio);
    if (default_out_device_index < 0) {
        fprintf(stderr, "no output device found");
        return 1;
    }

    struct SoundIoDevice *device = soundio_get_output_device(soundio, default_out_device_index);
    if (!device) {
        fprintf(stderr, "out of memory");
        return 1;
    }

    fprintf(stderr, "Output device: %s\n", device->name);

    init(); // Initialize the WASM module
    samplebuffer = Z_allocateSampleBufferZ_ii(128); // Allocate the sample buffer in the WASM module memory

    struct SoundIoOutStream *outstream = soundio_outstream_create(device);
    outstream->format = SoundIoFormatFloat32NE;
    outstream->write_callback = write_callback;
    outstream->sample_rate = 44100;
    
    if ((err = soundio_outstream_open(outstream))) {
        fprintf(stderr, "unable to open device: %s", soundio_strerror(err));
        return 1;
    }

    if (outstream->layout_error)
        fprintf(stderr, "unable to set channel layout: %s\n", soundio_strerror(outstream->layout_error));

    if ((err = soundio_outstream_start(outstream))) {
        fprintf(stderr, "unable to start device: %s", soundio_strerror(err));
        return 1;
    }

    for (;;)
        soundio_wait_events(soundio);

    soundio_outstream_destroy(outstream);
    soundio_device_unref(device);
    soundio_destroy(soundio);
    return 0;
}

In this main file, the audio output is initialized and configured with the same samplerate as the exported song WASM module ( 44100 frames per second ). This sound library use a callback whenever new audio data is needed, and so we call into the WASM module to render new data from this callback. The WASM module generates audio data into it's own buffer, and so we need to copy data from that buffer into the audio output buffer.

We can compile this using a c compiler like gcc:

gcc -O2 -I$WASM2C main.c song.c $WASM2C/wasm-rt-impl.c -lm -lsoundio -o song

Note that we point to $WASM2C as an include dir and we also need to link the wasm runtime. Set the $WASM2C variable to the location of wasm2c runtime headers and sources. Also note that we've linked the math library -lm which is needed for WASM math operations like e.g. floor, which you can see have been used in the exported WASM module. Finally link the sound library -lsoundio and output to our native executable file -o song.

After compiling you can execute the file:

./song

and music will be playing...

See all the files generated and used in this example in this gist.