Skip to content

Enable arm64_32-apple-watchos (ILP32) static-lib build — cleanup of #97#98

Merged
chaploud merged 8 commits into
mainfrom
develop/arm64_32-apple-watchos
May 17, 2026
Merged

Enable arm64_32-apple-watchos (ILP32) static-lib build — cleanup of #97#98
chaploud merged 8 commits into
mainfrom
develop/arm64_32-apple-watchos

Conversation

@chaploud
Copy link
Copy Markdown
Contributor

Replacement for #97 with project-conventional cleanup stacked on top of @matthargett's original commit. Authored-by metadata on commit 3d563237 is preserved.

Closes #97.

Summary

Enables zig build static-lib -Dtarget=aarch64-watchos-ilp32 -Djit=false for Apple Watch SE / SE2 / S4-S8 (ILP32 ABI). Matt's original commit (3d563237) ships untouched; the 6 stacked commits address review feedback so the change is safe to land on main.

Why a replacement PR

The maintainerCanModify=true flag on #97 unfortunately did not allow us to push directly to the fork branch from clojurewasm/zwasm's admin account (GitHub rejected with 403 — likely an org-side restriction on rebeckerspecialties). Rather than block on that, the cleanup commits land here with full credit preserved.

Commits

# Subject
1 Enable arm64_32-apple-watchos (aarch64-watchos-ilp32) build (matthargett — original)
2 fix(c_api): use std.debug.no_panic on ILP32, scope override to that ABI
3 fix(build): scope static-lib single_threaded to ILP32 targets
4 fix(types): require explicit config.io on ILP32 when WASI or timeout is requested
5 docs(D139,W55): document arm64_32-apple-watchos ILP32 support strategy
6 ci: build-only smoke check for aarch64-watchos-ilp32
7 fix(types): rename ILP32 io guard to error.MissingIo

What the cleanup changed vs #97

  • single_threaded is now ILP32-only (target.result.ptrBitWidth() < 64). Enable arm64_32-apple-watchos (Apple Watch SE2 / Series 4-8) build #97 forced it on every static-lib target, silently degrading memory.atomic.wait/notify for 64-bit Linux / macOS / Windows C-API consumers.
  • panic namespace shrunk to one linepub const panic = if (ilp32) std.debug.no_panic else std.debug.FullPanic(std.debug.defaultPanic);. Zig 0.16 already ships std.debug.no_panic for exactly this purpose; the original ~80-line hand-rolled @trap() namespace also affected every C-API consumer's panic messages.
  • error.MissingIo on ILP32 when WASI or timeout_ms is requested without config.io — the previous code left vm.io as undefined and relied on the embedder not exercising deadline / WASI host preopen paths. Now we fail loud.
  • D139 in .dev/decisions.md documents the support matrix, alternatives considered, and the rationale per Enable arm64_32-apple-watchos (Apple Watch SE2 / Series 4-8) build #97's safety strategy. W55 in .dev/checklist.md tracks the Zig 0.16 .a packing bug and other follow-ups.
  • Build-only CI smoke for aarch64-watchos-ilp32 on macos-latest — GitHub runners have no watchOS SDK so the archive isn't exercised, but the comptime gates are checked.

