Skip to content

feat: Temporary PR to view accumulated changes to compile libDispatch to Wasm - #1

Closed
scottmarchant wants to merge 8 commits into
mainfrom
feat/scottm/libDispatchWasm
Closed

feat: Temporary PR to view accumulated changes to compile libDispatch to Wasm#1
scottmarchant wants to merge 8 commits into
mainfrom
feat/scottm/libDispatchWasm

Conversation

@scottmarchant

Copy link
Copy Markdown
Collaborator

No description provided.

scottmarchant and others added 8 commits June 9, 2026 21:30
First milestone of the WebAssembly/WASI port. Single-threaded libdispatch now
compiles for wasm32-wasip1 and runs real GCD APIs (dispatch_async,
dispatch_group + notify, dispatch_after timers, dispatch_main) on a cooperative
run loop with no worker-thread pool and no JavaScript. Verified on wasmtime and
browser_wasi_shim; module imports are wasi_snapshot_preview1 only.

- event: new DISPATCH_EVENT_BACKEND_WASI + src/event/event_wasi.c (timer-only
  backend; _dispatch_event_loop_drain sleeps via poll_oneoff to next deadline)
- queue.c: dispatch_main() -> _dispatch_wasi_runloop_main cooperative loop
  (drains mgr queue + anon timers + root queues, then blocks to next deadline);
  _gettid, sigsuspend/sig_thread, root_queues_init_once wasi branches
- internal.h: force DISPATCH_USE_PTHREAD_POOL=0 (keep INTERNAL_WORKQUEUE=1);
  wasi system-include branch
- shims: wasi lock encoding + trapping sema4 + wait_on_address (lock.h/.c),
  clock_gettime time (time.h), 1-cpu hw_config, getprogname, _dispatch_thread_switch
- public headers: widen __unix__ gates to include __wasi__ (os/object.h,
  dispatch/dispatch.h, private/private.h)
- init.c: stderr logging; avoid __builtin_return_address (unsupported on wasm)
- io.c/transform.c: wasi major()/endian shims
- CMake: detect CMAKE_SYSTEM_NAME==WASI, add _WASI_EMULATED_* defines, gate
  -Werror off during bring-up

Blocking APIs (dispatch_sync, blocking semaphore/group waits) trap by design.
Main-queue draining, the Swift overlay, and the threaded variant are follow-ups.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The thread-bound main queue already drains correctly under the cooperative
dispatch_main() run loop (it flows through its target root queue, which the
loop drains), so DispatchQueue.main async/after work with no further changes.

Add tests/wasi:
- dispatch_wasi_smoke.c        async + group(+notify) + dispatch_after
- dispatch_wasi_mainqueue.c    main-queue serial FIFO + interleave + timer
- run.sh / README.md           build + run under wasmtime

