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 found

Reimplementing 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.gz or wasi-sdk-XX.0-mingw.tar.gz for Windows).
  • Extract it to a convenient directory on your machine (for example, /opt/wasi-sdk or C:\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 serve

On Windows (PowerShell):

$env:WASI_SDK_PATH="C:\wasi-sdk"
trunk serve

How 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.