WebAssembly, C, Emscripten and React

Marvin Irwin
Apr 27 · 4 min read

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

Setup

Enter or clone the project directory of your C library and run create-react-app. You should have the 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.

A minimal example of this working can be found in this repo.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade