Building WASM Filters in C++

In this tutorial we will write an Envoy filter in C++ and build it using wasme.

Creating a C++ WASM module

Refer to the installation guide for installing wasme, the WebAssembly Hub CLI.

Let’s create a new filter called cpp-filter:

wasme init cpp-filter

You’ll be asked with an interactive prompt which language platform you are building for. At time of writing, wasme includes separate bases for Istio 1.5.x and Gloo 1.3.x:

? What language do you wish to use for the filter:
  ▸ cpp
? With which platform do you wish to use the filter?:
    istio 1.5.x
  ▸ gloo 1.3.x

? What language do you wish to use for the filter:
  ▸ cpp
? With which platform do you wish to use the filter?:
  ▸ istio 1.5.x
    gloo 1.3.x
INFO[0014] extracting 5072 bytes to /Users/ilackarms/go/src/

The init command will place our base filter into the cpp-filter directory:

cd cpp-filter
tree .

├── bazel
│   └── external
│       ├── BUILD
│       ├── emscripten-toolchain.BUILD
│       └── envoy-wasm-api.BUILD
├── filter.proto
├── runtime-config.json
└── toolchain
    ├── BUILD
    ├── cc_toolchain_config.bzl

wasme uses Bazel to build C++ filters under the hood.

The runtime-config.json file present in WASM filter modules is required by wasme to build the filter.

At least must one valid root_id matching the WASM Filter must be present in the rootIds field.

Making changes to the base filter

The new directory contains all files necessary to build and deploy a WASM filter with wasme. A brief description of each file is found below:

File Description
BUILD The Bazel BUILD file used to build the filter.
WORKSPACE The Bazel WORKSPACE file used to build the filter.
bazel/ Bazel external dependencies.
toolchain/ Bazel tooling for building wasm modules. The source code for the filter, written in C++.
filter.proto The protobuf schema of the filter configuration.
runtime-config.json Config stored with the filter image used to load the filter at runtime.

Open in your favorite text editor. We’ll make some changes to customize our new filter.

Navigate to the AddHeaderContext::onResponseHeaders method defined near the bottom of the file. Let’s add a new header that we can use to verify our module was executed correctly. Let’s add a new response header hello: world!:

    addResponseHeader("hello", "world!");

Your method should look like this:

FilterHeadersStatus AddHeaderContext::onResponseHeaders(uint32_t) {
  addResponseHeader("hello", "world!");
  return FilterHeadersStatus::Continue;

The code above will add the hello: world! header to HTTP responses processed by our filter.

Building the filter

Now, let’s build a WASM image from our filter with wasme. The filter will be tagged and stored in a local registry, similar to how Docker stores images.

Images tagged with wasme have the following format:

<registry address>/<registry username|org>/<image name>:<version tag>

See the wasme push documentation for instructions on pushing filters built with wasme.

In this example we’ll include the registry address so our image can be pushed to the remote registry, along with GitHub username which will be used to authenticate to the registry.

Build and tag our image like so:

wasme build cpp -t$YOUR_USERNAME/add-header:v0.1 .

wasme build runs a build container inside of Docker which may run into issues due to SELinux (on Linux environments). To disable, run sudo setenforce 0

The module will take up to a few minutes to build. In the background, wasme has launched a Docker container to run the necessary build steps.

When the build has finished, you’ll be able to see the image with wasme list:

wasme list
NAME                                     SHA      UPDATED             SIZE   TAGS  bbfdf674 26 Jan 20 10:45 EST 1.0 MB v0.1

Next Steps

Now that we’ve successfully built our image, we can try running it locally or pushing it to a remote registry so it can be pulled and deployed in a Kubernetes environment.