Using Emscripten Ports for Common Libraries

This page answers one question: how do you link a well-known C library such as zlib, libpng or SDL2 into an Emscripten build without cross-compiling it yourself — and when should you stop using the port and build the library from source instead?

Prerequisites

  • [ ] An activated emsdk (emcc --version works in the shell).
  • [ ] Network access on the first build; ports are downloaded from their upstream repositories.
  • [ ] A project that currently includes headers such as <zlib.h>, <png.h> or <SDL2/SDL.h>.
  • [ ] Optional: a CMake or Make build already wired to emcc as in building Emscripten projects with CMake.

What a port is

An Emscripten port is a recipe shipped inside emsdk that knows how to fetch one library at a pinned version, compile it with emcc, and drop the resulting static archive and headers into a cache. When you pass -sUSE_ZLIB=1, the compiler driver checks the cache, builds the library on first use, adds its include directory to the search path, and links the archive. Your own build never sees a configure script.

The cache lives in emsdk/upstream/emscripten/cache by default, or wherever EM_CACHE points. Ports are compiled once per combination of relevant flags — a threaded build of SDL2 and an unthreaded one are cached separately, as are builds with and without -fwasm-exceptions. That keeps the second build fast but means the cache can grow large on CI machines that try many configurations.

What -sUSE_ZLIB=1 does on the first build The flag triggers a lookup in the Emscripten cache. On a miss the port recipe downloads the pinned source archive, compiles it with emcc, and stores the static library and headers in the cache. Every later build links straight from the cache. -sUSE_ZLIB=1 flag on compile and link cache lookup keyed by flags that matter fetch + build first use only, emcc -O2 libz.a + headers stored in EM_CACHE link step archive added automatically

Step 1 — list what is available

emcc --show-ports
Available official ports:
    boost_headers (USE_BOOST_HEADERS=1; Boost license)
    bzip2 (USE_BZIP2=1; BSD license)
    freetype (USE_FREETYPE=1; freetype license)
    giflib (USE_GIFLIB=1; MIT license)
    harfbuzz (USE_HARFBUZZ=1; MIT license)
    libjpeg (USE_LIBJPEG=1; BSD license)
    libpng (USE_LIBPNG=1; zlib license)
    sdl2 (USE_SDL=2; zlib license)
    sdl2_image (USE_SDL_IMAGE=2; zlib license)
    sqlite3 (USE_SQLITE3=1; public domain)
    zlib (USE_ZLIB=1; zlib license)
    ...

The list shows each port’s flag and licence. Check the licence before you ship — a port is still third-party code inside your binary, and it is easy to forget that a flag pulled in a library with obligations.

Ports provide headers as well as archives, so the flag has to be present when the source files that include those headers compile, not only at link time.

emcc -O2 -sUSE_ZLIB=1 -sUSE_LIBPNG=1 -c thumbnail.c -o thumbnail.o
emcc -O2 -sUSE_ZLIB=1 -sUSE_LIBPNG=1 thumbnail.o \
  -sEXPORTED_FUNCTIONS=_make_thumbnail,_malloc,_free \
  -sEXPORTED_RUNTIME_METHODS=HEAPU8 \
  -o thumbnail.js

With CMake, put the flags in both the compile and link options so every target sees them:

emcmake cmake -B build -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_C_FLAGS="-sUSE_ZLIB=1 -sUSE_LIBPNG=1" \
  -DCMAKE_EXE_LINKER_FLAGS="-sUSE_ZLIB=1 -sUSE_LIBPNG=1"
cmake --build build

Many CMake projects call find_package(ZLIB) or find_package(PNG). The Emscripten toolchain file recognises these for the common ports and points them at the cache, so those calls succeed without extra hints. For less common libraries, set ZLIB_INCLUDE_DIR and friends explicitly from $(em-config CACHE)/sysroot/include.

Step 3 — configure ports that have options

Some ports take settings. SDL2_image needs to know which formats to include; each extra format grows the binary, so the default is deliberately minimal.

emcc -O2 game.c \
  -sUSE_SDL=2 \
  -sUSE_SDL_IMAGE=2 \
  -sSDL2_IMAGE_FORMATS='["png","jpg"]' \
  -o game.html

Newer emsdk releases also accept the --use-port form, which reads more clearly and allows options inline:

emcc -O2 game.c --use-port=sdl2 --use-port=sdl2_image:formats=png,jpg -o game.html

Step 4 — prefetch for offline and CI builds

The first build of a port needs the network, which is exactly what a sandboxed CI job does not have. Build the ports once in a setup step and cache the directory.

export EM_CACHE="$PWD/.emcache"
embuilder build zlib libpng sdl2 --pic   # --pic only if you also build side modules
embuilder build zlib libpng sdl2

Then cache .emcache in CI keyed on the emsdk version, as covered in cross-platform build automation. A port cache built by one emsdk version should not be reused by another: the recipes and their pinned upstream versions change between releases.

First build versus cached build with four ports Wall-clock link time for a project using zlib, libpng, libjpeg and SDL2 ports. The first build compiles every port from source; later builds link straight from the cache. link step wall time (seconds) first build (cold cache) 94 s prefetched with embuilder 6.2 s second local build 5.8 s The 88-second difference is spent entirely compiling third-party C; caching EM_CACHE in CI recovers it on every run.

Step 5 — check what the port actually compiled

A port is a black box until you look inside it, and the two things worth checking are the upstream version and the flags it was built with. The version matters for security fixes; the flags matter because a port built at -O2 without -msimd128 will not vectorize even if the rest of your code does.

# which version does this emsdk pin?
grep -E "^(VERSION|TAG|HASH)" "$(dirname $(which emcc))/tools/ports/libpng.py"

# what ended up in the cache, and how big is it?
ls -la "$(em-config CACHE)/sysroot/lib/wasm32-emscripten/" | grep -E "libz|libpng|libjpeg|SDL2"

# which object files from the archive were actually linked?
emcc -O2 thumbnail.o -sUSE_ZLIB=1 -sUSE_LIBPNG=1 -Wl,--trace-symbol=png_read_info -o /dev/null.js

The --trace-symbol option is a quick way to prove which archive a symbol came from when you suspect a duplicate. For a fuller picture, link once with -Wl,--Map=link.map and search the map file for the archive name; every pulled-in object is listed with its size.

It is also worth recording the resolved port versions alongside the build, the same way you record crate or npm versions. A one-line script that greps the port recipes for their tags and writes them into a BUILD_INFO file means a security advisory against a library can be answered with a lookup instead of an investigation. That is especially important for libraries that parse untrusted input — image decoders and compression libraries are frequent subjects of advisories, and they are also the most popular ports.

Step 6 — know when to stop using the port

Ports are convenient and opinionated. You get one version, one set of configure options and whatever optimization level the recipe chose. That is usually fine; it stops being fine when:

  • you need a newer version than the one emsdk pins — a security fix, for example;
  • you need a configure option the recipe does not expose, such as disabling a codec to save size;
  • you need the library built with the same unusual flags as the rest of your code (a custom -msimd128 build, -fwasm-exceptions, or a different allocator).

At that point build the library yourself with emconfigure ./configure or emcmake cmake, install it into a prefix, and point your build at the prefix. The legacy C migration guide covers the configure-script dance in detail.

Port versus building from source Ports win on convenience and cache reuse; building from source wins on control over version, options and flags. The switch is worth it when a requirement falls outside the recipe. Emscripten port one flag, no configure script cached per flag combination version pinned by the emsdk release options limited to what the recipe exposes default for common libraries Build from source any version, including patched forks every configure option available same flags as the rest of your code you own caching and reproducibility when the port does not fit

Expected output

Running with EMCC_DEBUG=1 on the first build shows the port being fetched and compiled:

emcc: retrieving port: zlib from https://github.com/madler/zlib/archive/refs/tags/v1.3.1.zip
emcc: unpacking port: zlib
cache:INFO: generating port: sysroot/lib/wasm32-emscripten/libz.a... (this will be cached in ".../cache/sysroot/lib/wasm32-emscripten/libz.a" for subsequent builds)
cache:INFO:  - ok

On later builds that block disappears. To confirm the library actually linked, look for its symbols in the output:

wasm-objdump -x thumbnail.wasm | grep -E 'inflate|png_create_read_struct' | head

Gotchas

  • fatal error: 'png.h' file not found. The -sUSE_LIBPNG=1 flag was passed at link time only. Add it to the compile flags of every translation unit that includes the header.
  • wasm-ld: error: undefined symbol: inflate. libpng depends on zlib, and some build setups link only the archives they were told about. Add -sUSE_ZLIB=1 explicitly.
  • CI fails with a network error on a clean runner. The port cache was not restored, so the build tried to download. Prefetch with embuilder in a cached step, or vendor the cache.
  • Two copies of a library in one binary. A static archive you built yourself and a port of the same library both got linked. Symbol clashes may be silent; twiggy will show the duplication.

Performance note

The libpng port added 112 KB to an uncompressed release build (41 KB after Brotli), of which zlib accounted for roughly a third. Disabling the write path — which the port recipe does not do — and building from source saved another 30 KB, which was worth it for a decode-only thumbnail tool and not for anything else. Measure with twiggy before deciding the port is too heavy.

Frequently Asked Questions

Can I use a port from Rust? Not directly — ports are an emcc feature. A Rust crate targeting wasm32-unknown-emscripten can link against a port’s archive, but most Rust projects use a pure-Rust equivalent (such as miniz_oxide for zlib) or compile the C library with the cc crate.

Where is the source for a port recipe? In emsdk/upstream/emscripten/tools/ports/, one Python file per library. Reading it is the fastest way to see exactly which version and options you are getting.

Do ports work with -fwasm-exceptions or SIMD? Ports are rebuilt for some of the flags that change the ABI, such as exceptions and threads, because a mismatch would break linking. Purely performance flags like -msimd128 are generally not propagated, so the port runs scalar code. If a port is on your hot path, building it from source with your flags is the fix.

Can I write a port for my own library? Yes. Emscripten accepts external port files with --use-port=path/to/myport.py, using the same recipe format. It is worth doing for an internal library many projects consume.

← Back to C/C++ to Wasm with Emscripten