Building box2d for wasm32

I’m trying to use the allyourcodebase package of box2d to build a wasm32 static library that emscripten can link to when building my main project (that is written in C).

this is the command I’m running:

git clone https://github.com/allyourcodebase/box2d
cd box2d
cat >build.zig <<EOF 
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const box2d_dep = b.dependency("box2d", .{});

    const lib = b.addStaticLibrary(.{
        .name = "box2d",
        .target = target,
        .optimize = optimize,
    });
    lib.linkLibC();
    lib.addIncludePath(box2d_dep.path("include"));
    lib.installHeadersDirectory(box2d_dep.path("include"), "", .{});
    lib.addCSourceFiles(.{
        .root = box2d_dep.path("src"),
        .flags = &.{
            "-std=c17",
        },
        .files = &.{
            "aabb.c",
            "array.c",
            "bitset.c",
            "body.c",
            "broad_phase.c",
            "constraint_graph.c",
            "contact.c",
            "contact_solver.c",
            "core.c",
            "distance.c",
            "distance_joint.c",
            "dynamic_tree.c",
            "geometry.c",
            "hull.c",
            "id_pool.c",
            "island.c",
            "joint.c",
            "manifold.c",
            "math_functions.c",
            "motor_joint.c",
            "mouse_joint.c",
            "prismatic_joint.c",
            "revolute_joint.c",
            "shape.c",
            "solver.c",
            "solver_set.c",
            "stack_allocator.c",
            "table.c",
            "timer.c",
            "types.c",
            "weld_joint.c",
            "wheel_joint.c",
            "world.c",
        },
    });
    b.installArtifact(lib);
}
EOF
zig build -Dtarget=wasm32-emscripten

which produces the error:

install
└─ install box2d
   └─ zig build-lib box2d Debug wasm32-freestanding 32 errors
/Users/daniel/.cache/zig/p/1220c6750412ebc698694cd08aca4fe87fda8e2df0d4ffa7d33e7de5b8ca5bebb643/include/box2d/math_functions.h:9:10: error: 'math.h' file not found
#include <math.h>
         ^~~~~~~~~
/Users/daniel/.cache/zig/p/1220c6750412ebc698694cd08aca4fe87fda8e2df0d4ffa7d33e7de5b8ca5bebb643/src/math_functions.c:4:10: note: in file included from /Users/daniel/.cache/zig/p/1220c6750412ebc698694cd08aca4fe87fda8e2df0d4ffa7d33e7de5b8ca5bebb643/src/math_functions.c:4:
#include "box2d/math_functions.h"

Any ideas how to get a wasm32 library built of box2d?

I believe Alex is working on a libc for zig that would work in this instance (see Remove `wasm32-freestanding-musl` by alexrp · Pull Request #22240 · ziglang/zig · GitHub).

I guess there use to be wasm32-freestanding-musl which would give access to the musl libc headers, but, it was removed since it was pretty broken.

You could make this work without changes to Zig if you’re up for the task. You can provide your own libc by calling setLibCFile on the box2d library. You could hook this up to your own implementation or even my ziglibc project, or just copy code from it if you like. I might be willing to pair with you on this via Discord if you like as well, I have same username on the various zig servers. (we could probably upstream this to the box2d repo as well)

Why do you use wasm32-freestanding when you are using emscripten?
Have you tried using wasm32-emscripten?

That doesn’t work either

Have you specified the --sysroot include to the emscripten sysroot?
In what way does it fail, what are the error messages?

For this game I had to manually add the sysroot include folder to the include pathes so that zig finds the header files for things like math.h when compiling for wasm32-emscripten: zig15game/build.zig at 5d0a33d753528df890f0ab5dd56cd04ff6d0e1c2 · SimonLSchlee/zig15game · GitHub

My guess would be that some similar variation could work for a library, that is linked later.

Here is the exact command I’m running so you can give it a try:

git clone https://github.com/allyourcodebase/box2d
cd box2d
cat >build.zig <<EOF 
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const box2d_dep = b.dependency("box2d", .{});

    const lib = b.addStaticLibrary(.{
        .name = "box2d",
        .target = target,
        .optimize = optimize,
    });
    lib.linkLibC();
    lib.addIncludePath(box2d_dep.path("include"));
    lib.installHeadersDirectory(box2d_dep.path("include"), "", .{});
    lib.addCSourceFiles(.{
        .root = box2d_dep.path("src"),
        .flags = &.{
            "-std=c17",
        },
        .files = &.{
            "aabb.c",
            "array.c",
            "bitset.c",
            "body.c",
            "broad_phase.c",
            "constraint_graph.c",
            "contact.c",
            "contact_solver.c",
            "core.c",
            "distance.c",
            "distance_joint.c",
            "dynamic_tree.c",
            "geometry.c",
            "hull.c",
            "id_pool.c",
            "island.c",
            "joint.c",
            "manifold.c",
            "math_functions.c",
            "motor_joint.c",
            "mouse_joint.c",
            "prismatic_joint.c",
            "revolute_joint.c",
            "shape.c",
            "solver.c",
            "solver_set.c",
            "stack_allocator.c",
            "table.c",
            "timer.c",
            "types.c",
            "weld_joint.c",
            "wheel_joint.c",
            "world.c",
        },
    });
    b.installArtifact(lib);
}
EOF
zig build -Dtarget=wasm32-emscripten

and the first error I get:

/Users/daniel/.cache/zig/p/1220c6750412ebc698694cd08aca4fe87fda8e2df0d4ffa7d33e7de5b8ca5bebb643/include/box2d/math_functions.h:9:10: error: 'math.h' file not found
#include <math.h>
         ^~~~~~~~~
/Users/daniel/.cache/zig/p/1220c6750412ebc698694cd08aca4fe87fda8e2df0d4ffa7d33e7de5b8ca5bebb643/src/table.c:6:10: note: in file included from /Users/daniel/.cache/zig/p/1220c6750412ebc698694cd08aca4fe87fda8e2df0d4ffa7d33e7de5b8ca5bebb643/src/table.c:6:
#include "core.h"
         ^
/Users/daniel/.cache/zig/p/1220c6750412ebc698694cd08aca4fe87fda8e2df0d4ffa7d33e7de5b8ca5bebb643/src/core.h:6:10: note: in file included from /Users/daniel/.cache/zig/p/1220c6750412ebc698694cd08aca4fe87fda8e2df0d4ffa7d33e7de5b8ca5bebb643/src/core.h:6:
#include "box2d/math_functions.h"
         ^

I’ve hit the same regression porting my project from 0.8.1 to ‘0.14.0-dev.3217+5b9b5e45c’ whilst building LVGL for freestanding wasm.
The only hack I’ve found so far is to reference the linux C headers (in my case Asahi) directly.

wasm_lib.addIncludePath(b.path("../../../../../usr/include"));

Referencing the headers with an absolute path caused an error something about not being allowed to reference stuff above the build folder. I cant check exactly what the errors are, because today I’m getting some strange ‘failed to check cache’ error.

However I think the above hack works because in the case of LVGL the C dependencies are probably resolved via macros in the headers, ie simple std and string stuff.

I’ve encountered that problem too in my WASM Zig experiments - I guess Zig simply doesn’t have any C library support for wasm32-emscripten and wasm32-freestanding (wasm32-wasi works fine).

Since I’m depending on the Emscripten SDK anyway for linking and Emscripten-specific APIs, I basically use the Emsdk as a complete C sysroot and inject the Emscripten system include path into the C code that’s compiled via Zig, e.g.:

…and since include paths propagate through the dependency tree, other C dependencies just need to depend on the sokol C library to find the Emscripten C headers.

…oops I might have been wrong on that propagation thing… because this is how I inject the Emscripten sysroot into other C dependencies:

2 Likes

The only Wasm target Zig provides a libc for is wasm32-wasi-musl (it used to claim to support wasm32-freestanding-musl but this never really worked in practice). For all other targets you have to explicitly supply the headers yourself and make sure they are added to the header search paths.

For Emscripten specifically the headers are located in the sysroot/include subpath of the Emscripten cache, which you can print the path to by running em-config CACHE. If you don’t want to hard-code the path I suggest using b.sysroot and requiring the user invoking zig build to explicitly provide a sysroot by passing --sysroot "$(em-config CACHE)/sysroot".

var emscripten_system_include_path: ?std.Build.LazyPath = null;
switch (target.result.os.tag) {
    .emscripten => {
        if (b.sysroot) |sysroot| {
            emscripten_system_include_path = .{ .cwd_relative = b.pathJoin(&.{ sysroot, "include" }) };
        } else {
            std.log.err("'--sysroot' is required when building for Emscripten", .{});
            std.process.exit(1);
        }
    },
    else => {},
}

// ...

if (emscripten_system_include_path) |path| {
    lib.addSystemIncludePath(path);
}

One caveat is that there’s unfortunately no catch-all way to add default header search paths for all artifacts (including those from dependencies), meaning that any packages you depend on will also need to explicitly handle --sysroot the same way in their build.zig files.

This should be enough to build Box2D. If you get errors like error: call to undeclared function 'clock_gettime' it’s because -std=c17 is disabling non-standards compliant features. Changing it to -std=gnu17 should fix it.

2 Likes

I was finally able to get zig to compile box2d for wasm32-emscripten using:

BOX2D_PATH=$HOME/box2d-3.1.0
BOX2D_SRC_FILES=($BOX2D_PATH/src/*.c(N))
zig build-lib $BOX2D_SRC_FILES \
-DBOX2D_DISABLE_SIMD \
-cflags -std=c17 -mbulk-memory -matomics -msimd128 -msse2 -- \
-I$BOX2D_PATH/include \
-lc -OReleaseFast --name box2d -static \
-target wasm32-emscripten -mcpu baseline \
-isystem $EMSDK/upstream/emscripten/cache/sysroot/include

The -DBOX2D_DISABLE_SIMD is required because it seems like zig’s SIMD headers don’t support wasm32?:

zig-macos-aarch64-0.13.0/lib/include/emmintrin.h:14:2: 
error: "This header is only meant to be used on x86 and x64 architecture"

box2d-3.1.0/src/contact_solver.c:527:10: note: in file included from box2d-3.1.0/src/contact_solver.c:527:
#include <emmintrin.h>

emcc is able to compile box2d with simd enabled.

BOX2D_OBJ_FILES := $(notdir $(BOX2D_SRC_FILES:.c=.o))
emcc-box2d:
    emcc -mbulk-memory -matomics -msimd128 -msse2 -I$(BOX2D_PATH)/include -c $(BOX2D_SRC_FILES) && \
    emar qc libbox2d.a $(BOX2D_OBJ_FILES) && \
    emranlib libbox2d.a

If anyone knows how to compile SIMD for wasm32-emscripten, I’d appreciate your advice!

Following the bread crumb trail (Box2D source code and Emscripten docs), you seem to do everything right (e.g. -msimd128 -msse2 should do the right thing and allow Emscripten - or rather Clang/‘zig-cc’ to compile code that includes emmintrin.h).

But this looks wrong:

zig-macos-aarch64-0.13.0/lib/include/emmintrin.h

…here the code uses a macOS include instead of an Emscripten include. => also wrong assumption, see @alexrp’s comment below - I think it’s still the wrong header though.

AFAIK -isystem isn’t enough to redirect system includes to the Emscripten toolchain, you’ll also explicitly need to pass a regular header search path via -I $EMSDK/upstream/emscripten/cache/sysroot/include… or maybe -isystem $EMSDK/upstream/emscripten/cache/sysroot/include

…disregard the above, somehow my mind was confusing -isystem with -isysroot… so the -isystem is already pointing to the Emscripten headers, however:

The emmintrin.h header seems to be under .../include/compat/ in the Emscripten sysroot cache, so maybe you also need to add another search path that includes the compat/ subdir.

PS: it’s also weird that Zig seems to fallback to the macOS zig toolchain headers when compiling with -target wasm32-emscripten, not sure if this is a Zig bug or inherited from Clang. I would actually expect a compiler error that emmintrin.h cannot be found (since it is in the compat/ subdir in the Emscripten SDK.

Here, zig-macos-aarch64-0.13.0 is the name of the directory containing the zig binary, not the location of the macOS headers. That would be zig-macos-aarch64-0.13.0/lib/libc/include/any-macos-any.

1 Like

…ah ok, it still looks wrong to me though, I think when compiling for wasm32-emscripten it needs to pick the emmintrin.h header from the Emscripten SDK, because that uses wasm-specific SIMD intrinsics for ‘emulation’:

…while the header in the Zig toolchain indeed requires x86:

(tbf: Emscripten providing a header which emulates x86 intrinsics on WASM is kinda ‘special’ for cross-compiling)