React + WebAssembly: Compiling a C Library
Hey all, if you’ve gone through the previous tutorial on Getting started with React+WebAssembly or are already familiar with the basics of React/WebAssembly and want to learn how to compile a real-world C library into WebAssembly and use it in the browser, then this is the tutorial for you!
High-level strategy
Our approach is going to be pretty much identical to the previous tutorial:
- Create a file with the code we want to export from C
- Create and run a Makefile that converts said code into WebAssembly
- On the React side, read and use the generated WebAssembly code
The C Library we’ll be using in this example is the one for WebP. WebP is a modern image format developed largely by Google that generally creates smaller images than their PNG alternatives.
The library can be found in this github repo. Take a moment to get acquainted with it. Take a special look at the src folder. We’ll be using functions from there.
Project structure
Here is how I structured my project:
So to get this set up,
- create a folder webp-app, go to that folder, and run
git clone https://github.com/webmproject/libwebp.git
2. create a Makefile file
3. create a webp.c file
4. Create a new react project by running
npx create-react-app wasm-react-webp
If you followed along in the previous tutorial, these steps should all be very familiar. If you were successful, here’s a high five, and let’s move on!
Let’s write some C
Rather than modifying the original webp library, we can create a new C file that simply calls our C functions. Open up the webp.c file we created earlier and copy-paste the following:
#include "emscripten.h"#include "src/webp/encode.h"EMSCRIPTEN_KEEPALIVEint version() { return WebPGetEncoderVersion();}
This new function, version(), will go to the original webp function, WebPGetEncoderVersion(), and pass along its response. This simple function will help us verify our C-to-React pipeline. For non-C developers, notice the following line:
#include "src/webp/encode.h"
This is similar to “import” in React in that once you call #include …, you can invoke the functions in that file.
Compiling to WebAssembly
Let’s copy-paste the following to the Makefile we created earlier:
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
This is pretty much identical to the Makefile we made in the previous tutorial. The only difference is we added an -I flag — for declaring input file paths — with three files:
- libwebp: this helps the compiler find all the header files,
- webp.c: this is our custom file to export the version function.
- libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c: this makes all the C files in src/ accessible to us so we can call the functions as needed.
With that said, in your terminal, run:
make
Once that has completed running, you should see a new file: webp.mjs. Move that file to wasm-react-app/src.
Open up your App.js file and copy-paste the following:
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;
Just as in the previous tutorial, we created a state hook for holding our WebAssembly function.
Finally, don’t forget to add the following to your eslintConfig settings in package.json:
"ignorePatterns": [ "src/webp.mjs"]
At this point, you should be able to run the server and see the webP version properly display in the browser. If you want to double-check your work up to this point, here’s the source code:
Encoding an image
We are now going to go a step further and encode an image using the webP library.
As per usual, we’ll start with the webp.c file. Append the following to that file
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];}
There’s a lot to unpack here, so let’s start from the beginning.
create_buffer: this function both allocates a place in memory for us to be able to place our webP image as well as returns us the starting point of that memory location so that we can easily access it when we need to place our image.
destroy_buffer: This is to free up the memory once we’re done using it.
encode: After collecting the original image, height, width, and image quality preference via inputs, the image is encoded into webP. The returned value is the encoded image’s size. We store the new image at index-0 and the size at index-1 of the result array.
free_result: We free up any used space for a given image.
get_result_pointer: The pointer to the webP encoded image.
get_result_size: The encoded image’s size.
Update the Makefile
Now let’s go ahead and update our Makefile to include our new functions. It should look something like the following:
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
Go ahead an run the Makefile in the terminal:
make
Home stretch
We’re at the home stretch, we just need to make use of these new functions now on the React side. Replace your App.js with the following:
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;
Let’s unpack what we’ve done.
- We’ve removed the version state hook and replaced it with an assemblyApi state hook. This is because since we now have a bunch of webAssembly functions, we need an easy way to access any of them.
- We’ve added an imgRef to the component. This is our DOM target where we will render our final encoded image.
- loadImage is the function that will take our image path and extract not just its image data, but also the width and height. We need those three pieces for our encoding process.
- After calling create_buffer, this line
Module.HEAP8.set(image.data, p);
puts the image’s data into the buffer.
5. Let’s look at this line next:
api.encode(p, image.width, image.height, 100);
Per the webP API documentation, the image quality is a number between 0 and 100. There’s no reason not to go for the best, so let’s do that. Or not! The choice is yours.
6. The next interesting line is:
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
Module.HEAP8.buffer is the buffer we created and then used earlier. We pass along the pointer to the image as well as the the image size. Since we used the uint8_t data type for storing/handling our image on the C side, we are now telling the Javascript side to do the same.
6. We now pass this Uint8Array as the parameter to our Blob. We can now set the imgRef’s src to our newly created Blob.
Fireworks
You should now see the React icon that comes with React apps, except this time it’s encoded in webP.
Not sure if anything actually happened? Go to your Network tab in your console and check out the blob. Under “Type” it reads “webp”. Pretty cool, right!
Unrelated PSA: Looking for a new high paying software development job? Send me your resume to alexleondeveloper@gmail.com and I’ll get back to you!
If you want to check your work on any part of this tutorial, feel free to check out the source code here:
Best of luck with your WebAssembly journey!