Skip to content

Add Emscripten support - #43

Open
cyberneticangel wants to merge 4 commits into
allyourcodebase:mainfrom
cyberneticangel:main
Open

cyberneticangel wants to merge 4 commits into
allyourcodebase:mainfrom
cyberneticangel:main

Conversation

@cyberneticangel

Copy link
Copy Markdown

Closes #5.

Adds a default target configuration for wasm32-emscripten, an emcc-linked build of the example, and a CI job that builds it.

The Emscripten sysroot isn't bundled with Zig, so, as with Android and iOS, its headers are passed in with -Dinclude_path:

embuilder build sysroot # only needed once on a fresh emsdk install
zig build run-example -Dtarget=wasm32-emscripten -Dinclude_path="$(em-config CACHE)/sysroot/include"

Changes

  • src/emscripten.zig: sources and SDL_build_config.h values for Emscripten. These mirror upstream's cmake/PreseedEmscriptenCache.cmake and the Emscripten section of CMakeLists.txt:
    • Emscripten audio, video (GLES2/WebGL), joystick, sensor, power, filesystem and camera drivers.
    • Threads are disabled (SDL_THREADS_DISABLED). This matches upstream's default when building without -pthread.
    • SDL_LOADSO_DUMMY, plus dummy haptics, process, dialog and tray. HIDAPI is disabled.
  • src/sdl.zon:
    • Added sensor/emscripten/SDL_emscriptensensor.c, which was missing.
    • Added tray/dummy/SDL_tray.c to the Emscripten group, the same way the Android group does.
    • Moved main/generic/SDL_sysmain_callbacks.c out of generic into its own main_callbacks_generic group. build.zig compiles that group for every target except Emscripten (see below).
  • build.zig:
    • Added the Emscripten target config.
    • Passes -Dinclude_path to translate_c on Emscripten, because the SDL headers include libc headers.
    • Rejects -Dlinkage=dynamic on Emscripten. Upstream doesn't support shared builds there either.
    • On Emscripten, the example is built as a static library and linked with emcc into zig-out/web, and run-example runs emrun.
  • src/example.zig: switched to SDL's main callbacks (see below).
  • CI: added mymindstorm/setup-emsdk (emsdk 6.0.12) and an Emscripten example build to the Linux jobs.
  • README: ticked Emscripten and documented the setup.

Notes for reviewers

These are the decisions you may want to weigh in on:

  1. The generic main callbacks were silently overriding Emscripten's. main/generic/SDL_sysmain_callbacks.c is only #ifdef'd out on iOS. With both it and main/emscripten/SDL_sysmain_callbacks.c in the archive, the linker picked the generic one, whose blocking loop froze the browser and left the canvas black. Upstream CMake only adds the generic file when a platform has no implementation of its own, and the new group mirrors that. Behavior is unchanged for every other target.

  2. The example uses main callbacks on all platforms now. A blocking while loop can't run in the browser, so the example uses SDL_AppInit/SDL_AppIterate/SDL_AppEvent/SDL_AppQuit. SDL_main.h isn't part of the translated module, so the example declares SDL_EnterAppMainCallbacks itself. Errors are now logged with SDL_Log and return SDL_APP_FAILURE instead of panicking. I can keep the old loop for native targets if you'd rather not change the desktop example.

  3. UBSan traps instead of calling a runtime on Emscripten. In Debug/ReleaseSafe, Zig instruments C code with UBSan handlers whose runtime it normally links itself. Here emcc does the final link. Zig's UBSan runtime can't be built for Emscripten (I tried bundle_ubsan_rt = true), so sanitize_c is set to .trap in those modes. Undefined behavior still stops execution, but with a wasm unreachable rather than a diagnostic. Release modes are untouched.

  4. Workaround for a Zig 0.16 std bug. std.Io.Threaded doesn't compile for Emscripten: std.os.emscripten.W.STOPSIG is declared to return u32 but builds an enum with @enumFromInt. Startup code references the default debug Io, so even an empty program hits it. On Emscripten the example sets std_options_debug_threaded_io = null and std_options_debug_io = std.Io.failing, which is also why it logs through SDL_Log. This is already fixed in Zig 0.17.0, so the workaround is only needed on 0.16. It's harmless on newer versions and can be dropped once 0.17 is the minimum supported version. User apps targeting Emscripten with Zig 0.16 need the same workaround.

  5. Users link their own apps with emcc. Zig can't link Emscripten executables. The example's build step shows how to do it, and the README points to it. The example links with -sALLOW_MEMORY_GROWTH.

Testing

  • With Zig 0.16.0, built the library in Debug, ReleaseSafe, ReleaseFast and ReleaseSmall, and linked each one with emcc into a small C test program that uses the main callbacks.
  • Ran that C test in Firefox. It reported the emscripten video driver and the opengles2 renderer, and a pixel readback from the canvas showed the clear color changing every frame.
  • Built the Zig example for wasm32-emscripten in Debug and ReleaseFast with both Zig 0.16.0 and 0.17.0. Ran it in Firefox: it started with video driver: emscripten, rendered frames and logged no errors.
  • Native examples still build for x86_64-linux-gnu, x86_64-windows-gnu and native, and zig fmt --check passes. Ran the callback-based example natively on Linux: it shows the pulsing gradient as before.
  • CI passes on my fork for 0.16.0, 0.17.0 and master on all three runners, including the new Emscripten build on Linux: https://github.com/cyberneticangel/SDL/actions/runs/38901064616

Not included

  • Threads (-pthread), which need extra emcc flags and cross-origin isolation headers from the server.
  • IDBFS persistent storage (SDL_EMSCRIPTEN_PERSISTENT_PATH is left empty).

cyberneticangel and others added 4 commits October 16, 2026 14:20
Add a default target configuration for `wasm32-emscripten`, mirroring
upstream's CMake configuration for Emscripten. The Emscripten sysroot is
not bundled with Zig, so it must be passed with `-Dinclude_path`.

- Add `src/emscripten.zig` with the Emscripten sources and build config.
  Threads are disabled, matching upstream's default without `-pthread`.
  C sanitizers trap instead of calling into a runtime, since Zig's UBSan
  runtime can't be built for Emscripten and `emcc` does the final link.
- Fix `src/sdl.zon`: add the missing Emscripten sensor driver and the
  dummy tray, and split the generic main callbacks into their own group
  so they aren't linked in place of Emscripten's implementation.
- Build the example for Emscripten by linking it with `emcc` into
  `zig-out/web`, and run it with `emrun`.
- Port the example to SDL's main callbacks so it doesn't block the
  browser's event loop, and work around `std.Io.Threaded` failing to
  compile for Emscripten on Zig 0.16.
- Build the Emscripten example in CI.
- Fail early with a clear error when `-Dlinkage=dynamic` is used for
  Emscripten, which upstream doesn't support.
- Mark Emscripten as supported in the README, and document the
  `-Dinclude_path` and `emcc`/`emrun` setup needed to build for it.
Zig 0.17 renamed the `std.builtin.OptimizeMode` fields, keeping the old
names only as deprecated declarations, so the bare `.Debug` enum literal
no longer resolves. Spell out `std.builtin.OptimizeMode.Debug` like the
rest of `build.zig` does.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Emscripten Support

1 participant