WASM instead of C Dependencies?
Tags: webassembly, rust, c, go
Published: 2022-01-14 10:56:20

I have web applications written in Rust and Go that need some basic image processing (reading JPEGs, PNGs, writing JPEGs, WebPs, AVIFs and resizing). This is something I always struggle with, because most libraries for image processing are written in C (`libpng`, `libwebp`, `mozjpeg`; or higher-level ones like `vips`). While there are usually dependencies in each language build on top of those C dependencies, like [`bimg`](https://github.com/h2non/bimg) for Go, I don’t like having C dependencies in a Rust, Go or even Node.js projects.

## Pain during builds

Let’s take `bimg` as an example (just and example, I had the same experience with similar dependencies in Rust, Go and Node.js):

```bash
go get -u github.com/h2non/bimg
```

Trying to get it fails on my system with:

```bash
# pkg-config --cflags  -- vips vips vips vips
Package vips was not found in the pkg-config search path.
Perhaps you should add the directory containing `vips.pc'
to the PKG_CONFIG_PATH environment variable
No package 'vips' found
```

So I have to get `vips` first:

```bash
brew install vips
```

Now I have a dependency that is not managed by the Go toolchain as the rest of my code. Installing it is different from OS to OS.

And does it work now? Nope.

```bash
% go get -u github.com/h2non/bimg
go build github.com/h2non/bimg: invalid flag in pkg-config --cflags: -Xpreprocessor
```

After allowing the cflag, I can finally get the dependency:

```bash
env CGO_CFLAGS_ALLOW="-Xpreprocessor" go get -u github.com/h2non/bimg
```

This is not the developer experience I am aiming for. Not for myself, not for my future self who forgot how it worked, and not for other devs that have to work with my projects.

## Pain during deployments

This is highly subjective, but I really want my `Dockerfile`s to either be `FROM scratch` or `FROM gcr.io/distroless/static` ([`GoogleContainerTools/distroless`](https://github.com/GoogleContainerTools/distroless)). To qualify, my applications must compile to static binaries and must not require `libc`. The worst case I’d be fine with is `FROM gcr.io/distroless/base` (static compiled, but `libc` is fine).

If I want to build a minimal Docker image with a program that as an example depends on `vips`, I'd have to add a long list of shared objects to the image:

```
/opt/vips/lib/libvips.so.42
/usr/lib/libgobject-2.0.so.0
/usr/lib/libglib-2.0.so.0
/usr/lib/libintl.so.8
/lib/ld-musl-x86_64.so.1
/usr/lib/libexpat.so.1
/usr/lib/libheif.so.1
/usr/lib/libwebpmux.so.3
/usr/lib/libwebpdemux.so.2
/usr/lib/libwebp.so.7
/usr/lib/libpng16.so.16
/usr/lib/libjpeg.so.8
/usr/lib/libexif.so.12
/usr/lib/libgmodule-2.0.so.0
/usr/lib/libgio-2.0.so.0
/usr/lib/libffi.so.8
/usr/lib/libpcre.so.1
/usr/lib/libaom.so.3
/usr/lib/libde265.so.0
/usr/lib/libx265.so.199
/usr/lib/libstdc++.so.6
/usr/lib/libgcc_s.so.1
/lib/libz.so.1
/lib/libmount.so.1
/lib/libblkid.so.1
```

## WebAssembly as an alternative solution?

So I was wondering if WebAssembly would be ready to act as a solution to provide C libraries to other ecosystems with less headaches. The questions I was wondering about are:
1. How much slower is WASM compare to a C binding?
2. Does WASM allow me to get rid of all C dependencies?

## How much slower is WASM compare to a C binding?

WASM will be slower compared to C bindings, this is a fact I wasn’t wondering about. I just wanted to get a rough idea of how much slower. So I did a benchmark - as for all benchmarks take it with a grain of salt.

Testsetup: Encode a 6048x2048px big image using [`mozjpeg`](https://github.com/mozilla/mozjpeg) ([￼`mozjpeg-sys`￼](https://github.com/kornelski/mozjpeg-sys) to be more specific), resize it down to 1008x665px using [￼`PistonDevelopers/resize`￼](https://github.com/PistonDevelopers/resize), and decode it again using `mozjpeg`.

As a reference, when executing the test without going through WASM (so Rust directly compiled to an executable), the image transformation takes around `205ms`. I compared this to running the same code compiled to WASM in different WASM runtimes in Go. The results are:
- [`Wasmer`](https://wasmer.io) LLVM runtime: `265ms`
- [`Wasmtime`](https://wasmtime.dev): `272ms`
- [`Wasmer`](https://wasmer.io) Cranelift runtime: `275ms`
- [`wazero`](https://github.com/tetratelabs/wazero): `450000ms` (`7.5min`)

Now I know that it is roughly 25% slower (for the first three) for that specific use-case. Since I’d heavily cache transformed images, I could life with the 25% slowdown.

## Does WASM allow me to get rid of all C dependencies?

Yes, for Rust, as there are plenty of WASM runtimes written in Rust (`Wasmer` and `Wasmtime` for example). But unfortunately no for Go, as `wazero` was the only runtime I could find that is written in Go (and it doesn't work well enough for my use-case). `Wasmer` and `Wasmtime` are also available in Go, but only through `CGO` as both consume C APIs provided by the underlying Rust implementation. So I'd get rid of a C dependency by compiling it to WASM just to add a new C dependency to run the WASM.

## Conclusion

I’d consider WASM as a good alternative for C dependencies for Node.js projects, if the use-case does allow for the slowdown compared to C bindings.

I personally have projects in Rust, Go and on Cloudflare Workers (V8). So my ideal solution would be a Rust project that:
- I can consume as WASM in Rust or directly as a Rust dependency if I am fine with the C dependencies,
- I can consume as WASM in Cloudflare Workers and
- I can consume via C bindings in Go.

My inner monk is still struggling a bit with the third point. But I already have a POC for it and it at least reduces the amount of shared objects I have to provide to my final Docker image to `libc` and `libgcc1`, which would allow me to at least use `gcr.io/distroless/cc` as a base image.

[`GoogleChromeLabs/squoosh`](https://github.com/GoogleChromeLabs/squoosh) is actually pretty close to what I'd want, but isn't published as a Rust dependency and heavily relies on Node.js so doesn't work in Cloudflare Workers.