Verified

  • Mac aarch64 Commit Gate: 7/7 PASS (tests / spec / e2e 796 / realworld 50/50 / ffi 80/80 / bench-quick / minimal)
  • Ubuntu x86_64 (OrbStack) Commit Gate: 6/6 PASS (tests / spec / e2e 796 / realworld 50/50 / ffi 80/80 / minimal)
  • zig build static-lib -Dtarget=aarch64-watchos-ilp32 -Djit=false -Dcomponent=false -Dwat=false on Mac: produces 2.7 MB arm64_32 / armv8 Mach-O object (Zig 0.16 archive-packing bug for this triple still requires ar rcs workaround on the consumer side, as noted in Enable arm64_32-apple-watchos (Apple Watch SE2 / Series 4-8) build #97 and tracked in W55)

Test plan

  • bash scripts/gate-commit.sh on Mac aarch64
  • bash scripts/gate-commit.sh on Ubuntu x86_64
  • CI green on all platforms (Mac / Linux / Windows / aarch64-watchos-ilp32 smoke)
  • After squash-merge: bash scripts/record-merge-bench.sh on Mac (per D-g policy)

matthargett and others added 7 commits May 17, 2026 01:18
The previous override declared a custom `panic` namespace and
`std_options` unconditionally for every C-API consumer (Mac / iOS /
Linux / Windows static and shared libs), losing pretty panic
messages and stdlib logging to work around a compile error that
only surfaces on ILP32 (arm64_32-apple-watchos).

Zig 0.16 already ships `std.debug.no_panic` — the canonical
trap-on-every-panic namespace. Use it directly on ILP32 and fall
back to `std.debug.FullPanic(std.debug.defaultPanic)` (the stdlib
default) on every other target. Same gating for `std_options`.

Net: ~80 fewer hand-rolled lines, no behaviour change for 64-bit
C-API consumers, ILP32 build still compiles.
`.single_threaded = true` was being forced on every static-lib
build (macOS / iOS / Linux / Windows), silently degrading wasm-
threads `atomic.wait/notify` on 64-bit C-API consumers — the wait
queue paths in `src/memory.zig` rely on `std.Thread.Mutex` /
`Condition`, which become no-ops under single_threaded.

The ILP32 motivation (std.Io.Threaded not compiling under
arm64_32-apple-watchos) only applies when usize is 32-bit, so gate
on `target.result.ptrBitWidth() < 64` and leave the default
(multi-threaded) on every other target.
…is requested

The previous ILP32 code path left `vm.io` as `undefined` whenever
config.io was null, on the assumption that consumers without WASI
would never dereference it. That is unsafe: `setDeadlineTimeoutMs`
unconditionally calls `std.Io.Timestamp.now(self.io, ...)` for any
non-null `config.timeout_ms`, and `applyWasiOptions` reaches into
`io.vtable.now` / `io.openDir` via `addPreopenPath` when WASI is
enabled. Both would deref an undefined vtable.

Refuse loadCore early with `error.IlpRequiresExplicitIo` if either
of these features is requested on ILP32 without a caller-supplied
io. Pure-interpreter workloads (the watchOS wasm-benchmark use
case: no WASI, no timeout, no atomics) are unaffected.

Wasm modules executing `memory.atomic.wait/notify` also reach io
but can't be detected statically — embedders who run such modules
on ILP32 must pass config.io themselves.
Add a D## entry capturing why ILP32 needs separate handling
(std.Io.Threaded fails u64 → usize narrowing under that ABI),
which features stay supported, which are explicitly refused
(WASI / timeout without config.io), and the comptime gating
strategy that keeps 64-bit consumers byte-identical.

Track open follow-ups under W55-watchos-ilp32 in checklist.md:
upstream Zig .a packing bug, C-header documentation of the
new error code, and the atomic.wait/notify correctness gap.

Tables touched by `md-table-align` (per project convention)
collapsed adjacent rows in D128 / D137 that were already
mis-aligned — pure whitespace, no semantic change.
GitHub Actions runners do not ship the watchOS SDK so the
resulting archive cannot be linked or executed there; without
a build-only gate the ILP32 comptime branches in src/c_api.zig,
src/guard.zig, src/memory.zig, src/types.zig, src/vm.zig and
the build.zig single_threaded selector will rot the next time
anyone touches those files.

Add a dedicated job that runs `zig build static-lib
-Dtarget=aarch64-watchos-ilp32 -Djit=false -Dcomponent=false
-Dwat=false` on macos-latest and verifies the resulting cached
`libzwasm_zcu.o` is the expected `arm64_32 / armv8` Mach-O
object — Zig 0.16's broken .a packing for this triple (D139
known issue) means the `.a` itself is only the SYMDEF, not a
useful linkable artefact, so we point the check at the `.o`
directly.
The error returned when loadCore refuses to auto-init io on
ILP32 is part of WasmModule.load's inferred error set across
every target, not just watchOS. Naming it after the ABI
(`IlpRequiresExplicitIo`) leaks that detail into the public
cross-target error surface and asks 64-bit callers to handle
an arm-name they can never observe.

`error.MissingIo` describes the cause (config.io was null when
something needed it) without advertising the platform. Same
loud-failure semantics, neutral name.
CI run on macos-latest failed because the runner's `file` version
prints `Mach-O object arm64_32_v8` while local Darwin 25 prints
`Mach-O 64_32-bit armv8 object`. The original grep matched only
the local form. Accept either by switching to `grep -E
'arm64_32|64_32-bit armv8'` — the substring `arm64_32` is the
stable cpusubtype name common to both.
@chaploud chaploud merged commit 428774d into main May 17, 2026
11 checks passed
@chaploud chaploud deleted the develop/arm64_32-apple-watchos branch May 17, 2026 14:58
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.

2 participants