An Example of Compling C into WebAssembly

An Example of Compling C into WebAssembly

·

4 min read

Install Emscripten

Emscripten is a compiler tool to compile C and C++ code into WebAssembly. We could install Emscripten by following its installation guide, but I recommend using its docker image to avoid some installation hassles. All you need to do now is to install Docker environment on your computer.

Prepare C Code

In order to use the C library, we must first define some interfaces for calls between js and C code. So let's we download C library code first.

git clone https://github.com/webmproject/libwebp

Then we need to write our webp.c code which contains all apis we need.

#include "emscripten.h"
#include <stdlib.h>

// include this header to use webp encoder function
#include "src/webp/encode.h"

EMSCRIPTEN_KEEPALIVE
int version()
{
  return WebPGetEncoderVersion();
}

// Allocate memory for image data based on width and height
EMSCRIPTEN_KEEPALIVE
uint8_t *create_buffer(int width, int height)
{
  return malloc(width * height * 4 * sizeof(uint8_t));
}

// free this memory
EMSCRIPTEN_KEEPALIVE
void destroy_buffer(uint8_t *p)
{
  free(p);
}

int result[2];

// this is the function to encode png into webp
EMSCRIPTEN_KEEPALIVE
void 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);

  // we can see here, the output image data saved in result variable
  result[0] = (int)img_out;
  result[1] = size;
}

// free memory
EMSCRIPTEN_KEEPALIVE
void free_result(uint8_t *result)
{
  WebPFree(result);
}

// get pointer for resule image data
EMSCRIPTEN_KEEPALIVE
int get_result_pointer()
{
  return result[0];
}

// get pointer for result image size
EMSCRIPTEN_KEEPALIVE
int get_result_size()
{
  return result[1];
}

The api is simple, create_buffer is used to allocate memory for input image data, encode is used to convert png into webp format, get_result_pointer and get_result_size are used to retrieve result data.

As you can see, we should use macro EMSCRIPTEN_KEEPALIVE to export functions.

Compile C into WebAssembly

Now we have docker environment, C library code and C api code, it's time to compile C code into WebAssembly. The compiling command is as follows.

docker run \
  --rm \
  -v $(pwd):/src \
  emscripten/emsdk \
  emcc -O1 \
  -s WASM=1 \
  -s ALLOW_MEMORY_GROWTH=1 \
  -s EXPORTED_RUNTIME_METHODS='["cwrap"]' \
  -I libwebp \
  webp.c libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c \
  -o webp.js

Next, let's look at each option one by one to see what it means.

  • docker run: to run a docker image
  • --rm: remove container after running
  • -v $(pwd):/src: bind current directory from the host in the container into the /srcdirectory
  • emscripten/emsdk: get the latest tag of this container
  • emcc: begin to use emscripten to compile
  • -O1: stands for simple optimizations
  • -s WASM=1: to output wasm file
  • -s ALLOW_MEMORY_GROWTH=1: allows the total amount of memory used to change depending on the demands of the application
  • -s EXPORTED_RUNTIME_METHODS='["cwrap"]': to export cwrap function
  • -I libwebp: to specify C header files
  • webp.c libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c: this is C code to compile
  • -o webp.js: to output webp.js file

If everything is ok, we should see two files generated: webp.js and webp.wasm.

Use WebAssembly

Now it's time that we call the api defined above to do the real job.

The process is simple.

  • load webp.js file, in which it will try to load webp.wasm file
  • wrap the C apis
  • load png image
  • allocate memory and set image data in it
  • convert image and get the result
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <script src="./webp.js"></script>
  <script>
    let api;
    Module.onRuntimeInitialized = async _ => {

      // wrap c functions
      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",]),
        free_result: Module.cwrap("free_result", "", ["number"]),
        get_result_pointer: Module.cwrap("get_result_pointer", "number", []),
        get_result_size: Module.cwrap("get_result_size", "number", []),
      };

      // test version api
      console.log(api.version());

      (async () => {

        // load png image
        const image = await loadImage('./image.png');

        // allocate memory
        const p = api.create_buffer(image.width, image.height);

        // set image data into memory
        Module.HEAP8.set(image.data, p);

        // convert
        api.encode(p, image.width, image.height, 100);

        // get result
        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);

        // free memory
        api.free_result(resultPointer);
        api.destroy_buffer(p);

        // show result
        const blob = new Blob([result], { type: 'image/webp' });
        const blobURL = URL.createObjectURL(blob);
        const img = document.createElement('img');
        img.src = blobURL;
        document.body.appendChild(img)
      })()
    };

    async function loadImage(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);
    }

  </script>
</body>

</html>

Now open the html file and we can see image file size is reduced from to 323KB to 201KB.

133445017-fab4b8ae-07f8-421d-a762-9328bc56cd24.png