WebAssembly, C, Emscripten and React

The following are things I learned making the Gifsicle library callable from React with .wasm files.

A working example can be cloned here.

Setup

Enter or clone the project directory of your C library and run create-react-app. You should have a directory structure similar to this.

C_PROJECT/ 
. src/
. C_PROJECT-react/

Building the library

Simply run emconfigure and emmake in front of whatever command builds for native, and if you’re lucky it will build without issue. You’ll want to set MY_CONFIGURE_SCRIPT, PROJECT_LOWER, PROJECT_PASCAL and the source to their respective values if you plan on building something which isn’t Gifsicle.

config.sh

#!/usr/bin/env bashMY_CONFIGURE_SCRIPT=./configure --disable-gifviewsudo docker run — rm -v $(pwd):/src trzeci/emscripten emconfigure $MY_CONFIGURE_SCRIPT

build.sh

#!/usr/bin/env bash
PROJECT_LOWER=gifsicle
PROJECT_PASCAL=Gifsicle
HTML=$PROJECT_LOWER-react/src/$PROJECT_LOWER.html
JS=$PROJECT_LOWER-react/src/$PROJECT_LOWER.js
WASM=$PROJECT_LOWER-react/src/$PROJECT_LOWER.wasm
WASM_PUBLIC=$PROJECT_LOWER-react/public/$PROJECT_LOWER.wasm
WASM_FILENAME=$PROJECT_LOWER.wasm
WASM_LOOKUP='wasmBinaryFile = locateFile'
sudo docker run --rm -v $(pwd):/src trzeci/emscripten emmake make
sudo docker run --rm -v $(pwd):/src trzeci/emscripten emcc \
src/clp.o \
src/fmalloc.o \
src/giffunc.o \
src/gifread.o \
src/gifunopt.o \
src/merge.o \
src/optimize.o \
src/quantize.o \
src/support.o \
src/xform.o \
src/gifsicle.o \
src/gifwrite.o \
# This will make the generated javascript file export a function which you can call at will
-s MODULARIZE=1 \
# This is the name of your export
-s EXPORT_NAME=$PROJECT_PASCAL \
# Your library may have some undefined symbols in it, mine did so I'll ignore them for now
-s ERROR_ON_UNDEFINED_SYMBOLS=0 \
# If you specify the output as html, emscripten will generate the .wasm, .js and .html files ready for use
-o ${HTML} \
# The .wasm will need to be put in the public directory, as create-react-app will not bundle it automatically
cp ${WASM} ${WASM_PUBLIC}
# disable eslint on the generated javascript
sed -i.old '1s;^;\/* eslint-disable *\/;' ${JS}
# Replace the relative path with an absolute one, necessary to access public files
sed -i.old "s|$WASM_FILENAME|/$WASM_FILENAME|" ${JS}
# The generated javascript will try to resolve the path relative to the website directory. Comment out this line
sed -i.old "s|$WASM_LOOKUP|// $WASM_LOOKUP|" ${JS}

Use in App.js

The simplest possible example is as follows

App.js
import React, {Component} from 'react';
import Gifsicle from './gifsicle.js';
Gifsicle();
...

Your program will run its main method, and a possible error out because you’ve given it no input, check the developer console to see the results.

The Module object, StdIn, and StdOut

Emscripten has provided the Module object to interface nicely with .wasm.

// stdin is a function which produces characters until it is finished.  Then it produces null
const stdin = function writeToStdIn(buf) {
return function() {
if (!buf.length) {
return null;
}
const c = buf[0];
buf = buf.slice(1);
return c;
};
};
// stdout is a function which accepts characters
const stdout = function (char) {
console.log(char);
};
// so is stderr
const stderr = function (char) {
console.warn(char);
};
// Here we will send bytes from this image to an instance Gifsicle
fetch('./an_example_gif.gif')
.then(async g => {
const b = new Uint8Array(await g.arrayBuffer());
const Module = {};
Module.stdout = stdout;
Module.stderr = stderr;
Module.stdin = stdin(b);
Gifsicle(Module);
});

Transferring a lot of data with MEMFS

Using the stdout and stdin were too slow to transfer images, so I looked at ffmpegjs which I knew had to deal with the same problem I did.

Emscripten has a FS api which manipulates MEMFS, its in-memory filesystem. Files included with --pre-js have access to this API. I’ve simplified the pre.js of ffmpegjs to get the bare minimum needed to transfer large chunks of data to and from javascript/wasm. I also add a callback function so I can get the results in a promise if I want.

var __ffmpegjs_utf8ToStr;
__ffmpegjs_utf8ToStr = UTF8ArrayToString;
__ffmpegjs_opts = Module;
var __ffmpegjs_return;
function __ffmpegjs_toU8(data) {
if (Array.isArray(data) || data instanceof ArrayBuffer) {
data = new Uint8Array(data);
} else if (!data) {
// `null` for empty files.
data = new Uint8Array(0);
} else if (!(data instanceof Uint8Array)) {
// Avoid unnecessary copying.
data = new Uint8Array(data.buffer);
}
return data;
}
Object.keys(__ffmpegjs_opts).forEach(function (key) {
if (key != "mounts" && key != "MEMFS" && key != "cb") {
Module[key] = __ffmpegjs_opts[key];
}
});
// Before we run, enter our work directory and create files passed in like {MEMFS: [{name: 'foo.txt', data: new Uint8Array()}]}Module["preRun"] = function () {
FS.mkdir("/work");
FS.chdir("/work");
(__ffmpegjs_opts["MEMFS"] || []).forEach(function (file) {
if (file["name"].match(/\//)) {
throw new Error("Bad file name");
}
var fd = FS.open(file["name"], "w+");
var data = __ffmpegjs_toU8(file["data"]);
FS.write(fd, data, 0, data.length);
FS.close(fd);
});
};
// After we're done, search for files in our immediate directory
Module["postRun"] = function () {
function listFiles(dir) {
var contents = FS.lookupPath(dir).node.contents;
var filenames = Object.keys(contents);
return filenames.map(function (filename) {
return contents[filename];
});
}
var inFiles = Object.create(null);
(__ffmpegjs_opts["MEMFS"] || []).forEach(function (file) {
inFiles[file.name] = null;
});
var outFiles = listFiles("/work").filter(function (file) {
return !(file.name in inFiles);
}).map(function (file) {
var data = __ffmpegjs_toU8(file.contents);
return {"name": file.name, "data": data};
});
__ffmpegjs_return = {"MEMFS": outFiles};
// Call the callback if we have one
__ffmpegjs_opts["cb"] && __ffmpegjs_opts["cb"](__ffmpegjs_return);
};

Add this to a file called pre.js and add --pre-js pre.js to your emcc call, Your code will be bundled into the output and you’ll be able to pass in the MEMFS and cb parameters when you call your object.

Photo by Lea Katharina on Unsplash

Written by

Considers any piece of software without vim bindings incomplete

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store