React + WebAssembly: Compiling a C Library

High-level strategy

Project structure

git clone https://github.com/webmproject/libwebp.git
npx create-react-app wasm-react-webp

Let’s write some C

#include "emscripten.h"#include "src/webp/encode.h"EMSCRIPTEN_KEEPALIVEint version() {  return WebPGetEncoderVersion();}
#include "src/webp/encode.h"

Compiling to WebAssembly

webp.mjs: webp.cemcc --no-entry \-I libwebp \webp.c \libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c -o webp.mjs  \-s ENVIRONMENT='web'  \-s SINGLE_FILE=1  \-s EXPORT_NAME='createModule'  \-s USE_ES6_IMPORT_META=0  \-s EXPORTED_FUNCTIONS='["_version", "_malloc", "_free"]'  \-s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]'  \-O3
make
import React, { useState, useEffect } from "react";import createModule from "./webp.mjs";function App() {  const [version, setVersion] = useState();  useEffect(    () => {    createModule().then((Module) => {      setVersion(() => Module.cwrap("version", "number", []));    });  }, []);  if (!version) {    return "Loading webassembly...";  }  return (    <div className="App">      <p>version: {version()}</p>    </div>  );}export default App;
"ignorePatterns": [  "src/webp.mjs"]

Encoding an image

EMSCRIPTEN_KEEPALIVEuint8_t* create_buffer(int width, int height) {  return malloc(width * height * 4 * sizeof(uint8_t));}EMSCRIPTEN_KEEPALIVEvoid destroy_buffer(uint8_t* p) {  free(p);}int result[2];EMSCRIPTEN_KEEPALIVEvoid encode(uint8_t* img_in, int width, int height, float quality) {  uint8_t* img_out;  size_t size;  size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);  result[0] = (int)img_out;  result[1] = size;}EMSCRIPTEN_KEEPALIVEvoid free_result(uint8_t* result) {  WebPFree(result);}EMSCRIPTEN_KEEPALIVEint get_result_pointer() {  return result[0];}EMSCRIPTEN_KEEPALIVEint get_result_size() {  return result[1];}

Update the Makefile

webp.mjs: webp.cemcc --no-entry \-I libwebp \webp.c \libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c -o webp.mjs  \-s ENVIRONMENT='web'  \-s SINGLE_FILE=1  \-s EXPORT_NAME='createModule'  \-s USE_ES6_IMPORT_META=0  \-s EXPORTED_FUNCTIONS='["_version", "_create_buffer", "_destroy_buffer", "_encode", "_free_result", "_get_result_pointer", "_get_result_size", "_malloc", "_free"]'  \-s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]'  \-O3
make

Home stretch

import React, { useState, useEffect, useRef } from "react";import createModule from "./webp.mjs";function App() {  const [assemblyApi, setAssemblyApi] = useState();  const imgRef = useRef();  const loadImage = async (src) => {    // Load image    const imgBlob = await fetch(src).then(resp => resp.blob());    const img = await createImageBitmap(imgBlob);    // Make canvas same size as image    const canvas = document.createElement('canvas');    canvas.width = img.width;    canvas.height = img.height;    // Draw image onto canvas    const ctx = canvas.getContext('2d');    ctx.drawImage(img, 0, 0);    return ctx.getImageData(0, 0, img.width, img.height);  }  useEffect(    () => {    createModule().then(async (Module) => {    const api = {      version: Module.cwrap("version", "number", []),      create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),      destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),      encode: Module.cwrap("encode", "", ["number", "number", "number", "number"]),      get_result_pointer: Module.cwrap("get_result_pointer", "number", []),      get_result_size: Module.cwrap("get_result_size", "number", []),      free_result: Module.cwrap("free_result", "", ["number"])    }    setAssemblyApi(() => api);    const image = await loadImage(process.env.PUBLIC_URL + '/logo512.png');    const p = api.create_buffer(image.width, image.height);      Module.HEAP8.set(image.data, p);    api.encode(p, image.width, image.height, 100);    const resultPointer = api.get_result_pointer();    const resultSize = api.get_result_size();    const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);    const result = new Uint8Array(resultView);    api.free_result(resultPointer);    api.destroy_buffer(p);    const blob = new Blob([result], {type: 'image/webp'});    const blobURL = URL.createObjectURL(blob);      imgRef.current.src = blobURL;    });  }, []);  if (!assemblyApi) {    return "Loading webassembly...";  }  return (    <div className="App">      <p>version: {assemblyApi.version()}</p>      <p>Our webP image below:</p>      <img ref={imgRef} />    </div>  );}export default App;
Module.HEAP8.set(image.data, p);
api.encode(p, image.width, image.height, 100);
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);

Fireworks

--

--

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