How to Use C Libraries with Rust, Trunk, and WebAssembly (Solving Missing stdlib.h)
The Challenge: Compiling C Code for the Browser with Trunk
When building Rust web applications using trunk, your compilation target is typically wasm32-unknown-unknown. This target is designed to run in bare-metal environments like web browsers, meaning it has no operating system and, consequently, no C standard library (libc).
If you try to compile a C library (such as a fork of cubiomes) using the cc crate for this target, you will inevitably run into compilation errors such as:
fatal error: 'stdlib.h' file not foundReimplementing libc from scratch is a massive undertaking and completely unnecessary. Instead, you can use the WASI SDK to provide the missing headers and runtime library, compiling your C code into WebAssembly that seamlessly integrates with your Rust WASM app.
The Solution: Compiling C with WASI SDK
The WASI SDK provides a Clang compiler and a sysroot containing a C standard library implementation designed specifically for WebAssembly. We can configure the Rust cc crate in our build.rs to use this SDK when compiling for the WASM target.
Step 1: Download the WASI SDK
First, you need to download the WASI SDK for your operating system:
- Go to the WASI SDK Releases page.
- Download the appropriate archive for your OS (e.g.,
wasi-sdk-XX.0-linux.tar.gzorwasi-sdk-XX.0-mingw.tar.gzfor Windows). - Extract it to a convenient directory on your machine (for example,
/opt/wasi-sdkorC:\wasi-sdk).
Step 2: Configure Your build.rs
Next, update your build.rs script to detect when you are compiling for wasm32-unknown-unknown. When targeting WASM, configure the cc builder to use the Clang compiler and sysroot provided by the WASI SDK.
Here is a robust build.rs template you can use:
use std::env;
use std::path::PathBuf;
fn main() {
let target = env::var("TARGET").unwrap();
let mut build = cc::Build::new();
// Add your C source files here
build.file("src/c/cubiomes/generator.c");
// build.file("src/c/cubiomes/another_file.c");
if target == "wasm32-unknown-unknown" {
// Retrieve the WASI SDK path from an environment variable
let wasi_sdk = env::var("WASI_SDK_PATH")
.expect("WASI_SDK_PATH environment variable must be set to compile C to WASM");
let wasi_sdk_path = PathBuf::from(wasi_sdk);
// Configure the compiler to use WASI SDK's Clang
let clang = wasi_sdk_path.join("bin").join("clang");
let ar = wasi_sdk_path.join("bin").join("llvm-ar");
let sysroot = wasi_sdk_path.join("share").join("wasi-sysroot");
build.compiler(clang);
build.archiver(ar);
// Pass target and sysroot flags to Clang
build.flag("--target=wasm32-wasi");
build.flag(&format!("--sysroot={}", sysroot.to_str().unwrap()));
}
build.compile("cubiomes");
}Step 3: Run Trunk with the Environment Variable
Because Trunk compiles your project behind the scenes using Cargo, you just need to ensure that the WASI_SDK_PATH environment variable is set when you run your Trunk commands.
On Linux/macOS:
export WASI_SDK_PATH=/opt/wasi-sdk
trunk serveOn Windows (PowerShell):
$env:WASI_SDK_PATH="C:\wasi-sdk"
trunk serveHow This Works Under the Hood
When Trunk triggers cargo build --target wasm32-unknown-unknown, your build.rs intercepts the process. Instead of using your system's default C compiler (which targets your native OS), it routes the compilation through WASI SDK's Clang.
Clang compiles the C code into a wasm32-wasi object file, resolving all standard headers like stdlib.h and stdio.h from the WASI sysroot. Cargo then links this compiled C object with your Rust code into the final .wasm file that Trunk serves to your browser.
Important Considerations for Web Browsers
- System Calls: The WASI libc expects a WASI environment. In a browser, standard I/O (like
printf) or file system access might fail or do nothing unless you provide a WASI polyfill. Keep your C usage focused on computation rather than OS-level operations. - Performance: Compiling C to WebAssembly is highly performant, but ensure any memory allocated in C is properly freed to prevent memory leaks in the browser environment.