Verified on wasmtime and browser_wasi_shim; main-queue work runs in strict
FIFO order. Blocking APIs still trap by design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Swift overlay (src/swift/*.swift) now cross-compiles to a Dispatch.swiftmodule
+ libswiftDispatch.a for wasm32-wasip1, and a downstream `import Dispatch`
program (DispatchQueue.async/asyncAfter, DispatchGroup.notify, dispatchMain())
compiles, links, and runs under wasmtime — imports limited to
wasi_snapshot_preview1 (no JavaScript).

- src/swift/Source.swift, Wrapper.swift: gate the Darwin/BSD-only source-type
  accessors (PROC/VNODE/FS-object/etc.) with `&& !os(WASI)` so WASI follows the
  Linux exclusion (those C types don't exist on wasi).
- os/generic_unix_base.h: on wasi, don't pull <sys/param.h> (it includes
  <endian.h>, whose __bswap* inlines are redefined inside the Swift clang
  importer's CDispatch module); provide MIN/MAX/howmany directly instead.
- tests/wasi: add dispatch_wasi_swift.swift (the import-Dispatch consumer test)
  and swift-overlay.md (build recipe + toolchain notes: use the swift.org
  toolchain matching the Wasm SDK, emulation defines, module-map, clang_rt).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add dispatch_wasi_api.swift covering custom serial queue FIFO ordering,
DispatchWorkItem, DispatchSemaphore (signal/wait fast path), concurrentPerform,
DispatchData, and a repeating DispatchSource timer (fires 3x, exercising the
timer re-arm path). All pass under wasmtime via the single-threaded run loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s trap

Empirically refine what works vs traps on the single-threaded port:
- dispatch_sync to a non-current queue runs INLINE and works (global + serial
  verified) — uncontended sync needs no blocking on one thread. (Better than the
  plan assumed; only re-entrant sync onto the current queue deadlocks/crashes.)
- A dispatch_semaphore_wait(FOREVER) with nothing to signal it raises a clean
  DISPATCH_CLIENT_CRASH (wasm trap), not a hang.

Add dispatch_wasi_sync.c and correct tests/wasi/README scope accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The single-threaded wasi build compiles cleanly under the project's full
-Werror -Wall -Wextra set once the unused pool/thread vars are marked
DISPATCH_UNUSED and the pthread-pool context array is POOL-gated (done in the
earlier commits). Remove the temporary -Wno-error bring-up band-aids:
- DispatchCompilerWarnings.cmake: drop the `if(NOT DISPATCH_WASI)` guard
- CMakeLists.txt / src/CMakeLists.txt: drop the -Wno-error overrides

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tests/wasi/swift-demo: a vanilla SwiftPM executable that `import Dispatch` and
cross-compiles to wasm via `swift build --swift-sdk` — the real downstream flow.
Verified: builds and runs under wasmtime (custom queue async, DispatchGroup
notify on .main, DispatchQueue.main.asyncAfter), imports wasi_snapshot_preview1
only (no JavaScript).

- build-prefix.sh: builds the overlay (-module-link-name swiftDispatch) and
  assembles a self-contained SDK-overlay prefix (headers + module maps +
  Dispatch.swiftmodule + libs).
- Package.swift / main.swift: the consumer (flags only needed because the
  overlay is consumed out-of-tree; upstreamed, `import Dispatch` needs none).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When configured for WASI (toolchain sets CMAKE_SYSTEM_NAME=WASI),
tests/CMakeLists.txt routes to tests/wasi/CMakeLists.txt and returns early
(the legacy bsdtest harness uses fork/exec, absent on WASI). The wasi
CMakeLists builds dispatch_wasi_smoke/mainqueue/sync to wasm32-wasip1 and
registers them as ctest cases run under a WASI runtime (wasmtime by default;
override with -DWASI_TEST_RUNNER). Verified: `ctest` -> 3/3 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@scottmarchant

scottmarchant commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Comparison: PR #1 vs PR #2 vs PR #3 (libdispatch → Wasm/WASI)

(Updated after two review rounds on #3: a full-branch review with fixes, then an adversarial probe round from the #2 side with a hardening series. Every row is verified empirically — built and executed. Cross-posted on #1, #2, and #3.)

All three branches were built for wasm32-unknown-wasip1 on macOS arm64 and their test suites run. Toolchain: swift.org 6.3.3 toolchain + Swift 6.3.3 Wasm SDK (#2/#3), wasi-sdk 33 (#1), wasmtime 47, Node 26, CMake 4.4 (floor verified at exactly 3.31.0). #3's event-source and re-entrancy behavior additionally verified under WasmKit (built from source) and @bjorn3/browser_wasi_shim; native neutrality proven with Linux container builds (identical 23/23 upstream tests on main and #3).

Branch summary

#1 (feat/scottm/libDispatchWasm) #2 (krodak/libdispatch-wasm) #3 (feat/scottm/libDispatchWasmV2)
Runtime design defer-everything: work runs only under dispatch_main(); blocking traps cooperative eager drain; blocking waits drain other work #2's runtime + fd/signal event sources + poke-defer brackets: pokes never eager-drain beneath caller-held locks (blocking waits still pump there, by documented contract)
Diff vs main +874 / −26 (36 files) +2,148 / −75 (55 files) +4,861 / −81 (76 files; stacked on #2)
ctest 3/3 30/30 52/52
Swift overlay manual out-of-tree recipe, not in CI CMake-integrated, autolink modulemap, consumer tests same as #2

Dispatch API support

Legend: ✅ works · ⚠️ works with caveats · ⏱ contract violation (returns instantly instead of waiting) · 💥 traps · ❌ broken/unsupported.

Queues & submission

API #1 #2 #3
Queue create/attrs/label/target/specifics, global & main queues
dispatch_async / barrier_async ⚠️ nothing runs until dispatch_main() ✅ eager ✅ eager; pinned by test
dispatch_sync / barrier_sync ✅ inline; re-entrancy detection unreliable (tid bug) ✅ inline; but a poke from inside a sync body eager-drains under the held barrier locksync(qA){async(qB){sync(qA)}} crashes where threaded platforms complete ✅ inline; pokes inside sync bodies, dispatch_once initializers, dispose, and set_specific defer and flush after the critical section — that program now completes (tested on 3 runtimes)
dispatch_after, timers (uptime & wall) ✅ (wall deadline movable by host clock) ✅ (wall anchored at arm; tested)
dispatch_apply ✅ inline-serial ✅ inline-serial ✅ inline-serial
dispatch_main ⚠️ busy-spins at 100 % CPU when idle ✅ traps loudly when idle forever ✅ parks in host poll on armed timers and fd sources; a signal-source-only park still traps as truly idle (in-process raise() cannot reach a parked sole thread — deliberate, documented)
QoS order & fairness ❌ inverted + starvation ✅ tested
dispatch_assert_queue / _not ❌ inverted by tid-encoding bug ✅ + regression test

Synchronization

API #1 #2 #3
dispatch_once ⚠️ initializer that submits work eager-drains under the once gate (drained item re-entering the once crashes; threaded platforms wait) ✅ deferred under the gate
Semaphore create/signal/uncontended wait
semaphore_wait(FOREVER) 💥 traps ✅ drains cooperatively ✅ also wakes on fd events (tested)
semaphore_wait(timeout) ⏱ instant ✅ honors deadline ✅ + regression test for the timeout-vs-signal race (see the "working as designed" note in #3's description)
group_wait (timed / FOREVER) ⏱ instant / ❌ returns −1 ✅ / ✅ ✅ / ✅
dispatch_block_wait ⏱ instant
Provable single-thread deadlocks 💥 (all blocking) 💥 named diagnostics; contended unfair lock/once gate spins silently at 100 % CPU (empty _dispatch_thread_switch) 💥 named diagnostics everywhere — the lock/gate spin is now a named crash

Sources

API #1 #2 #3
Timer sources
User-data sources
READ / WRITE fd sources ❌ silently never fires 💥 traps at registration ✅ via poll_oneoff fd subscriptions; armed-source count now unbounded (growable poll set; previously capped at 64 with a load-dependent trap)
SIGNAL sources ❌ silent 💥 traps ✅ in-process raise() semantics, count-accurate; registration saves and unregistration restores the app's own signal() disposition (previously reset to SIG_DFL, so the app's next raise() terminated the process); SIG_ERR checked
PROC / VNODE / Mach / memory-pressure ❌ — no WASI facility exists
fd closed while source armed (n/a — sources never fire) (n/a) 💥 named crash on both harvest shapes (per-subscription POLLNVAL and wasmtime's whole-call EBADF), each pinned by a limited-host runner mode. Deliberate: kqueue logs a vanish, epoll silently never fires again. Caveat: on Node, the guest closing an armed stdio fd aborts the host inside libuv before libdispatch can see it
pipe EOF with an un-canceled source (n/a) (n/a) ✅ readable-at-EOF delivered per the Darwin contract (handler observes the 0-byte read and cancels); a handler that never cancels on a host without the poll hangup flag becomes a named crash via a windowed rate guard instead of a silent ~500k-fires/sec spin

Data & I/O

API #1 #2 #3
DispatchData (all operations)
dispatch_read/write, DispatchIO on regular files ✅ verified ✅ verified ✅ verified
Same on non-regular fds (streams) ❌ hangs silently 💥 traps ✅ tested: a DispatchIO stream channel on stdin completes a read whose payload arrives 250 ms into the wait

Test coverage

Suite #1 #2 #3
Upstream libdispatch tests 13 13
Focused WASI semantics tests 3 bespoke 13 32#2's set + the earlier contract/event-source/review-regression tests + the probe-round additions: async-and-wait (privdata funnel), pipe-EOF in both host shapes, close-while-armed, group wait woken by an fd source, payload-asserted regular-file IO
Swift consumer tests (in ctest) 0 (2 manual) 2 2
Runner self-checks 3 5 (early-guest-exit robustness; a limited-host mode that denies fd poll subscriptions, pinning the ENOTSUP capability crash)
Total in CI 3 30 52

Harness note (#3): Node is now optional — without it (or below 19.8) everything still builds and all tests register as visible-but-DISABLED, the same shape as the missing-WASI_TEST_RUNNER path (#1's design, adopted per the two-ports report).

Runtime support

Capability wasmtime 47 Node node:wasi WasmKit browser_wasi_shim uwasi
Timers / queues / sync (all three PRs) ⚠️ busy-wait sleeps, single-subscription ❌ no poll_oneoff
fd read/write sources (#3) ✅ (regular files via always-ready path) ✅ (bounded poll slices dodge its infinite-timeout host trap) ❌ loud named crash at registration ❌ same
Signal sources (#3) ✅ (pure libc)

Review round (what changed since the previous version of this comment)

Two rounds. First, a high-effort review of #3's full diff vs main (i.e. including the #2 base) produced 10 findings; each was reproduced with a test before fixing. Fixed: eager-drain-under-held-locks (the biggest semantic gap vs threaded platforms — see the sync row above), signal disposition save/restore, the 64-fd poll cap, the signal-pending latch protocol, a 63-line main-queue-drain fork (now hoisted to a shared function with DISPATCH_COCOA_COMPAT-gated divergences), silent lock-contention spins (now named crashes), and test-runner robustness. Two findings were refuted by their own reproduction tests — most instructively, the timed-semaphore "dropped signal": the low-level early-timeout path exists, but upstream's _dispatch_semaphore_wait_slow re-checks the semaphore value on timeout and delivers the signal — pre-existing upstream defense-in-depth working as designed (details in #3's description). No pre-existing upstream libdispatch bug needed fixing.

Second, an adversarial probe round from the #2 side found one bypassed funnel in the poke-defer fix (dispatch_async_and_wait with a dispatch_block_create block — what Swift's asyncAndWait(execute:) produces — crashed with an internal diagnostic; fixed with the same bracket plus a four-shape regression test) and hardened three edges: the pipe-EOF hot loop became a named crash, close-while-armed got its named diagnostic on wasmtime's whole-call-EBADF shape, and the signal-restore path checks SIG_ERR. It also independently re-verified the "working as designed" semaphore analysis and the Darwin-identity of the main-queue-drain hoist, and added tests for two behaviors previously verified only by probe. 46/46 became 52/52, independently reproduced on both sides.

Bottom line

#1 is the minimal seed but breaks contracts on APIs it nominally supports. #2 made the supported set behave per spec and the unsupported set fail loudly; the review round found its one systemic gap — eager drains running client code beneath caller-held locks — plus a silent-spin path and the signal/cap issues, all inherited by and now fixed in #3. #3 is #2's design carried to completion: fd and signal sources work, the re-entrancy divergence from threaded platforms is confined to documented, pinned-by-test semantics, and every failure mode is a named crash. The remaining unsupported surface (process/vnode/memory-pressure/Mach, cross-process signals, true parallelism) is bounded by WASI itself.

🤖 Generated with Claude Code

scottmarchant pushed a commit that referenced this pull request Aug 14, 2026
Fold the best of the first WASI port candidate (PR #1) and the findings
from the side-by-side comparison of both candidates into the test suite:

- sync-inline.c, main-queue-order.c: inline dispatch_sync without a
  drain and thread-bound main-queue FIFO ordering, adapted from PR #1's
  dispatch_wasi_sync and dispatch_wasi_mainqueue tests.
- blocking-waits.c: pins the blocking-wait contracts that PR #1's
  trap-on-block design violated: dispatch_block_wait runs the queued
  block, dispatch_group_wait(FOREVER) returns once the group empties,
  and a timed semaphore wait consumes its full timeout instead of
  returning early.
- api-surface.c: one-binary sweep of the object/attr/block/data/group/
  source families, derived from the probe program used to compare the
  two candidates.
- assert-queue.c: dispatch_assert_queue must trap off-queue and pass
  on-queue. Guards the tid-vs-DLOCK_OWNER_MASK encoding in shims/lock.h;
  PR #1 shipped an unshifted constant tid that masked to DLOCK_OWNER_NULL,
  making assert_queue pass off-queue and assert_queue_not trap spuriously.

All 35 ctest cases pass under wasmtime/node with the Swift overlay
enabled (swift.org 6.3.3 toolchain + Swift 6.3.3 Wasm SDK).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@scottmarchant

Copy link
Copy Markdown
Collaborator Author

Closing: the two-port evaluation concluded in favor of #2's cooperative eager-drain runtime, and the combined branch (#3) carries that runtime plus this PR's test scenarios and consumer documentation. #3 is the single source of truth going forward; upstream-facing slices will be cut from it one at a time. See the comparison comment above for the full evaluation record.

scottmarchant pushed a commit that referenced this pull request Aug 17, 2026
Fold the best of the first WASI port candidate (PR #1) and the findings
from the side-by-side comparison of both candidates into the test suite:

- sync-inline.c, main-queue-order.c: inline dispatch_sync without a
  drain and thread-bound main-queue FIFO ordering, adapted from PR #1's
  dispatch_wasi_sync and dispatch_wasi_mainqueue tests.
- blocking-waits.c: pins the blocking-wait contracts that PR #1's
  trap-on-block design violated: dispatch_block_wait runs the queued
  block, dispatch_group_wait(FOREVER) returns once the group empties,
  and a timed semaphore wait consumes its full timeout instead of
  returning early.
- api-surface.c: one-binary sweep of the object/attr/block/data/group/
  source families, derived from the probe program used to compare the
  two candidates.
- assert-queue.c: dispatch_assert_queue must trap off-queue and pass
  on-queue. Guards the tid-vs-DLOCK_OWNER_MASK encoding in shims/lock.h;
  PR #1 shipped an unshifted constant tid that masked to DLOCK_OWNER_NULL,
  making assert_queue pass off-queue and assert_queue_not trap spuriously.

All 35 ctest cases pass under wasmtime/node with the Swift overlay
enabled (swift.org 6.3.3 toolchain + Swift 6.3.3 Wasm SDK).

Co-authored-by: Krzysztof Rodak <krodak.konta@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
scottmarchant added a commit that referenced this pull request Aug 17, 2026
Fold the best of the first WASI port candidate (PR #1) and the findings
from the side-by-side comparison of both candidates into the test suite:

- sync-inline.c, main-queue-order.c: inline dispatch_sync without a
  drain and thread-bound main-queue FIFO ordering, adapted from PR #1's
  dispatch_wasi_sync and dispatch_wasi_mainqueue tests.
- blocking-waits.c: pins the blocking-wait contracts that PR #1's
  trap-on-block design violated: dispatch_block_wait runs the queued
  block, dispatch_group_wait(FOREVER) returns once the group empties,
  and a timed semaphore wait consumes its full timeout instead of
  returning early.
- api-surface.c: one-binary sweep of the object/attr/block/data/group/
  source families, derived from the probe program used to compare the
  two candidates.
- assert-queue.c: dispatch_assert_queue must trap off-queue and pass
  on-queue. Guards the tid-vs-DLOCK_OWNER_MASK encoding in shims/lock.h;
  PR #1 shipped an unshifted constant tid that masked to DLOCK_OWNER_NULL,
  making assert_queue pass off-queue and assert_queue_not trap spuriously.

All 35 ctest cases pass under wasmtime/node with the Swift overlay
enabled (swift.org 6.3.3 toolchain + Swift 6.3.3 Wasm SDK).

Co-authored-by: Krzysztof Rodak <krodak.konta@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
scottmarchant added a commit that referenced this pull request Aug 17, 2026
Fold the best of the first WASI port candidate (PR #1) and the findings
from the side-by-side comparison of both candidates into the test suite:

- sync-inline.c, main-queue-order.c: inline dispatch_sync without a
  drain and thread-bound main-queue FIFO ordering, adapted from PR #1's
  dispatch_wasi_sync and dispatch_wasi_mainqueue tests.
- blocking-waits.c: pins the blocking-wait contracts that PR #1's
  trap-on-block design violated: dispatch_block_wait runs the queued
  block, dispatch_group_wait(FOREVER) returns once the group empties,
  and a timed semaphore wait consumes its full timeout instead of
  returning early.
- api-surface.c: one-binary sweep of the object/attr/block/data/group/
  source families, derived from the probe program used to compare the
  two candidates.
- assert-queue.c: dispatch_assert_queue must trap off-queue and pass
  on-queue. Guards the tid-vs-DLOCK_OWNER_MASK encoding in shims/lock.h;
  PR #1 shipped an unshifted constant tid that masked to DLOCK_OWNER_NULL,
  making assert_queue pass off-queue and assert_queue_not trap spuriously.

All 35 ctest cases pass under wasmtime/node with the Swift overlay
enabled (swift.org 6.3.3 toolchain + Swift 6.3.3 Wasm SDK).

Co-authored-by: Krzysztof Rodak <krodak.konta@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant