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 --versionworks 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
emccas 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.
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.
Step 2 — add the flag to both compile and link
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.
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
-msimd128build,-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.
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=1flag 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=1explicitly.- CI fails with a network error on a clean runner. The port cache was not restored, so the build tried to
download. Prefetch with
embuilderin 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;
twiggywill 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.
Related
- Building Emscripten projects with CMake — where port flags go in a CMake build.
- Porting pthreads code with Emscripten — ports are cached separately for threaded builds.
- Using the Emscripten file system API — feeding files to libraries that expect paths.
- Caching Rust Wasm builds in GitHub Actions — the same caching ideas, applied to cargo.
← Back to C/C++ to Wasm with Emscripten