From dc08d3904035ea29f9381643026e116402abd937 Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Fri, 14 Aug 2026 08:33:49 +0200 Subject: [PATCH 1/4] Add cooperative libdispatch support for single-threaded WASI --- CMakeLists.txt | 52 ++++- cmake/toolchains/WASI.cmake | 104 ++++++++++ dispatch/CMakeLists.txt | 5 +- dispatch/dispatch.h | 5 +- dispatch/wasi/module.modulemap | 27 +++ os/object.h | 2 +- src/CMakeLists.txt | 18 ++ src/event/event_config.h | 10 +- src/event/event_internal.h | 13 ++ src/event/event_wasi.c | 349 +++++++++++++++++++++++++++++++++ src/init.c | 22 +++ src/internal.h | 24 ++- src/io.c | 7 +- src/queue.c | 211 +++++++++++++++++++- src/shims.h | 2 +- src/shims/getprogname.h | 3 + src/shims/hw_config.h | 4 +- src/shims/lock.c | 123 +++++++++++- src/shims/lock.h | 62 ++++++ src/shims/time.h | 4 + src/transform.c | 5 +- 21 files changed, 1020 insertions(+), 32 deletions(-) create mode 100644 cmake/toolchains/WASI.cmake create mode 100644 dispatch/wasi/module.modulemap create mode 100644 src/event/event_wasi.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a4e24097..0a5f8b9bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,6 +104,12 @@ set(CMAKE_THREAD_PREFER_PTHREAD TRUE) set(THREADS_PREFER_PTHREAD_FLAG TRUE) if(ANDROID) set(CMAKE_HAVE_LIBC_PTHREAD YES) +elseif(CMAKE_SYSTEM_NAME STREQUAL "WASI") + # wasi-libc provides pthread stubs directly. Static-library try_compile + # cannot prove that by linking, and -pthread selects the unsupported + # threaded ABI instead of the single-threaded wasip1 target. + set(CMAKE_HAVE_LIBC_PTHREAD YES CACHE BOOL "WASI pthread stubs are in libc") + set(THREADS_PREFER_PTHREAD_FLAG OFF) endif() find_package(Threads REQUIRED) @@ -127,16 +133,28 @@ include(EnableFramePointers) # NOTE(abdulras) this is the CMake supported way to control whether we generate # shared or static libraries. This impacts the behaviour of `add_library` in # what type of library it generates. -option(BUILD_SHARED_LIBS "build shared libraries" ON) +if(CMAKE_SYSTEM_NAME STREQUAL "WASI") + set(BUILD_SHARED_LIBS_DEFAULT OFF) +else() + set(BUILD_SHARED_LIBS_DEFAULT ON) +endif() +option(BUILD_SHARED_LIBS "build shared libraries" ${BUILD_SHARED_LIBS_DEFAULT}) option(DISPATCH_ENABLE_ASSERTS "enable debug assertions" FALSE) option(ENABLE_DISPATCH_INIT_CONSTRUCTOR "enable libdispatch_init as a constructor" ON) set(USE_LIBDISPATCH_INIT_CONSTRUCTOR ${ENABLE_DISPATCH_INIT_CONSTRUCTOR}) -option(ENABLE_DTRACE "enable dtrace support" "") +if(CMAKE_SYSTEM_NAME STREQUAL "WASI") + set(ENABLE_DTRACE_DEFAULT OFF) +else() + set(ENABLE_DTRACE_DEFAULT "") +endif() +option(ENABLE_DTRACE "enable dtrace support" "${ENABLE_DTRACE_DEFAULT}") -if(APPLE OR BSD) +if(CMAKE_SYSTEM_NAME STREQUAL "WASI") + set(ENABLE_INTERNAL_PTHREAD_WORKQUEUES_DEFAULT ON) +elseif(APPLE OR BSD) set(ENABLE_INTERNAL_PTHREAD_WORKQUEUES_DEFAULT OFF) else() set(ENABLE_INTERNAL_PTHREAD_WORKQUEUES_DEFAULT ON) @@ -166,6 +184,29 @@ endif() option(ENABLE_THREAD_LOCAL_STORAGE "enable usage of thread local storage via _Thread_local" ON) set(DISPATCH_USE_THREAD_LOCAL_STORAGE ${ENABLE_THREAD_LOCAL_STORAGE}) +if(CMAKE_SYSTEM_NAME STREQUAL "WASI") + # CMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY makes link-dependent checks + # compile-only. These values are the exported-symbol set of Swift 6.3.2's + # single-threaded wasip1 libc.a; cache entries remain caller-overridable. + set(HAVE__PTHREAD_WORKQUEUE_INIT 0 CACHE BOOL "WASI libc result") + set(HAVE_GETPROGNAME 0 CACHE BOOL "WASI libc result") + set(HAVE_MACH_ABSOLUTE_TIME 0 CACHE BOOL "WASI libc result") + set(HAVE_MACH_APPROXIMATE_TIME 0 CACHE BOOL "WASI libc result") + set(HAVE_MACH_PORT_CONSTRUCT 0 CACHE BOOL "WASI libc result") + set(HAVE_MALLOC_CREATE_ZONE 0 CACHE BOOL "WASI libc result") + set(HAVE_POSIX_FADVISE 1 CACHE BOOL "WASI libc result") + set(HAVE_POSIX_SPAWNP 0 CACHE BOOL "WASI libc result") + set(HAVE_PTHREAD_KEY_INIT_NP 0 CACHE BOOL "WASI libc result") + set(HAVE_PTHREAD_ATTR_SETCPUPERCENT_NP 0 CACHE BOOL "WASI libc result") + set(HAVE_PTHREAD_YIELD_NP 0 CACHE BOOL "WASI libc result") + set(HAVE_PTHREAD_MAIN_NP 0 CACHE BOOL "WASI libc result") + set(HAVE_PTHREAD_WORKQUEUE_SETDISPATCH_NP 0 CACHE BOOL "WASI libc result") + set(HAVE_STRLCPY 1 CACHE BOOL "WASI libc result") + set(HAVE_SYSCONF 1 CACHE BOOL "WASI libc result") + set(HAVE_ARC4RANDOM 1 CACHE BOOL "WASI libc result") + set(USE_POSIX_SEM 0 CACHE BOOL "WASI libc result") +endif() + check_linker_flag(C "LINKER:--build-id=sha1" LINKER_SUPPORTS_BUILD_ID) @@ -306,7 +347,10 @@ if(leaks_EXECUTABLE) endif() -if(APPLE) +if(CMAKE_SYSTEM_NAME STREQUAL "WASI") + add_compile_options($<$:-fmodule-map-file=${PROJECT_SOURCE_DIR}/dispatch/wasi/module.modulemap> + $<$:-fmodule-map-file=${PROJECT_SOURCE_DIR}/private/generic/module.modulemap>) +elseif(APPLE) add_compile_options($<:$:-fmodule-map-file=${PROJECT_SOURCE_DIR}/dispatch/darwin/module.modulemap> $<:$:-fmodule-map-file=${PROJECT_SOURCE_DIR}/private/darwin/module.modulemap>) else() diff --git a/cmake/toolchains/WASI.cmake b/cmake/toolchains/WASI.cmake new file mode 100644 index 000000000..ed2df157e --- /dev/null +++ b/cmake/toolchains/WASI.cmake @@ -0,0 +1,104 @@ +if(CMAKE_VERSION VERSION_LESS 3.31) + message(FATAL_ERROR + "WASI builds require CMake 3.31 or newer for CMAKE_SYSTEM_NAME=WASI support") +endif() + +set(CMAKE_SYSTEM_NAME WASI) +set(CMAKE_SYSTEM_PROCESSOR wasm32) + +set(SWIFT_WASI_TOOLCHAIN_PATH "${SWIFT_WASI_TOOLCHAIN_PATH}" CACHE PATH + "Host Swift .xctoolchain used to build for WASI") +set(SWIFT_WASI_SDK_PATH "${SWIFT_WASI_SDK_PATH}" CACHE PATH + "wasm32-unknown-wasip1 directory in a Swift WASI SDK") +set(SWIFT_WASI_STATIC_RESOURCES_OVERRIDE "" CACHE PATH + "Optional override for the Swift static resource directory") +set(DISPATCH_WASI_BUILTINS_OVERRIDE "" CACHE FILEPATH + "Optional override for the WASI compiler-rt builtins archive") +list(APPEND CMAKE_TRY_COMPILE_PLATFORM_VARIABLES + SWIFT_WASI_TOOLCHAIN_PATH + SWIFT_WASI_SDK_PATH + SWIFT_WASI_STATIC_RESOURCES_OVERRIDE + DISPATCH_WASI_BUILTINS_OVERRIDE) + +if(NOT SWIFT_WASI_TOOLCHAIN_PATH) + message(FATAL_ERROR "Set SWIFT_WASI_TOOLCHAIN_PATH to the host Swift .xctoolchain") +endif() +if(NOT SWIFT_WASI_SDK_PATH) + message(FATAL_ERROR "Set SWIFT_WASI_SDK_PATH to the wasm32-unknown-wasip1 SDK directory") +endif() + +if(NOT IS_DIRECTORY "${SWIFT_WASI_TOOLCHAIN_PATH}") + message(FATAL_ERROR "SWIFT_WASI_TOOLCHAIN_PATH is not a directory: ${SWIFT_WASI_TOOLCHAIN_PATH}") +endif() +if(NOT IS_DIRECTORY "${SWIFT_WASI_SDK_PATH}") + message(FATAL_ERROR "SWIFT_WASI_SDK_PATH is not a directory: ${SWIFT_WASI_SDK_PATH}") +endif() + +set(_dispatch_wasi_clang "${SWIFT_WASI_TOOLCHAIN_PATH}/usr/bin/clang") +set(_dispatch_wasi_clangxx "${SWIFT_WASI_TOOLCHAIN_PATH}/usr/bin/clang++") +set(_dispatch_wasi_ar "${SWIFT_WASI_TOOLCHAIN_PATH}/usr/bin/llvm-ar") +set(_dispatch_wasi_ranlib "${SWIFT_WASI_TOOLCHAIN_PATH}/usr/bin/llvm-ranlib") +set(_dispatch_wasi_swiftc "${SWIFT_WASI_TOOLCHAIN_PATH}/usr/bin/swiftc") +set(_dispatch_wasi_sysroot "${SWIFT_WASI_SDK_PATH}/WASI.sdk") + +foreach(_dispatch_wasi_tool IN ITEMS + "${_dispatch_wasi_clang}" + "${_dispatch_wasi_clangxx}" + "${_dispatch_wasi_ar}" + "${_dispatch_wasi_ranlib}") + if(NOT EXISTS "${_dispatch_wasi_tool}") + message(FATAL_ERROR "Required WASI build tool does not exist: ${_dispatch_wasi_tool}") + endif() +endforeach() +if(NOT IS_DIRECTORY "${_dispatch_wasi_sysroot}") + message(FATAL_ERROR "WASI sysroot does not exist: ${_dispatch_wasi_sysroot}") +endif() + +if(SWIFT_WASI_STATIC_RESOURCES_OVERRIDE) + set(SWIFT_WASI_STATIC_RESOURCES "${SWIFT_WASI_STATIC_RESOURCES_OVERRIDE}") +else() + set(SWIFT_WASI_STATIC_RESOURCES + "${SWIFT_WASI_SDK_PATH}/swift.xctoolchain/usr/lib/swift_static") +endif() +if(DISPATCH_WASI_BUILTINS_OVERRIDE) + set(DISPATCH_WASI_BUILTINS "${DISPATCH_WASI_BUILTINS_OVERRIDE}") +else() + set(DISPATCH_WASI_BUILTINS + "${SWIFT_WASI_SDK_PATH}/swift.xctoolchain/usr/lib/clang/lib/wasip1/libclang_rt.builtins-wasm32.a") +endif() +set(SWIFT_WASI_CLANG_RESOURCES + "${SWIFT_WASI_SDK_PATH}/swift.xctoolchain/usr/lib/clang") +if(NOT IS_DIRECTORY "${SWIFT_WASI_STATIC_RESOURCES}") + message(FATAL_ERROR + "Swift static resource directory does not exist: ${SWIFT_WASI_STATIC_RESOURCES}") +endif() +if(NOT EXISTS "${DISPATCH_WASI_BUILTINS}") + message(FATAL_ERROR "WASI builtins archive does not exist: ${DISPATCH_WASI_BUILTINS}") +endif() +if(NOT IS_DIRECTORY "${SWIFT_WASI_CLANG_RESOURCES}") + message(FATAL_ERROR + "WASI Clang resource directory does not exist: ${SWIFT_WASI_CLANG_RESOURCES}") +endif() + +set(CMAKE_C_COMPILER "${_dispatch_wasi_clang}") +set(CMAKE_CXX_COMPILER "${_dispatch_wasi_clangxx}") +set(CMAKE_AR "${_dispatch_wasi_ar}") +set(CMAKE_RANLIB "${_dispatch_wasi_ranlib}") +set(CMAKE_C_COMPILER_TARGET wasm32-unknown-wasip1) +set(CMAKE_CXX_COMPILER_TARGET wasm32-unknown-wasip1) +set(CMAKE_SYSROOT "${_dispatch_wasi_sysroot}") +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) +set(CMAKE_EXECUTABLE_SUFFIX .wasm) + +if(ENABLE_SWIFT) + if(NOT EXISTS "${_dispatch_wasi_swiftc}") + message(FATAL_ERROR "Swift compiler does not exist: ${_dispatch_wasi_swiftc}") + endif() + set(CMAKE_Swift_COMPILER "${_dispatch_wasi_swiftc}") + set(CMAKE_Swift_COMPILER_TARGET wasm32-unknown-wasip1) + set(CMAKE_Swift_FLAGS + "-sdk \"${CMAKE_SYSROOT}\" -resource-dir \"${SWIFT_WASI_STATIC_RESOURCES}\"") + set(dispatch_MODULE_TRIPLE wasm32-unknown-wasip1 CACHE STRING "Swift module triple") + set(dispatch_ARCH wasm32 CACHE STRING "Swift architecture") + set(dispatch_PLATFORM wasi CACHE STRING "Swift platform") +endif() diff --git a/dispatch/CMakeLists.txt b/dispatch/CMakeLists.txt index 478ee8fba..66e79d5e1 100644 --- a/dispatch/CMakeLists.txt +++ b/dispatch/CMakeLists.txt @@ -1,5 +1,7 @@ -if(APPLE) +if(CMAKE_SYSTEM_NAME STREQUAL "WASI") + set(DISPATCH_MODULE_MAP ${PROJECT_SOURCE_DIR}/dispatch/wasi/module.modulemap) +elseif(APPLE) set(DISPATCH_MODULE_MAP ${PROJECT_SOURCE_DIR}/dispatch/darwin/module.modulemap) else() set(DISPATCH_MODULE_MAP ${PROJECT_SOURCE_DIR}/dispatch/generic/module.modulemap) @@ -30,4 +32,3 @@ if(ENABLE_SWIFT) DESTINATION "${INSTALL_DISPATCH_HEADERS_DIR}") endif() - diff --git a/dispatch/dispatch.h b/dispatch/dispatch.h index ef65e38c2..fc39a25c5 100644 --- a/dispatch/dispatch.h +++ b/dispatch/dispatch.h @@ -28,7 +28,7 @@ #include #elif defined(_WIN32) #include -#elif defined(__unix__) +#elif defined(__unix__) || defined(__wasi__) #include #endif @@ -38,7 +38,8 @@ #include #include #include -#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) +#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) || \ + defined(__wasi__) #include #endif #include diff --git a/dispatch/wasi/module.modulemap b/dispatch/wasi/module.modulemap new file mode 100644 index 000000000..d59e27f48 --- /dev/null +++ b/dispatch/wasi/module.modulemap @@ -0,0 +1,27 @@ +module Dispatch { + requires blocks + export * + link "dispatch" + link "BlocksRuntime" + link "wasi-emulated-signal" + link "wasi-emulated-mman" + link "wasi-emulated-getpid" +} + +module DispatchIntrospection [system] [extern_c] { + header "introspection.h" + export * +} + +module CDispatch [system] [extern_c] { + umbrella header "dispatch.h" + export * + requires blocks + link "dispatch" + // Static WASI clients need explicit BlocksRuntime autolinking. Keep this + // in the WASI module map so generic Linux and Windows clients are unchanged. + link "BlocksRuntime" + link "wasi-emulated-signal" + link "wasi-emulated-mman" + link "wasi-emulated-getpid" +} diff --git a/os/object.h b/os/object.h index 1ad1158c5..869269ecd 100644 --- a/os/object.h +++ b/os/object.h @@ -28,7 +28,7 @@ #include #elif defined(_WIN32) #include -#elif defined(__unix__) +#elif defined(__unix__) || defined(__wasi__) #include #endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c10583054..50338ea7d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -38,6 +38,7 @@ add_library(dispatch event/event_epoll.c event/event_internal.h event/event_kevent.c + event/event_wasi.c event/event_windows.c firehose/firehose_internal.h shims/android_stubs.h @@ -105,6 +106,17 @@ if(WIN32) elseif(ANDROID) target_compile_options(dispatch PRIVATE -U_GNU_SOURCE) +elseif(CMAKE_SYSTEM_NAME STREQUAL "WASI") + target_compile_definitions(dispatch PRIVATE + DISPATCH_HW_CONFIG_UP=1) + target_compile_definitions(dispatch PUBLIC + "$<$:_WASI_EMULATED_SIGNAL>" + "$<$:_WASI_EMULATED_MMAN>" + "$<$:_WASI_EMULATED_GETPID>") + target_compile_options(dispatch INTERFACE + "$<$:SHELL:-Xcc -D_WASI_EMULATED_SIGNAL>" + "$<$:SHELL:-Xcc -D_WASI_EMULATED_MMAN>" + "$<$:SHELL:-Xcc -D_WASI_EMULATED_GETPID>") endif() if(DISPATCH_ENABLE_ASSERTS) target_compile_definitions(dispatch PRIVATE @@ -152,6 +164,12 @@ target_link_libraries(dispatch PRIVATE Threads::Threads) target_link_libraries(dispatch PUBLIC BlocksRuntime::BlocksRuntime) +if(CMAKE_SYSTEM_NAME STREQUAL "WASI") + target_link_libraries(dispatch PUBLIC + wasi-emulated-signal + wasi-emulated-mman + wasi-emulated-getpid) +endif() if(WIN32) target_link_libraries(dispatch PRIVATE AdvAPI32 diff --git a/src/event/event_config.h b/src/event/event_config.h index fac801256..32d43707d 100644 --- a/src/event/event_config.h +++ b/src/event/event_config.h @@ -21,19 +21,27 @@ #ifndef __DISPATCH_EVENT_EVENT_CONFIG__ #define __DISPATCH_EVENT_EVENT_CONFIG__ -#if defined(__linux__) +#if defined(__wasi__) +# define DISPATCH_EVENT_BACKEND_EPOLL 0 +# define DISPATCH_EVENT_BACKEND_KEVENT 0 +# define DISPATCH_EVENT_BACKEND_WASI 1 +# define DISPATCH_EVENT_BACKEND_WINDOWS 0 +#elif defined(__linux__) # include # define DISPATCH_EVENT_BACKEND_EPOLL 1 # define DISPATCH_EVENT_BACKEND_KEVENT 0 +# define DISPATCH_EVENT_BACKEND_WASI 0 # define DISPATCH_EVENT_BACKEND_WINDOWS 0 #elif __has_include() # include # define DISPATCH_EVENT_BACKEND_EPOLL 0 # define DISPATCH_EVENT_BACKEND_KEVENT 1 +# define DISPATCH_EVENT_BACKEND_WASI 0 # define DISPATCH_EVENT_BACKEND_WINDOWS 0 #elif defined(_WIN32) # define DISPATCH_EVENT_BACKEND_EPOLL 0 # define DISPATCH_EVENT_BACKEND_KEVENT 0 +# define DISPATCH_EVENT_BACKEND_WASI 0 # define DISPATCH_EVENT_BACKEND_WINDOWS 1 #else # error unsupported event loop diff --git a/src/event/event_internal.h b/src/event/event_internal.h index 5b2c7fc80..27279a5f4 100644 --- a/src/event/event_internal.h +++ b/src/event/event_internal.h @@ -699,6 +699,19 @@ void _dispatch_event_loop_timer_delete(dispatch_timer_heap_t dth, uint32_t tidx) void _dispatch_event_loop_drain_timers(dispatch_timer_heap_t dth, uint32_t count); +#if DISPATCH_EVENT_BACKEND_WASI +// Cooperative drain for single-threaded WASI. The pending-work bookkeeping +// and the drain loop live in event_wasi.c; the actual queue draining is +// implemented in queue.c (it needs the static drain machinery there). +void _dispatch_wasi_drain(void); +void _dispatch_wasi_root_queue_poke(dispatch_queue_global_t dq); +void _dispatch_wasi_main_queue_poke(void); +// implemented in queue.c on behalf of the WASI event backend: +void _dispatch_wasi_root_queue_drain(dispatch_queue_global_t dq); +void _dispatch_wasi_mgr_queue_drain(void); +void _dispatch_wasi_main_queue_drain(void); +#endif // DISPATCH_EVENT_BACKEND_WASI + DISPATCH_ALWAYS_INLINE static inline void _dispatch_timers_heap_dirty(dispatch_timer_heap_t dth, uint32_t tidx) diff --git a/src/event/event_wasi.c b/src/event/event_wasi.c new file mode 100644 index 000000000..a9ea0acdf --- /dev/null +++ b/src/event/event_wasi.c @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2026 Apple Inc. All rights reserved. + * + * @APPLE_APACHE_LICENSE_HEADER_START@ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @APPLE_APACHE_LICENSE_HEADER_END@ + */ + +#include "internal.h" +#if DISPATCH_EVENT_BACKEND_WASI + +#include + +#if !DISPATCH_USE_MGR_THREAD +#error unsupported configuration +#endif + +// wasm32-wasip1-threads compiles with the atomics feature (and -pthread +// additionally defines _REENTRANT); plain wasip1 defines neither +#if defined(__wasm_atomics__) || defined(_REENTRANT) +#error the WASI event backend assumes a single-threaded (wasip1) target +#endif + +// WASI (wasm32-unknown-wasip1) is single-threaded: there is no manager +// thread, no worker thread pool, and no blocking wait primitive. This +// backend implements a cooperative drain instead: pokes record pending work +// (root queues, the main queue, the manager queue) and the sole thread +// drains it eagerly, either right away when it is not already draining, or +// from the blocking-wait loops in shims/lock.c and from dispatch_main(). +// One drain step runs, in priority order: due timers (they unblock waits +// and re-fill the queues), then the manager queue (it arms timers and +// finishes source setup), then the thread-bound main queue, then one pending +// root item. A new root scan starts at the highest QoS, then rotates among +// roots that remain pending so one queue cannot hold the sole thread. +// +// Timers keep their generic heap; this backend only tracks the nearest +// armed deadline per clock so that idle waits can sleep until a timer is +// due and fire it (see _dispatch_wasi_drain_one). + +struct _dispatch_wasi_timeout_s { + uint64_t dwt_deadline; // in the _dispatch_uptime() clock domain + bool dwt_armed; +}; + +static struct _dispatch_wasi_timeout_s _dispatch_wasi_timeout[DISPATCH_CLOCK_COUNT]; + +static bool _dispatch_wasi_root_pending[DISPATCH_ROOT_QUEUE_COUNT]; +static size_t _dispatch_wasi_next_root = DISPATCH_ROOT_QUEUE_COUNT - 1; +static bool _dispatch_wasi_main_pending; +static bool _dispatch_wasi_mgr_pending; +static bool _dispatch_wasi_draining; + +#pragma mark dispatch_unote_t + +bool +_dispatch_unote_register_muxed(dispatch_unote_t du) +{ + // File-descriptor and signal dispatch sources are unsupported on + // single-threaded WASI. Returning false would merely unregister the + // unote silently (_dispatch_source_refs_finalize_unregistration), so + // fail loudly instead. + DISPATCH_CLIENT_CRASH(du._du->du_filter, + "file-descriptor and signal dispatch sources are " + "unsupported on single-threaded WASI"); +} + +void +_dispatch_unote_resume_muxed(dispatch_unote_t du DISPATCH_UNUSED) +{ + // never reached: registration crashes +} + +bool +_dispatch_unote_unregister_muxed(dispatch_unote_t du DISPATCH_UNUSED) +{ + // never reached: registration crashes + return true; +} + +#pragma mark timers + +static void +_dispatch_event_merge_timer(dispatch_clock_t clock) +{ + dispatch_timer_heap_t dth = _dispatch_timers_heap; + uint32_t tidx = DISPATCH_TIMER_INDEX(clock, 0); + + _dispatch_wasi_timeout[clock].dwt_armed = false; + + _dispatch_timers_heap_dirty(dth, tidx); + dth[tidx].dth_needs_program = true; + dth[tidx].dth_armed = false; +} + +void +_dispatch_event_loop_timer_arm(dispatch_timer_heap_t dth DISPATCH_UNUSED, + uint32_t tidx, dispatch_timer_delay_s range, + dispatch_clock_now_cache_t nows) +{ + dispatch_clock_t clock = DISPATCH_TIMER_CLOCK(tidx); + + // range.delay is relative to "now" in the timer's own clock domain; all + // clocks advance in nanoseconds, so anchoring the deadline on the uptime + // clock keeps a single sleepable domain for _dispatch_wasi_next_timer_ns() + _dispatch_wasi_timeout[clock].dwt_deadline = range.delay + + _dispatch_time_now_cached(DISPATCH_CLOCK_UPTIME, nows); + _dispatch_wasi_timeout[clock].dwt_armed = true; +} + +void +_dispatch_event_loop_timer_delete(dispatch_timer_heap_t dth DISPATCH_UNUSED, + uint32_t tidx) +{ + _dispatch_wasi_timeout[DISPATCH_TIMER_CLOCK(tidx)].dwt_armed = false; +} + +uint64_t +_dispatch_wasi_next_timer_ns(void) +{ + uint64_t next = 0; + for (size_t i = 0; i < countof(_dispatch_wasi_timeout); i++) { + if (!_dispatch_wasi_timeout[i].dwt_armed) continue; + uint64_t deadline = _dispatch_wasi_timeout[i].dwt_deadline; + if (!next || deadline < next) next = deadline; + } + return next; +} + +DISPATCH_ALWAYS_INLINE +static inline bool +_dispatch_wasi_merge_due_timers(void) +{ + uint64_t now = _dispatch_uptime(); + bool fired = false; + for (size_t i = 0; i < countof(_dispatch_wasi_timeout); i++) { + if (_dispatch_wasi_timeout[i].dwt_armed && + _dispatch_wasi_timeout[i].dwt_deadline <= now) { + _dispatch_event_merge_timer((dispatch_clock_t)i); + fired = true; + } + } + return fired; +} + +#pragma mark sleeping + +void +_dispatch_wasi_sleep_until(uint64_t uptime_ns) +{ + uint64_t now = _dispatch_uptime(); + if (uptime_ns <= now) return; + uint64_t delta = uptime_ns - now; + struct timespec ts = { + .tv_sec = (time_t)(delta / NSEC_PER_SEC), + .tv_nsec = (long)(delta % NSEC_PER_SEC), + }; + while (nanosleep(&ts, &ts) == -1 && errno == EINTR) { + } +} + +void +_dispatch_wasi_sleep_briefly_or_until(uint64_t deadline_uptime_ns) +{ + uint64_t next_timer = _dispatch_wasi_next_timer_ns(); + if (next_timer && next_timer < deadline_uptime_ns) { + deadline_uptime_ns = next_timer; + } + _dispatch_wasi_sleep_until(deadline_uptime_ns); +} + +#pragma mark cooperative drain + +bool +_dispatch_wasi_drain_one(void) +{ + if (_dispatch_wasi_draining) { + // no nested drains: the queue state machinery (thread frames, wlh, + // dq_state drain locks) is not reentrant on the sole thread. The + // outer drain picks deferred work up. + return false; + } + _dispatch_wasi_draining = true; + bool did_work = true; + if (_dispatch_wasi_merge_due_timers() || + _dispatch_timers_heap[0].dth_dirty_bits) { + // a due timer counts as a pending item (see shims/lock.h): firing it + // pushes the timer source's handler onto its target queue + _dispatch_event_loop_drain_timers(_dispatch_timers_heap, + DISPATCH_TIMER_COUNT); + } else if (_dispatch_wasi_mgr_pending) { + _dispatch_wasi_mgr_pending = false; + _dispatch_wasi_mgr_queue_drain(); + } else if (_dispatch_wasi_main_pending) { + _dispatch_wasi_main_pending = false; + _dispatch_wasi_main_queue_drain(); + } else { + did_work = false; + // Start at the highest QoS, then continue below the root queue that + // last ran so a self-replenishing queue cannot starve the others. + for (size_t offset = 0; + offset < countof(_dispatch_wasi_root_pending); offset++) { + size_t i = (_dispatch_wasi_next_root + + countof(_dispatch_wasi_root_pending) - offset) % + countof(_dispatch_wasi_root_pending); + if (!_dispatch_wasi_root_pending[i]) continue; + bool roots_were_waiting = false; + for (size_t j = 0; + j < countof(_dispatch_wasi_root_pending); j++) { + roots_were_waiting |= j != i && + _dispatch_wasi_root_pending[j]; + } + _dispatch_wasi_root_pending[i] = false; + _dispatch_wasi_root_queue_drain(&_dispatch_root_queues[i]); + bool keep_rotating = roots_were_waiting || + _dispatch_wasi_root_pending[i]; + _dispatch_wasi_next_root = keep_rotating && i ? i - 1 : + countof(_dispatch_wasi_root_pending) - 1; + did_work = true; + break; + } + } + _dispatch_wasi_draining = false; + return did_work; +} + +void +_dispatch_wasi_drain(void) +{ + while (_dispatch_wasi_drain_one()) { + } +} + +bool +_dispatch_wasi_in_drain(void) +{ + return _dispatch_wasi_draining; +} + +void +_dispatch_wasi_root_queue_poke(dispatch_queue_global_t dq) +{ + size_t idx = (size_t)(dq - _dispatch_root_queues); + if (unlikely(idx >= DISPATCH_ROOT_QUEUE_COUNT)) { + DISPATCH_INTERNAL_CRASH(dq, "Poke of a non-global root queue on WASI"); + } + _dispatch_wasi_root_pending[idx] = true; + if (!_dispatch_wasi_draining) { + _dispatch_wasi_drain(); + } +} + +void +_dispatch_wasi_main_queue_poke(void) +{ + _dispatch_wasi_main_pending = true; + if (!_dispatch_wasi_draining) { + _dispatch_wasi_drain(); + } +} + +#pragma mark dispatch_loop + +void +_dispatch_event_loop_atfork_child(void) +{ +} + +void +_dispatch_event_loop_poke(dispatch_wlh_t wlh, + uint64_t dq_state DISPATCH_UNUSED, uint32_t flags DISPATCH_UNUSED) +{ + if (wlh == DISPATCH_WLH_MANAGER) { + _dispatch_wasi_mgr_pending = true; + if (!_dispatch_wasi_draining) { + _dispatch_wasi_drain(); + } + return; + } + // every poke caller compiled outside DISPATCH_USE_KEVENT_WORKLOOP + // passes DISPATCH_WLH_MANAGER + DISPATCH_INTERNAL_CRASH((uintptr_t)wlh, + "unexpected non-manager event loop poke on WASI"); +} + +DISPATCH_NOINLINE +void +_dispatch_event_loop_drain(uint32_t flags DISPATCH_UNUSED) +{ + // only reachable from the manager thread loop, which never runs on + // single-threaded WASI (the manager queue is drained cooperatively by + // _dispatch_wasi_mgr_queue_drain instead) + DISPATCH_INTERNAL_CRASH(0, "manager event loop cannot run on WASI"); +} + +void +_dispatch_event_loop_cancel_waiter(dispatch_sync_context_t dsc) +{ + (void)dsc; +} + +void +_dispatch_event_loop_wake_owner(dispatch_sync_context_t dsc, + dispatch_wlh_t wlh, uint64_t old_state, uint64_t new_state) +{ + (void)dsc; (void)wlh; (void)old_state; (void)new_state; +} + +void +_dispatch_event_loop_wait_for_ownership(dispatch_sync_context_t dsc) +{ + if (dsc->dsc_release_storage) { + _dispatch_queue_release_storage(dsc->dc_data); + } +} + +void +_dispatch_event_loop_end_ownership(dispatch_wlh_t wlh, uint64_t old_state, + uint64_t new_state, uint32_t flags) +{ + (void)wlh; (void)old_state; (void)new_state; (void)flags; +} + +#if DISPATCH_WLH_DEBUG +void +_dispatch_event_loop_assert_not_owned(dispatch_wlh_t wlh) +{ + (void)wlh; +} +#endif + +void +_dispatch_event_loop_leave_immediate(uint64_t dq_state) +{ + (void)dq_state; +} + +#endif // DISPATCH_EVENT_BACKEND_WASI diff --git a/src/init.c b/src/init.c index b7364fea3..26f2f5a50 100644 --- a/src/init.c +++ b/src/init.c @@ -87,6 +87,7 @@ dispatch_atfork_child(void) _dispatch_unsafe_fork = 0; } +#if !defined(__wasi__) int _dispatch_sigmask(void) { @@ -109,6 +110,7 @@ _dispatch_sigmask(void) r |= pthread_sigmask(SIG_BLOCK, &mask, NULL); return dispatch_assume_zero(r); } +#endif // !defined(__wasi__) #endif #pragma mark - @@ -886,12 +888,18 @@ _dispatch_get_build(void) return _dispatch_build; } +#if defined(__wasi__) +// WebAssembly has not implemented __builtin_return_address; losing the +// repeated-log suppression only makes bug logs noisier +#define _dispatch_bug_log_is_repeated() false +#else #define _dispatch_bug_log_is_repeated() ({ \ static void *last_seen; \ void *previous = last_seen; \ last_seen =__builtin_return_address(0); \ last_seen == previous; \ }) +#endif #if HAVE_OS_FAULT_WITH_PAYLOAD __attribute__((__format__(__printf__,2,3))) @@ -1251,6 +1259,20 @@ _dispatch_vsyslog(const char *msg, va_list ap) free(buffer); } +#elif defined(__wasi__) +static inline void +_dispatch_syslog(const char *msg) +{ + // WASI has no syslog; log to stderr + fprintf(stderr, "%s\n", msg); +} + +static inline void +_dispatch_vsyslog(const char *msg, va_list ap) +{ + vfprintf(stderr, msg, ap); + fputc('\n', stderr); +} #else // DISPATCH_USE_SIMPLE_ASL static inline void _dispatch_syslog(const char *msg) diff --git a/src/internal.h b/src/internal.h index a67a771dc..690c2579c 100644 --- a/src/internal.h +++ b/src/internal.h @@ -269,11 +269,13 @@ upcast(dispatch_object_t dou) #if defined(_WIN32) #include #else +#if !defined(__wasi__) #include +#endif #ifdef __ANDROID__ #include #endif /* __ANDROID__ */ -#if !defined(__linux__) +#if !defined(__linux__) && !defined(__wasi__) #include #include #endif @@ -312,7 +314,8 @@ upcast(dispatch_object_t dou) #include #include #include -#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) +#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) || \ + defined(__wasi__) #include #endif #if defined(_WIN32) @@ -506,7 +509,8 @@ void _dispatch_abort(size_t line, uintptr_t val); #endif #endif // DISPATCH_USE_SIMPLE_ASL -#if !DISPATCH_USE_SIMPLE_ASL && !DISPATCH_USE_OS_DEBUG_LOG && !defined(_WIN32) +#if !DISPATCH_USE_SIMPLE_ASL && !DISPATCH_USE_OS_DEBUG_LOG && \ + !defined(_WIN32) && !defined(__wasi__) #include #endif @@ -632,7 +636,7 @@ void *_dispatch_calloc(size_t num_items, size_t size); const char *_dispatch_strdup_if_mutable(const char *str); void _dispatch_vtable_init(void); char *_dispatch_get_build(void); -#if !defined(_WIN32) +#if !defined(_WIN32) && !defined(__wasi__) int _dispatch_sigmask(void); #endif @@ -994,11 +998,19 @@ _dispatch_ktrace_impl(uint32_t code, uint64_t a, uint64_t b, #define _dispatch_set_crash_log_message(msg) \ _dispatch_set_crash_log_message_dynamic((msg)) #define _dispatch_set_crash_log_message_dynamic(msg) _RPTF0(_CRT_ASSERT, (msg)) -#else // _WIN32 +#elif defined(__wasi__) +// a wasm trap carries no crash-log payload: print the reason before +// trapping so crashes stay diagnosable under WASI runtimes +#define _dispatch_set_crash_log_cause_and_message(ac, msg) \ + _dispatch_log("%s (cause: 0x%llx)", (msg), (unsigned long long)(ac)) +#define _dispatch_set_crash_log_message(msg) _dispatch_log("%s", (msg)) +#define _dispatch_set_crash_log_message_dynamic(msg) \ + _dispatch_log("%s", (msg)) +#else #define _dispatch_set_crash_log_cause_and_message(ac, msg) ((void)(ac)) #define _dispatch_set_crash_log_message(msg) #define _dispatch_set_crash_log_message_dynamic(msg) -#endif // _WIN32 +#endif // _WIN32 / __wasi__ #if HAVE_MACH // MIG_REPLY_MISMATCH means either: diff --git a/src/io.c b/src/io.c index 7ae1e0efe..31de565ac 100644 --- a/src/io.c +++ b/src/io.c @@ -1534,7 +1534,12 @@ _dispatch_fd_entry_create_with_fd(dispatch_fd_t fd, uintptr_t hash) break; ); } +#if defined(__wasi__) + // WASI has no notion of device majors + dev_t dev = 0; +#else dev_t dev = (dev_t)major(st.st_dev); +#endif // We have to get the disk on the global dev queue. The // barrier queue cannot continue until that is complete dispatch_suspend(fd_entry->barrier_queue); @@ -1628,7 +1633,7 @@ _dispatch_fd_entry_create_with_path(dispatch_io_path_data_t path_data, _dispatch_fd_entry_debug("create: path %s", fd_entry, path_data->path); #endif if (S_ISREG(mode)) { -#if defined(_WIN32) +#if defined(_WIN32) || defined(__wasi__) _dispatch_disk_init(fd_entry, 0); #else _dispatch_disk_init(fd_entry, (dev_t)major(dev)); diff --git a/src/queue.c b/src/queue.c index 813e3fa47..f901fa818 100644 --- a/src/queue.c +++ b/src/queue.c @@ -5355,7 +5355,7 @@ _dispatch_queue_mgr_lock(struct dispatch_queue_static_s *dq) }); } -#if DISPATCH_USE_KEVENT_WORKQUEUE +#if DISPATCH_USE_KEVENT_WORKQUEUE || defined(__wasi__) DISPATCH_ALWAYS_INLINE static inline bool _dispatch_queue_mgr_unlock(struct dispatch_queue_static_s *dq) @@ -5368,7 +5368,7 @@ _dispatch_queue_mgr_unlock(struct dispatch_queue_static_s *dq) }); return _dq_state_is_dirty(old_state); } -#endif // DISPATCH_USE_KEVENT_WORKQUEUE +#endif // DISPATCH_USE_KEVENT_WORKQUEUE || defined(__wasi__) static void _dispatch_mgr_queue_drain(void) @@ -5398,6 +5398,28 @@ _dispatch_mgr_queue_drain(void) } } +#if defined(__wasi__) +DISPATCH_NOINLINE +void +_dispatch_wasi_mgr_queue_drain(void) +{ + // Single-threaded WASI has no manager thread: drain the manager queue + // inline the way the kevent workqueue manager drain does (see + // _dispatch_wlh_worker_thread_init/_reset), saving and restoring the + // current queue because the sole thread is not a pristine worker. + dispatch_queue_t old_dq = _dispatch_queue_get_current(); + _dispatch_queue_set_current(&_dispatch_mgr_q); + _dispatch_queue_mgr_lock(&_dispatch_mgr_q); + _dispatch_mgr_queue_drain(); + bool needs_poll = _dispatch_queue_mgr_unlock(&_dispatch_mgr_q); + _dispatch_queue_set_current(old_dq); + if (needs_poll) { + _dispatch_trace_runtime_event(worker_request, &_dispatch_mgr_q, 1); + _dispatch_event_loop_poke(DISPATCH_WLH_MANAGER, 0, 0); + } +} +#endif // defined(__wasi__) + void _dispatch_mgr_queue_push(dispatch_lane_t dq, dispatch_object_t dou, DISPATCH_UNUSED dispatch_qos_t qos) @@ -5680,12 +5702,12 @@ _dispatch_workloop_worker_thread(uint64_t *workloop_id, #pragma mark - #pragma mark dispatch_root_queue -#if DISPATCH_USE_PTHREAD_POOL +#if DISPATCH_USE_PTHREAD_POOL && !defined(__wasi__) static void *_dispatch_worker_thread(void *context); #if defined(_WIN32) static unsigned WINAPI _dispatch_worker_thread_thunk(LPVOID lpParameter); #endif -#endif // DISPATCH_USE_PTHREAD_POOL +#endif // DISPATCH_USE_PTHREAD_POOL && !defined(__wasi__) #if DISPATCH_DEBUG && DISPATCH_ROOT_QUEUE_DEBUG #define _dispatch_root_queue_debug(...) _dispatch_debug(__VA_ARGS__) @@ -5707,6 +5729,31 @@ DISPATCH_NOINLINE static void _dispatch_root_queue_poke_slow(dispatch_queue_global_t dq, int n, int floor) { +#if defined(__wasi__) + // Single-threaded WASI: there is no thread to create. Record a single + // pending "worker" in dgq_pending (consumed by the matching decrement in + // _dispatch_wasi_root_queue_drain(), mirroring _dispatch_worker_thread2) + // and drain cooperatively unless a drain is already running, in which + // case the outer drain picks the work up. + // + // The 0 -> 1 cmpxchg below is the only dgq_pending increment on this + // path: the fast-path cmpxchg in _dispatch_root_queue_poke() must be + // compiled out (it is only built when !DISPATCH_USE_INTERNAL_WORKQUEUE), + // or dgq_pending would be raised twice per poke and never return to 0. +#if !DISPATCH_USE_INTERNAL_WORKQUEUE +#error the WASI cooperative drain requires DISPATCH_USE_INTERNAL_WORKQUEUE +#endif + (void)floor; + _dispatch_root_queues_init(); + _dispatch_debug_root_queue(dq, __func__); + _dispatch_trace_runtime_event(worker_request, dq, (uint64_t)n); + if (!os_atomic_cmpxchg2o(dq, dgq_pending, 0, 1, relaxed)) { + _dispatch_root_queue_debug("worker thread request still pending for " + "global queue: %p", dq); + return; + } + _dispatch_wasi_root_queue_poke(dq); +#else // defined(__wasi__) int remaining = n; #if !defined(_WIN32) int r = ENOSYS; @@ -5814,6 +5861,7 @@ _dispatch_root_queue_poke_slow(dispatch_queue_global_t dq, int n, int floor) #else (void)floor; #endif // DISPATCH_USE_PTHREAD_POOL +#endif // defined(__wasi__) } DISPATCH_NOINLINE @@ -6113,6 +6161,7 @@ _dispatch_root_queue_drain_deferred_item(dispatch_deferred_items_t ddi } #endif +#if !defined(__wasi__) DISPATCH_NOT_TAIL_CALLED // prevent tailcall (for Instrument DTrace probe) static void _dispatch_root_queue_drain(dispatch_queue_global_t dq, @@ -6159,6 +6208,46 @@ _dispatch_root_queue_drain(dispatch_queue_global_t dq, _dispatch_clear_basepri(); _dispatch_queue_set_current(NULL); } +#endif // !defined(__wasi__) + +#if defined(__wasi__) +DISPATCH_NOINLINE +void +_dispatch_wasi_root_queue_drain(dispatch_queue_global_t dq) +{ + // Cooperative stand-in for one worker turn. Limit the turn to one root + // item so event_wasi.c can rotate among pending roots between turns. + dispatch_queue_t old_dq = _dispatch_queue_get_current(); + _dispatch_queue_set_current(NULL); + int pending = os_atomic_dec2o(dq, dgq_pending, relaxed); + dispatch_assert(pending >= 0); + (void)pending; + + dispatch_priority_t pri = dq->dq_priority; + _dispatch_queue_set_current(dq); + _dispatch_init_basepri(pri); + _dispatch_adopt_wlh_anon(); + + dispatch_invoke_context_s dic = { }; + _dispatch_perfmon_start(); + struct dispatch_object_s *item = _dispatch_root_queue_drain_one(dq); + if (item) { + _dispatch_continuation_pop_inline(item, &dic, + DISPATCH_INVOKE_WORKER_DRAIN | + DISPATCH_INVOKE_REDIRECTING_DRAIN, dq); + (void)_dispatch_reset_basepri_override(); + } + + if (pri & DISPATCH_PRIORITY_FLAG_OVERCOMMIT) { + _dispatch_perfmon_end(perfmon_thread_worker_oc); + } else { + _dispatch_perfmon_end(perfmon_thread_worker_non_oc); + } + _dispatch_reset_wlh(); + _dispatch_clear_basepri(); + _dispatch_queue_set_current(old_dq); +} +#endif // defined(__wasi__) #if !DISPATCH_USE_INTERNAL_WORKQUEUE static void @@ -6217,6 +6306,7 @@ _dispatch_root_queue_init_pthread_pool(dispatch_queue_global_t dq, _dispatch_sema4_create(sema, _DSEMA4_POLICY_LIFO); } +#if !defined(__wasi__) // 6618342 Contact the team that owns the Instrument DTrace probe before // renaming this symbol static void * @@ -6238,7 +6328,7 @@ _dispatch_worker_thread(void *context) pqc->dpq_thread_configure(); } -#if !defined(_WIN32) +#if !defined(_WIN32) && !defined(__wasi__) // workaround tweaks the kernel workqueue does for us _dispatch_sigmask(); #endif @@ -6358,6 +6448,7 @@ _dispatch_worker_thread_thunk(LPVOID lpParameter) return 0; } #endif // defined(_WIN32) +#endif // !defined(__wasi__) #endif // DISPATCH_USE_PTHREAD_POOL DISPATCH_NOINLINE @@ -6546,7 +6637,7 @@ _dispatch_pthread_root_queue_dispose(dispatch_queue_global_t dq, #pragma mark - #pragma mark dispatch_runloop_queue -#ifndef __linux__ +#if !defined(__linux__) && !defined(__wasi__) DISPATCH_STATIC_GLOBAL(bool _dispatch_program_is_probably_callback_driven); #endif @@ -7105,6 +7196,70 @@ _dispatch_main_queue_push(dispatch_queue_main_t dq, dispatch_object_t dou, } } +#if defined(__wasi__) +void +_dispatch_wasi_main_queue_drain(void) +{ + // Keep in sync with the DISPATCH_COCOA_COMPAT _dispatch_main_queue_drain() + // above. It deliberately diverges in exactly two places: + // 1. no _dispatch_main_q_handle_pred/_dispatch_runloop_queue_handle_init + // once: WASI has no runloop handle (no eventfd/pipe) and the sole + // thread owns the thread-bound main queue's drain lock for the + // lifetime of the program, so no runloop needs waking; + // 2. no `qos != _dispatch_priority_qos(dq->dq_priority)` check with + // _dispatch_main_queue_update_priority_from_thread: without + // HAVE_PTHREAD_WORKQUEUE_QOS both sides are always 0 and the + // override machinery it calls is Darwin-only. + dispatch_queue_main_t dq = &_dispatch_main_q; + dispatch_thread_frame_s dtf; + + if (!dq->dq_items_tail) { + return; + } + + _dispatch_perfmon_start_notrace(); + if (unlikely(!_dispatch_queue_is_thread_bound(dq))) { + DISPATCH_CLIENT_CRASH(0, "_dispatch_wasi_main_queue_drain called" + " after dispatch_main()"); + } + uint64_t dq_state = os_atomic_load2o(dq, dq_state, relaxed); + if (unlikely(!_dq_state_drain_locked_by_self(dq_state))) { + DISPATCH_CLIENT_CRASH((uintptr_t)dq_state, + "_dispatch_wasi_main_queue_drain called" + " from the wrong thread"); + } + + _dispatch_adopt_wlh_anon(); + _dispatch_thread_frame_push_and_rebase(&dtf, dq, NULL); + + pthread_priority_t pp = _dispatch_get_priority(); + dispatch_priority_t pri = _dispatch_priority_from_pp(pp); + voucher_t voucher = _voucher_copy(); + + dispatch_priority_t old_dbp = _dispatch_set_basepri(pri); + _dispatch_set_basepri_override_qos(DISPATCH_QOS_SATURATED); + + dispatch_invoke_context_s dic = { }; + struct dispatch_object_s *dc, *next_dc, *tail; + dc = os_mpsc_capture_snapshot(os_mpsc(dq, dq_items), &tail); + do { + next_dc = os_mpsc_pop_snapshot_head(dc, tail, do_next); + _dispatch_continuation_pop_inline(dc, &dic, + DISPATCH_INVOKE_THREAD_BOUND, dq); + } while ((dc = next_dc)); + + dx_wakeup(dq->_as_dq, 0, 0); + _dispatch_voucher_debug("main queue restore", voucher); + _dispatch_reset_basepri(old_dbp); + _dispatch_reset_basepri_override(); + _dispatch_reset_priority_and_voucher(pp, voucher); + _dispatch_thread_frame_pop(&dtf); + _dispatch_reset_wlh(); + _dispatch_force_cache_cleanup(); + _dispatch_perfmon_end_notrace(); +} +#endif // defined(__wasi__) + void _dispatch_main_queue_wakeup(dispatch_queue_main_t dq, dispatch_qos_t qos, dispatch_wakeup_flags_t flags) @@ -7113,11 +7268,20 @@ _dispatch_main_queue_wakeup(dispatch_queue_main_t dq, dispatch_qos_t qos, if (_dispatch_queue_is_thread_bound(dq)) { return _dispatch_runloop_queue_wakeup(dq->_as_dl, qos, flags); } +#endif +#if defined(__wasi__) + if (_dispatch_queue_is_thread_bound(dq)) { + // nothing else can run the thread-bound main queue on + // single-threaded WASI: note it for the cooperative drain, after + // letting the lane wakeup do the regular dq_state maintenance + _dispatch_lane_wakeup(dq, qos, flags); + return _dispatch_wasi_main_queue_poke(); + } #endif return _dispatch_lane_wakeup(dq, qos, flags); } -#if !defined(_WIN32) +#if !defined(_WIN32) && !defined(__wasi__) DISPATCH_NOINLINE DISPATCH_NORETURN static void _dispatch_sigsuspend(void) @@ -7128,8 +7292,9 @@ _dispatch_sigsuspend(void) sigsuspend(&mask); } } -#endif // !defined(_WIN32) +#endif // !defined(_WIN32) && !defined(__wasi__) +#if !defined(__wasi__) DISPATCH_NORETURN static void _dispatch_sig_thread(void *ctxt DISPATCH_UNUSED) @@ -7143,11 +7308,29 @@ _dispatch_sig_thread(void *ctxt DISPATCH_UNUSED) _dispatch_sigsuspend(); #endif } +#endif // !defined(__wasi__) void dispatch_main(void) { _dispatch_root_queues_init(); +#if defined(__wasi__) + // Cooperative single-threaded WASI: there is no way to park the main + // thread while other threads do the work, so dispatch_main() itself + // becomes the drain loop. When the process is fully idle with no armed + // timer, waiting would hang forever: crash loudly instead. + _dispatch_object_debug(&_dispatch_main_q, "%s", __func__); + for (;;) { + _dispatch_wasi_drain(); + uint64_t deadline = _dispatch_wasi_next_timer_ns(); + if (deadline) { + _dispatch_wasi_sleep_until(deadline); + continue; + } + DISPATCH_CLIENT_CRASH(0, + "dispatch_main(): no runnable work on single-threaded WASI"); + } +#else // defined(__wasi__) #if HAVE_PTHREAD_MAIN_NP if (pthread_main_np()) { #endif @@ -7178,6 +7361,7 @@ dispatch_main(void) } DISPATCH_CLIENT_CRASH(0, "dispatch_main() must be called on the main thread"); #endif +#endif // defined(__wasi__) } DISPATCH_NOINLINE @@ -7209,7 +7393,7 @@ _dispatch_queue_cleanup2(void) // similar non-POSIX API was called // this has to run before the DISPATCH_COCOA_COMPAT below // See dispatch_main for call to _dispatch_sig_thread on linux. -#ifndef __linux__ +#if !defined(__linux__) && !defined(__wasi__) if (_dispatch_program_is_probably_callback_driven) { _dispatch_barrier_async_detached_f(_dispatch_get_default_queue(true), NULL, _dispatch_sig_thread); @@ -7447,6 +7631,15 @@ _gettid(void) { return GetCurrentThreadId(); } +#elif defined(__wasi__) +DISPATCH_ALWAYS_INLINE +static inline pid_t +_gettid(void) +{ + // WASI is single-threaded; any nonzero constant works as the sole + // thread's id (the value seeds tsd->tid for lock-owner encoding). + return 1; +} #else #error "SYS_gettid unavailable on this system" #endif /* SYS_gettid */ diff --git a/src/shims.h b/src/shims.h index a65052dd0..1788c3533 100644 --- a/src/shims.h +++ b/src/shims.h @@ -33,7 +33,7 @@ #include "shims/generic_win_stubs.h" #endif // defined(_WIN32) -#if defined(_WIN32) || defined(__linux__) +#if defined(_WIN32) || defined(__linux__) || defined(__wasi__) #include "shims/generic_sys_queue.h" #endif diff --git a/src/shims/getprogname.h b/src/shims/getprogname.h index a768eedd1..3b9c7b01c 100644 --- a/src/shims/getprogname.h +++ b/src/shims/getprogname.h @@ -41,6 +41,9 @@ getprogname(void) return program_invocation_short_name; # elif defined(__ANDROID__) return __progname; +# elif defined(__wasi__) + // wasi-libc provides no getprogname(3) + return (char *)"unknown"; # else # error getprogname(3) is not available on this platform # endif diff --git a/src/shims/hw_config.h b/src/shims/hw_config.h index 4e6f7c3c9..35ba7e72e 100644 --- a/src/shims/hw_config.h +++ b/src/shims/hw_config.h @@ -197,12 +197,12 @@ _dispatch_hw_get_config(_dispatch_hw_config_t c) } #elif defined(__FreeBSD__) (void)c; name = "kern.smp.cpus"; -#elif defined(__OpenBSD__) +#elif defined(__OpenBSD__) || defined(__wasi__) (void)c; #endif if (name) { size_t valsz = sizeof(val); -#if !defined(__OpenBSD__) +#if !defined(__OpenBSD__) && !defined(__wasi__) r = sysctlbyname(name, &val, &valsz, NULL, 0); (void)dispatch_assume_zero(r); #endif diff --git a/src/shims/lock.c b/src/shims/lock.c index 85e44544c..c08ccdacd 100644 --- a/src/shims/lock.c +++ b/src/shims/lock.c @@ -67,7 +67,7 @@ _dispatch_thread_switch(dispatch_lock value, dispatch_lock_options_t flags, sched_yield(); } #endif // HAVE_UL_UNFAIR_LOCK -#elif defined(__unix__) +#elif defined(__unix__) || defined(__wasi__) #if !HAVE_UL_UNFAIR_LOCK && !HAVE_FUTEX_PI DISPATCH_ALWAYS_INLINE static inline void @@ -337,6 +337,88 @@ _dispatch_sema4_timedwait(_dispatch_sema4_t *sema, dispatch_time_t timeout) _pop_timer_resolution(resolution); return wait_result == WAIT_TIMEOUT; } +#elif defined(__wasi__) +DISPATCH_ALWAYS_INLINE +static inline bool +_dispatch_sema4_try_consume(_dispatch_sema4_t *sema) +{ + uint32_t value = os_atomic_load(sema, relaxed); + while (value > 0) { + if (os_atomic_cmpxchgv(sema, value, value - 1, &value, acquire)) { + return true; + } + } + return false; +} + +void +_dispatch_sema4_dispose_slow(_dispatch_sema4_t *sema, int policy DISPATCH_UNUSED) +{ + *sema = 0; +} + +void +_dispatch_sema4_signal(_dispatch_sema4_t *sema, long count) +{ + (void)os_atomic_add(sema, (uint32_t)count, release); +} + +void +_dispatch_sema4_wait(_dispatch_sema4_t *sema) +{ + for (;;) { + if (_dispatch_sema4_try_consume(sema)) return; + if (_dispatch_wasi_in_drain()) { + // nested waits can never be satisfied: no other work or timer + // can run while a drained item blocks the sole thread + DISPATCH_CLIENT_CRASH(0, "single-threaded WASI deadlock: " + "semaphore wait from within a drained work item"); + } + if (_dispatch_wasi_drain_one()) continue; + uint64_t deadline = _dispatch_wasi_next_timer_ns(); + if (deadline) { + _dispatch_wasi_sleep_until(deadline); + continue; + } + DISPATCH_CLIENT_CRASH(0, "single-threaded WASI deadlock: " + "semaphore wait with no runnable work or pending timers"); + } +} + +bool +_dispatch_sema4_timedwait(_dispatch_sema4_t *sema, dispatch_time_t timeout) +{ + do { + if (_dispatch_sema4_try_consume(sema)) return false; + if (_dispatch_wasi_drain_one()) continue; + uint64_t nsec = _dispatch_timeout(timeout); + if (nsec == 0) break; + if (nsec == DISPATCH_TIME_FOREVER) { + if (_dispatch_wasi_in_drain()) { + // nested waits can never be satisfied: no other work or + // timer can run while a drained item blocks the sole + // thread + DISPATCH_CLIENT_CRASH(0, "single-threaded WASI deadlock: " + "semaphore timedwait from within a drained " + "work item"); + } + uint64_t deadline = _dispatch_wasi_next_timer_ns(); + if (!deadline) { + DISPATCH_CLIENT_CRASH(0, "single-threaded WASI deadlock: " + "semaphore timedwait with no runnable work or " + "pending timers"); + } + _dispatch_wasi_sleep_until(deadline); + } else if (_dispatch_wasi_in_drain()) { + // timers cannot merge while nested: sleeping toward one would + // spin at its deadline; honor only the wait's own deadline + _dispatch_wasi_sleep_until(_dispatch_uptime() + nsec); + } else { + _dispatch_wasi_sleep_briefly_or_until(_dispatch_uptime() + nsec); + } + } while (_dispatch_timeout(timeout)); + return true; +} #else #error "port has to implement _dispatch_sema4_t" #endif @@ -560,6 +642,45 @@ _dispatch_wait_on_address(uint32_t volatile *_address, uint32_t value, return _umtx_op((void*)address, UMTX_OP_WAIT_UINT, value, (void*)(uintptr_t)sizeof(struct timespec), (void*)&ts); } return _umtx_op((void*)address, UMTX_OP_WAIT_UINT, value, 0, 0); +#elif defined(__wasi__) + (void)flags; + while (os_atomic_load(address, relaxed) == value) { + // re-check the deadline before draining so that a continuous stream + // of runnable work cannot make a timed wait overshoot it + if (nsecs != DISPATCH_TIME_FOREVER && + (nsecs = _dispatch_timeout(timeout)) == 0) { + return ETIMEDOUT; + } + if (_dispatch_wasi_drain_one()) continue; + if (nsecs != DISPATCH_TIME_FOREVER) { + if (_dispatch_wasi_in_drain()) { + // timers cannot merge while nested: sleeping toward one + // would spin at its deadline; honor only the wait's own + // deadline + _dispatch_wasi_sleep_until(_dispatch_uptime() + nsecs); + } else { + _dispatch_wasi_sleep_briefly_or_until( + _dispatch_uptime() + nsecs); + } + } else { + if (_dispatch_wasi_in_drain()) { + // nested waits can never be satisfied: no other work or + // timer can run while a drained item blocks the sole + // thread + DISPATCH_CLIENT_CRASH(0, "single-threaded WASI deadlock: " + "_dispatch_wait_on_address() from within a drained " + "work item"); + } + uint64_t deadline = _dispatch_wasi_next_timer_ns(); + if (!deadline) { + DISPATCH_CLIENT_CRASH(0, "single-threaded WASI deadlock: " + "_dispatch_wait_on_address() with no runnable work " + "or pending timers"); + } + _dispatch_wasi_sleep_until(deadline); + } + } + return 0; #else #error _dispatch_wait_on_address unimplemented for this platform #endif diff --git a/src/shims/lock.h b/src/shims/lock.h index 36ccc9920..7c98f6073 100644 --- a/src/shims/lock.h +++ b/src/shims/lock.h @@ -141,6 +141,27 @@ _dispatch_lock_owner(dispatch_lock lock_value) return lock_value & DLOCK_OWNER_MASK; } +#elif defined(__wasi__) + +typedef uint32_t dispatch_tid; +typedef uint32_t dispatch_lock; + +#define DLOCK_OWNER_NULL ((dispatch_tid)0) +#define DLOCK_OWNER_MASK ((dispatch_lock)0xfffffffc) +#define DLOCK_WAITERS_BIT ((dispatch_lock)0x00000001) +#define DLOCK_FAILED_TRYLOCK_BIT ((dispatch_lock)0x00000002) + +// tid is the constant 1 on single-threaded WASI; shift it clear of the +// low flag bits so the owner encoding survives DLOCK_OWNER_MASK +#define _dispatch_tid_self() ((dispatch_tid)(_dispatch_get_tsd_base()->tid << 2)) + +DISPATCH_ALWAYS_INLINE +static inline dispatch_tid +_dispatch_lock_owner(dispatch_lock lock_value) +{ + return lock_value & DLOCK_OWNER_MASK; +} + #else # error define _dispatch_lock encoding scheme for your platform here #endif @@ -263,10 +284,51 @@ void _dispatch_sema4_init(_dispatch_sema4_t *sema, int policy); #define _dispatch_sema4_is_created(sema) ((void)sema, 1) #define _dispatch_sema4_create_slow(sema, policy) ((void)sema, (void)policy) +#elif defined(__wasi__) + +// Single-threaded WASI: a plain counter; waiters make progress by +// cooperatively draining pending dispatch work instead of blocking +typedef uint32_t _dispatch_sema4_t; +#define _DSEMA4_POLICY_FIFO 0 +#define _DSEMA4_POLICY_LIFO 0 +#define _DSEMA4_TIMEOUT() ((errno) = ETIMEDOUT, -1) + +#define _dispatch_sema4_init(sema, policy) (void)(*(sema) = 0) +#define _dispatch_sema4_is_created(sema) ((void)sema, 1) +#define _dispatch_sema4_create_slow(sema, policy) ((void)sema, (void)policy) + #else #error "port has to implement _dispatch_sema4_t" #endif +#if defined(__wasi__) +// Cooperative drain support for single-threaded WASI, implemented in +// src/event/event_wasi.c: blocking waits drain pending dispatch work and +// timers instead of blocking the sole thread. +// +// Runs one pending item and returns true if it did. A due dispatch timer +// counts as a pending item and makes this return true; otherwise the wait +// loops below would busy-spin instead of firing it. +bool _dispatch_wasi_drain_one(void); +// Returns true while the cooperative drain is running a work item. Nested +// blocking waits can never be satisfied by more work: draining is refused +// while nested, so neither queued items nor due timers can run. Wait loops +// must therefore crash immediately on an indefinite nested wait, and sleep +// toward their own deadline only (never toward a timer) on a timed nested +// wait. +bool _dispatch_wasi_in_drain(void); +// Returns the uptime deadline of the nearest armed dispatch timer, in the +// _dispatch_uptime() clock domain; 0 means no timer is armed. +uint64_t _dispatch_wasi_next_timer_ns(void); +// Sleeps until the given _dispatch_uptime() deadline; returns immediately +// if the deadline is already past. +void _dispatch_wasi_sleep_until(uint64_t uptime_ns); +// Sleeps until min(deadline_uptime_ns, nearest armed dispatch timer): a +// plain sleep to the deadline would make timed waits sleep through timers +// that are due earlier. +void _dispatch_wasi_sleep_briefly_or_until(uint64_t deadline_uptime_ns); +#endif + void _dispatch_sema4_dispose_slow(_dispatch_sema4_t *sema, int policy); void _dispatch_sema4_signal(_dispatch_sema4_t *sema, long count); void _dispatch_sema4_wait(_dispatch_sema4_t *sema); diff --git a/src/shims/time.h b/src/shims/time.h index 9befd19e1..4aa21e042 100644 --- a/src/shims/time.h +++ b/src/shims/time.h @@ -153,6 +153,10 @@ _dispatch_uptime(void) ULONGLONG ullUnbiasedTime; _dispatch_QueryUnbiasedInterruptTimePrecise(&ullUnbiasedTime); return ullUnbiasedTime * 100; +#elif defined(__wasi__) + struct timespec ts; + dispatch_assume_zero(clock_gettime(CLOCK_MONOTONIC, &ts)); + return _dispatch_timespec_to_nano(ts); #else #error platform needs to implement _dispatch_uptime() #endif diff --git a/src/transform.c b/src/transform.c index 6e65567ad..2c458b9d2 100644 --- a/src/transform.c +++ b/src/transform.c @@ -22,7 +22,7 @@ #ifdef __APPLE__ #include -#elif __linux__ +#elif defined(__linux__) || defined(__wasi__) #include #define OSLittleEndian __LITTLE_ENDIAN #define OSBigEndian __BIG_ENDIAN @@ -35,7 +35,8 @@ #define OSBigEndian 4321 #endif -#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) +#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || \ + defined(__wasi__) #define OSSwapLittleToHostInt16 le16toh #define OSSwapBigToHostInt16 be16toh #define OSSwapHostToLittleInt16 htole16 From 5a2c59a3f99b984a495e57befc015e1699b2a7ac Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Fri, 14 Aug 2026 08:34:49 +0200 Subject: [PATCH 2/4] Build the Swift Dispatch overlay for WASI --- os/generic_unix_base.h | 15 +++++++++++++++ src/swift/CMakeLists.txt | 8 ++++++++ src/swift/Source.swift | 10 +++++----- src/swift/Wrapper.swift | 8 ++++---- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/os/generic_unix_base.h b/os/generic_unix_base.h index b77d2adaa..73b9c8435 100644 --- a/os/generic_unix_base.h +++ b/os/generic_unix_base.h @@ -23,6 +23,21 @@ #include #include #endif +#if defined(__wasi__) +/* + * Include before the textual . This only + * matters when this header is parsed into the CDispatch clang module by + * Swift's ClangImporter (plain C builds are unaffected): the toolchain's + * wasi-libc.modulemap declares as a header of the + * SwiftWASILibc module but leaves and textual, + * so the static inline __bswap* definitions from end up both + * inside SwiftWASILibc (via ) and textually in CDispatch + * (via ), which clang rejects as redefinitions. Importing + * the module first makes its include guards visible, so the textual + * re-include below is skipped instead of redefining them. + */ +#include +#endif #include #if __has_include() diff --git a/src/swift/CMakeLists.txt b/src/swift/CMakeLists.txt index a0082fb1e..78f9acd1f 100644 --- a/src/swift/CMakeLists.txt +++ b/src/swift/CMakeLists.txt @@ -20,6 +20,14 @@ target_compile_options(swiftDispatch PRIVATE "SHELL:-Xcc -fmodule-map-file=${PROJECT_SOURCE_DIR}/dispatch/module.modulemap" "SHELL:-Xcc -I${PROJECT_SOURCE_DIR}" "SHELL:-Xcc -I${PROJECT_SOURCE_DIR}/src/swift/shims") +if(CMAKE_SYSTEM_NAME STREQUAL "WASI") + target_compile_options(swiftDispatch PUBLIC + "$<$:SHELL:-sdk \"${CMAKE_SYSROOT}\">" + "$<$:SHELL:-resource-dir \"${SWIFT_WASI_STATIC_RESOURCES}\">" + "$<$:-static-stdlib>") + target_link_options(swiftDispatch INTERFACE + "SHELL:-Xclang-linker -resource-dir -Xclang-linker \"${SWIFT_WASI_CLANG_RESOURCES}\"") +endif() target_compile_options(swiftDispatch PUBLIC "SHELL:-vfsoverlay ${CMAKE_BINARY_DIR}/dispatch-vfs-overlay.yaml") set_target_properties(swiftDispatch PROPERTIES diff --git a/src/swift/Source.swift b/src/swift/Source.swift index 0c3abc7f9..abbe684cc 100644 --- a/src/swift/Source.swift +++ b/src/swift/Source.swift @@ -116,7 +116,7 @@ extension DispatchSource { } #endif -#if !os(Linux) && !os(Android) && !os(Windows) +#if !os(Linux) && !os(Android) && !os(Windows) && !os(WASI) public struct ProcessEvent : OptionSet, RawRepresentable { public let rawValue: UInt public init(rawValue: UInt) { self.rawValue = rawValue } @@ -186,7 +186,7 @@ extension DispatchSource { } #endif -#if !os(Linux) && !os(Android) && !os(Windows) +#if !os(Linux) && !os(Android) && !os(Windows) && !os(WASI) public class func makeProcessSource(identifier: pid_t, eventMask: ProcessEvent, queue: DispatchQueue? = nil) -> DispatchSourceProcess { let source = dispatch_source_create(_swift_dispatch_source_type_PROC(), UInt(identifier), eventMask.rawValue, queue?.__wrapped) return DispatchSource(source: source) as DispatchSourceProcess @@ -236,7 +236,7 @@ extension DispatchSource { return DispatchSource(source: source) as DispatchSourceUserDataReplace } -#if !os(Linux) && !os(Android) && !os(Windows) && !os(OpenBSD) && !os(FreeBSD) +#if !os(Linux) && !os(Android) && !os(Windows) && !os(OpenBSD) && !os(FreeBSD) && !os(WASI) public class func makeFileSystemObjectSource(fileDescriptor: Int32, eventMask: FileSystemEvent, queue: DispatchQueue? = nil) -> DispatchSourceFileSystemObject { let source = dispatch_source_create(_swift_dispatch_source_type_VNODE(), UInt(fileDescriptor), eventMask.rawValue, queue?.__wrapped) return DispatchSource(source: source) as DispatchSourceFileSystemObject @@ -302,7 +302,7 @@ extension DispatchSourceMemoryPressure { } #endif -#if !os(Linux) && !os(Android) && !os(Windows) +#if !os(Linux) && !os(Android) && !os(Windows) && !os(WASI) extension DispatchSourceProcess { public var handle: pid_t { return pid_t(CDispatch.dispatch_source_get_handle((self as! DispatchSource).__wrapped)) @@ -658,7 +658,7 @@ extension DispatchSourceTimer { } } -#if !os(Linux) && !os(Android) && !os(Windows) && !os(OpenBSD) +#if !os(Linux) && !os(Android) && !os(Windows) && !os(OpenBSD) && !os(WASI) extension DispatchSourceFileSystemObject { public var handle: Int32 { return Int32(dispatch_source_get_handle((self as! DispatchSource).__wrapped)) diff --git a/src/swift/Wrapper.swift b/src/swift/Wrapper.swift index a260697f2..80e907869 100644 --- a/src/swift/Wrapper.swift +++ b/src/swift/Wrapper.swift @@ -182,12 +182,12 @@ extension DispatchSource : DispatchSourceMachSend, } #endif -#if !os(Linux) && !os(Android) && !os(Windows) +#if !os(Linux) && !os(Android) && !os(Windows) && !os(WASI) extension DispatchSource : DispatchSourceProcess { } #endif -#if !os(Linux) && !os(Android) && !os(Windows) && !os(FreeBSD) && !os(OpenBSD) +#if !os(Linux) && !os(Android) && !os(Windows) && !os(FreeBSD) && !os(OpenBSD) && !os(WASI) extension DispatchSource : DispatchSourceFileSystemObject { } #endif @@ -277,7 +277,7 @@ public protocol DispatchSourceMemoryPressure : DispatchSourceProtocol { } #endif -#if !os(Linux) && !os(Android) && !os(Windows) +#if !os(Linux) && !os(Android) && !os(Windows) && !os(WASI) public protocol DispatchSourceProcess : DispatchSourceProtocol { var handle: pid_t { get } @@ -307,7 +307,7 @@ public protocol DispatchSourceTimer : DispatchSourceProtocol { func scheduleRepeating(wallDeadline: DispatchWallTime, interval: Double, leeway: DispatchTimeInterval) } -#if !os(Linux) && !os(Android) && !os(Windows) && !os(OpenBSD) +#if !os(Linux) && !os(Android) && !os(Windows) && !os(OpenBSD) && !os(WASI) public protocol DispatchSourceFileSystemObject : DispatchSourceProtocol { var handle: Int32 { get } From f380644b657cea2bb1ef2973b6eda2cf55faf684 Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Fri, 14 Aug 2026 08:35:50 +0200 Subject: [PATCH 3/4] Add WASI smoke and policy tests --- CMakeLists.txt | 3 + tests/CMakeLists.txt | 4 + tests/wasm/CMakeLists.txt | 127 ++++++++++++++ tests/wasm/README.md | 73 +++++++++ tests/wasm/barrier-order.c | 43 +++++ tests/wasm/clean-consumer.c | 34 ++++ tests/wasm/clean-consumer.swift | 7 + tests/wasm/dispatch-main-idle.c | 29 ++++ tests/wasm/dispatch-main-work.c | 30 ++++ tests/wasm/expect-runner-failure.cmake | 15 ++ tests/wasm/nested-timed-wait.c | 51 ++++++ tests/wasm/nested-wait.c | 36 ++++ tests/wasm/qos-order.c | 41 +++++ tests/wasm/root-fairness.c | 79 +++++++++ tests/wasm/run-wasi-test.mjs | 62 +++++++ tests/wasm/signal-source.c | 36 ++++ tests/wasm/smoke.c | 219 +++++++++++++++++++++++++ tests/wasm/unsupported-source.c | 35 ++++ tests/wasm/wall-timer.c | 51 ++++++ 19 files changed, 975 insertions(+) create mode 100644 tests/wasm/CMakeLists.txt create mode 100644 tests/wasm/README.md create mode 100644 tests/wasm/barrier-order.c create mode 100644 tests/wasm/clean-consumer.c create mode 100644 tests/wasm/clean-consumer.swift create mode 100644 tests/wasm/dispatch-main-idle.c create mode 100644 tests/wasm/dispatch-main-work.c create mode 100644 tests/wasm/expect-runner-failure.cmake create mode 100644 tests/wasm/nested-timed-wait.c create mode 100644 tests/wasm/nested-wait.c create mode 100644 tests/wasm/qos-order.c create mode 100644 tests/wasm/root-fairness.c create mode 100644 tests/wasm/run-wasi-test.mjs create mode 100644 tests/wasm/signal-source.c create mode 100644 tests/wasm/smoke.c create mode 100644 tests/wasm/unsupported-source.c create mode 100644 tests/wasm/wall-timer.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a5f8b9bd..35a3e03d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,6 +121,9 @@ include(CheckLibraryExists) include(CheckLinkerFlag) include(CheckSymbolExists) include(GNUInstallDirs) +if(CMAKE_SYSTEM_NAME STREQUAL "WASI") + set(BUILD_TESTING ON CACHE BOOL "build WASI smoke and policy tests") +endif() include(CTest) include(DispatchAppleOptions) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d2323c0cc..0d92fd7d8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,3 +1,7 @@ +if(CMAKE_SYSTEM_NAME STREQUAL "WASI") + add_subdirectory(wasm) + return() +endif() if(WIN32) execute_process(COMMAND diff --git a/tests/wasm/CMakeLists.txt b/tests/wasm/CMakeLists.txt new file mode 100644 index 000000000..84787a9a4 --- /dev/null +++ b/tests/wasm/CMakeLists.txt @@ -0,0 +1,127 @@ +find_program(NODE_EXECUTABLE node REQUIRED) +execute_process( + COMMAND "${NODE_EXECUTABLE}" --version + RESULT_VARIABLE NODE_VERSION_RESULT + OUTPUT_VARIABLE NODE_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE) +if(NOT NODE_VERSION_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to query Node version with ${NODE_EXECUTABLE}") +endif() +string(REGEX REPLACE "^v" "" NODE_VERSION "${NODE_VERSION}") +if(NODE_VERSION VERSION_LESS 19.8) + message(FATAL_ERROR "Node 19.8 or newer is required, found ${NODE_VERSION}") +endif() + +function(add_wasi_test name source mode expected timeout) + add_executable(${name} ${source}) + target_compile_options(${name} PRIVATE -fblocks -Wall) + target_link_libraries(${name} PRIVATE dispatch) + target_link_options(${name} PRIVATE + -nodefaultlibs + -lc + "${DISPATCH_WASI_BUILTINS}") + add_test(NAME ${name} + COMMAND "${NODE_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/run-wasi-test.mjs" + ${mode} "$" "${expected}" ${ARGN}) + set_tests_properties(${name} PROPERTIES + TIMEOUT ${timeout}) +endfunction() + +add_wasi_test(dispatch_wasi_smoke smoke.c success "ALL PASS (9/9)" 20) +add_wasi_test(dispatch_wasi_unsupported_source unsupported-source.c crash + "file-descriptor and signal dispatch sources are unsupported on single-threaded WASI" 5) +add_wasi_test(dispatch_wasi_signal_source signal-source.c crash + "file-descriptor and signal dispatch sources are unsupported on single-threaded WASI" 5) +add_wasi_test(dispatch_wasi_nested_wait nested-wait.c crash + "semaphore wait from within a drained work item" 5) +add_wasi_test(dispatch_wasi_nested_timed_wait nested-timed-wait.c success "probe OK" 5) +add_wasi_test(dispatch_wasi_dispatch_main_work dispatch-main-work.c crash + "dispatch_main(): no runnable work on single-threaded WASI" 5 "main queue work OK") +add_wasi_test(dispatch_wasi_dispatch_main_idle dispatch-main-idle.c crash + "dispatch_main(): no runnable work on single-threaded WASI" 5 + "dispatch_main idle entry") +add_wasi_test(dispatch_wasi_barrier_order barrier-order.c success "barrier order OK" 5) +add_wasi_test(dispatch_wasi_qos_order qos-order.c success "qos order OK" 5) +add_wasi_test(dispatch_wasi_root_fairness root-fairness.c success + "root fairness OK" 5) +add_wasi_test(dispatch_wasi_wall_timer wall-timer.c success "wall timer OK" 5) + +add_executable(dispatch_wasi_clean_c_consumer clean-consumer.c) +target_compile_options(dispatch_wasi_clean_c_consumer PRIVATE -fblocks -Wall) +target_link_libraries(dispatch_wasi_clean_c_consumer PRIVATE dispatch) +target_link_options(dispatch_wasi_clean_c_consumer PRIVATE + -nodefaultlibs + -lc + "${DISPATCH_WASI_BUILTINS}") +add_test(NAME dispatch_wasi_clean_c_consumer + COMMAND "${NODE_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/run-wasi-test.mjs" + success "$" "consumer OK") +set_tests_properties(dispatch_wasi_clean_c_consumer PROPERTIES + TIMEOUT 20) + +if(ENABLE_SWIFT) + add_executable(dispatch_wasi_clean_swift_consumer clean-consumer.swift) + target_link_libraries(dispatch_wasi_clean_swift_consumer PRIVATE swiftDispatch) + add_test(NAME dispatch_wasi_clean_swift_consumer + COMMAND "${NODE_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/run-wasi-test.mjs" + success "$" "Swift consumer OK") + set_tests_properties(dispatch_wasi_clean_swift_consumer PROPERTIES + TIMEOUT 20) + + add_executable(dispatch_wasi_swift_autolink_consumer clean-consumer.swift) + add_dependencies(dispatch_wasi_swift_autolink_consumer + swiftDispatch dispatch BlocksRuntime) + target_include_directories(dispatch_wasi_swift_autolink_consumer PRIVATE + "${PROJECT_BINARY_DIR}/src/swift/swift") + target_compile_options(dispatch_wasi_swift_autolink_consumer PRIVATE + "SHELL:-sdk \"${CMAKE_SYSROOT}\"" + "SHELL:-resource-dir \"${SWIFT_WASI_STATIC_RESOURCES}\"" + -static-stdlib + "SHELL:-vfsoverlay \"${PROJECT_BINARY_DIR}/dispatch-vfs-overlay.yaml\"" + "SHELL:-Xcc -fblocks" + "SHELL:-Xcc -I\"${PROJECT_SOURCE_DIR}\"" + "SHELL:-Xcc -D_WASI_EMULATED_SIGNAL" + "SHELL:-Xcc -D_WASI_EMULATED_MMAN" + "SHELL:-Xcc -D_WASI_EMULATED_GETPID") + target_link_directories(dispatch_wasi_swift_autolink_consumer PRIVATE + "${PROJECT_BINARY_DIR}/src" + "${PROJECT_BINARY_DIR}/src/swift" + "${PROJECT_BINARY_DIR}/src/BlocksRuntime") + target_link_options(dispatch_wasi_swift_autolink_consumer PRIVATE + "SHELL:-Xclang-linker -resource-dir -Xclang-linker \"${SWIFT_WASI_CLANG_RESOURCES}\"") + add_test(NAME dispatch_wasi_swift_autolink_consumer + COMMAND "${NODE_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/run-wasi-test.mjs" + success "$" "Swift consumer OK") + set_tests_properties(dispatch_wasi_swift_autolink_consumer PROPERTIES TIMEOUT 20) +endif() + +add_test(NAME dispatch_wasi_runner_wrong_mode + COMMAND "${CMAKE_COMMAND}" + -DNODE_EXECUTABLE=${NODE_EXECUTABLE} + -DRUNNER=${CMAKE_CURRENT_SOURCE_DIR}/run-wasi-test.mjs + -DBINARY=$ + -DMODE=crash + -DEXPECTED=ALL\ PASS + -P ${CMAKE_CURRENT_SOURCE_DIR}/expect-runner-failure.cmake) +add_test(NAME dispatch_wasi_runner_missing_diagnostic + COMMAND "${CMAKE_COMMAND}" + -DNODE_EXECUTABLE=${NODE_EXECUTABLE} + -DRUNNER=${CMAKE_CURRENT_SOURCE_DIR}/run-wasi-test.mjs + -DBINARY=$ + -DMODE=crash + -DEXPECTED=diagnostic-that-is-not-emitted + -P ${CMAKE_CURRENT_SOURCE_DIR}/expect-runner-failure.cmake) +add_test(NAME dispatch_wasi_runner_missing_marker + COMMAND "${CMAKE_COMMAND}" + -DNODE_EXECUTABLE=${NODE_EXECUTABLE} + -DRUNNER=${CMAKE_CURRENT_SOURCE_DIR}/run-wasi-test.mjs + -DBINARY=$ + -DMODE=crash + -DEXPECTED=main\ queue\ work\ OK + -DEXTRA_EXPECTED=marker-that-is-not-emitted + -P ${CMAKE_CURRENT_SOURCE_DIR}/expect-runner-failure.cmake) +set_tests_properties( + dispatch_wasi_runner_wrong_mode + dispatch_wasi_runner_missing_diagnostic + dispatch_wasi_runner_missing_marker + PROPERTIES TIMEOUT 20) diff --git a/tests/wasm/README.md b/tests/wasm/README.md new file mode 100644 index 000000000..e8e160d60 --- /dev/null +++ b/tests/wasm/README.md @@ -0,0 +1,73 @@ +# WASI tests + +## Prerequisites + +- Swift 6.3.2 release toolchain +- Matching Swift 6.3.2 `wasm32-unknown-wasip1` SDK +- CMake 3.31 or newer. This is the first release whose documentation recognizes + `CMAKE_SYSTEM_NAME=WASI`. +- Ninja 1.10 or newer +- Node.js 19.8 or newer + +Official Swift toolchains installed for the current user normally live under +`~/Library/Developer/Toolchains`. Swift SDK artifact bundles installed with +`swift sdk install` normally live under +`~/Library/org.swift.swiftpm/swift-sdks`. + +Set these paths before configuring: + +```sh +export SWIFT_WASI_TOOLCHAIN_PATH="$HOME/Library/Developer/Toolchains/swift-6.3.2-RELEASE.xctoolchain" +export SWIFT_WASI_SDK_PATH="$HOME/Library/org.swift.swiftpm/swift-sdks/swift-6.3.2-RELEASE_wasm.artifactbundle/swift-6.3.2-RELEASE_wasm/wasm32-unknown-wasip1" +``` + +`SWIFT_WASI_TOOLCHAIN_PATH` must be the `.xctoolchain` root containing +`usr/bin/clang`, `usr/bin/clang++`, and, for Swift builds, `usr/bin/swiftc`. +`SWIFT_WASI_SDK_PATH` must be the target directory containing both `WASI.sdk` +and `swift.xctoolchain/usr/lib/swift_static`. + +The toolchain derives and validates the Swift static resources and WASI +compiler-rt builtins from the SDK path on every configure. Nonstandard SDK +layouts can override them with `SWIFT_WASI_STATIC_RESOURCES_OVERRIDE` and +`DISPATCH_WASI_BUILTINS_OVERRIDE`. + +## WASI semantics + +Single-threaded WASI drains queues and timers cooperatively. `dispatch_main()` +drains useful main-queue work and armed timers, then traps with +`dispatch_main(): no runnable work on single-threaded WASI` when the process is +fully idle. It cannot block forever because there is no other thread that can +make progress. + +Read, write, and signal dispatch source types remain available to C and Swift, +but they fail with a named diagnostic when registration reaches the WASI event +backend. Swift read/write source factories and `DispatchIO` therefore remain +visible but also fail loudly when they attempt unsupported file-descriptor +registration. The C `DISPATCH_SOURCE_TYPE_PROC` declaration has no linkable +WASI definition because process sources are implemented only by the kevent +backend. Swift process and vnode source APIs are compiled out for WASI. + +Uptime and wall-clock timers fire normally. The WASI backend converts each wall +timer deadline to the uptime clock when it is armed, so later host wall-clock +adjustments do not reposition an already armed timer. + +## Build and test + +From the repository root, configure, build, and test the C library with one +command ladder: + +```sh +cmake -S . -B build-wasi -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/WASI.cmake -DSWIFT_WASI_TOOLCHAIN_PATH="$SWIFT_WASI_TOOLCHAIN_PATH" -DSWIFT_WASI_SDK_PATH="$SWIFT_WASI_SDK_PATH" -DBUILD_TESTING=ON && cmake --build build-wasi && ctest --test-dir build-wasi --output-on-failure +``` + +Add `-DENABLE_SWIFT=YES` to the configure step to build and test the Dispatch +Swift overlay. CTest runs every WebAssembly executable under Node WASI with a +watchdog. The runner captures guest output and passes only when the guest exit +mode and every expected output marker or diagnostic match. Focused tests cover +deferred barrier ordering, initial root-queue QoS order, fairness across +self-replenishing roots, uptime and wall timers, signal and file-descriptor +source failures, and both useful and immediately idle `dispatch_main()` paths. + +WASI selects `dispatch/wasi/module.modulemap` so static Swift clients autolink +BlocksRuntime and the WASI emulation archives without changing the generic +module map used by Linux and Windows. diff --git a/tests/wasm/barrier-order.c b/tests/wasm/barrier-order.c new file mode 100644 index 000000000..dec0302ec --- /dev/null +++ b/tests/wasm/barrier-order.c @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 Apple Inc. All rights reserved. + * + * @APPLE_APACHE_LICENSE_HEADER_START@ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @APPLE_APACHE_LICENSE_HEADER_END@ + */ + +#include +#include + +static int count; +static int order[3]; + +int +main(void) +{ + dispatch_queue_t queue = dispatch_queue_create("wasi.barrier", + DISPATCH_QUEUE_CONCURRENT); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + dispatch_async(queue, ^{ order[count++] = 1; }); + dispatch_barrier_async(queue, ^{ order[count++] = 2; }); + dispatch_async(queue, ^{ order[count++] = 3; }); + }); + dispatch_release(queue); + if (count != 3 || order[0] != 1 || order[1] != 2 || order[2] != 3) { + return 1; + } + puts("barrier order OK"); + return 0; +} diff --git a/tests/wasm/clean-consumer.c b/tests/wasm/clean-consumer.c new file mode 100644 index 000000000..e53db3c18 --- /dev/null +++ b/tests/wasm/clean-consumer.c @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2026 Apple Inc. All rights reserved. + * + * @APPLE_APACHE_LICENSE_HEADER_START@ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @APPLE_APACHE_LICENSE_HEADER_END@ + */ + +#include +#include + +int +main(void) +{ + __block int value = 0; + dispatch_queue_t queue = dispatch_queue_create("wasi.consumer", NULL); + dispatch_sync(queue, ^{ value = 1; }); + dispatch_release(queue); + if (value != 1) return 1; + puts("consumer OK"); + return 0; +} diff --git a/tests/wasm/clean-consumer.swift b/tests/wasm/clean-consumer.swift new file mode 100644 index 000000000..8277e9dc8 --- /dev/null +++ b/tests/wasm/clean-consumer.swift @@ -0,0 +1,7 @@ +import Dispatch + +let queue = DispatchQueue(label: "wasi.swift.consumer") +var value = 0 +queue.sync { value = 1 } +guard value == 1 else { fatalError("consumer did not run") } +print("Swift consumer OK") diff --git a/tests/wasm/dispatch-main-idle.c b/tests/wasm/dispatch-main-idle.c new file mode 100644 index 000000000..cc5fa7246 --- /dev/null +++ b/tests/wasm/dispatch-main-idle.c @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2026 Apple Inc. All rights reserved. + * + * @APPLE_APACHE_LICENSE_HEADER_START@ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @APPLE_APACHE_LICENSE_HEADER_END@ + */ + +#include +#include + +int +main(void) +{ + puts("dispatch_main idle entry"); + dispatch_main(); +} diff --git a/tests/wasm/dispatch-main-work.c b/tests/wasm/dispatch-main-work.c new file mode 100644 index 000000000..92ac609ac --- /dev/null +++ b/tests/wasm/dispatch-main-work.c @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2026 Apple Inc. All rights reserved. + * + * @APPLE_APACHE_LICENSE_HEADER_START@ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @APPLE_APACHE_LICENSE_HEADER_END@ + */ + +#include +#include + +int +main(void) +{ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 20 * NSEC_PER_MSEC), + dispatch_get_main_queue(), ^{ puts("main queue work OK"); }); + dispatch_main(); +} diff --git a/tests/wasm/expect-runner-failure.cmake b/tests/wasm/expect-runner-failure.cmake new file mode 100644 index 000000000..75acf4af8 --- /dev/null +++ b/tests/wasm/expect-runner-failure.cmake @@ -0,0 +1,15 @@ +set(expected_markers "${EXPECTED}") +if(DEFINED EXTRA_EXPECTED AND NOT EXTRA_EXPECTED STREQUAL "") + list(APPEND expected_markers "${EXTRA_EXPECTED}") +endif() + +execute_process( + COMMAND "${NODE_EXECUTABLE}" "${RUNNER}" "${MODE}" "${BINARY}" ${expected_markers} + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ERROR_VARIABLE error) + +if(result EQUAL 0) + message(FATAL_ERROR + "runner unexpectedly accepted mode=${MODE} expected=${expected_markers}\n${output}${error}") +endif() diff --git a/tests/wasm/nested-timed-wait.c b/tests/wasm/nested-timed-wait.c new file mode 100644 index 000000000..a63d8ecf7 --- /dev/null +++ b/tests/wasm/nested-timed-wait.c @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026 Apple Inc. All rights reserved. + * + * @APPLE_APACHE_LICENSE_HEADER_START@ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @APPLE_APACHE_LICENSE_HEADER_END@ + */ + +#include +#include +#include +#include + +static uint64_t +now_ms(void) +{ + struct timespec time; + clock_gettime(CLOCK_MONOTONIC, &time); + return (uint64_t)time.tv_sec * 1000 + (uint64_t)time.tv_nsec / 1000000; +} + +int +main(void) +{ + __block int passed = 0; + dispatch_queue_t queue = dispatch_queue_create("wasi.nested.timed", NULL); + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC), queue, ^{}); + dispatch_async(queue, ^{ + uint64_t start = now_ms(); + long result = dispatch_semaphore_wait(semaphore, + dispatch_time(DISPATCH_TIME_NOW, 200 * NSEC_PER_MSEC)); + uint64_t elapsed = now_ms() - start; + passed = result != 0 && elapsed >= 200; + }); + if (!passed) return 1; + puts("probe OK"); + return 0; +} diff --git a/tests/wasm/nested-wait.c b/tests/wasm/nested-wait.c new file mode 100644 index 000000000..6b6ba347f --- /dev/null +++ b/tests/wasm/nested-wait.c @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 Apple Inc. All rights reserved. + * + * @APPLE_APACHE_LICENSE_HEADER_START@ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @APPLE_APACHE_LICENSE_HEADER_END@ + */ + +#include +#include + +int +main(void) +{ + dispatch_queue_t queue = dispatch_queue_create("wasi.nested.wait", NULL); + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC), queue, ^{}); + dispatch_async(queue, ^{ + puts("nested wait starting"); + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + }); + puts("nested wait did not crash"); + return 0; +} diff --git a/tests/wasm/qos-order.c b/tests/wasm/qos-order.c new file mode 100644 index 000000000..6acecf754 --- /dev/null +++ b/tests/wasm/qos-order.c @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026 Apple Inc. All rights reserved. + * + * @APPLE_APACHE_LICENSE_HEADER_START@ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @APPLE_APACHE_LICENSE_HEADER_END@ + */ + +#include +#include + +static int count; +static int order[2]; + +int +main(void) +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ + order[count++] = 1; + }); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ + order[count++] = 2; + }); + }); + if (count != 2 || order[0] != 2 || order[1] != 1) return 1; + puts("qos order OK"); + return 0; +} diff --git a/tests/wasm/root-fairness.c b/tests/wasm/root-fairness.c new file mode 100644 index 000000000..9696a2d85 --- /dev/null +++ b/tests/wasm/root-fairness.c @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026 Apple Inc. All rights reserved. + * + * @APPLE_APACHE_LICENSE_HEADER_START@ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @APPLE_APACHE_LICENSE_HEADER_END@ + */ + +#include +#include + +#define HIGH_WORK_COUNT 100 + +static int high_count; +static int low_seen_after = -1; + +static void +default_work(void *context) +{ + (void)context; +} + +static void +high_work(void *context) +{ + dispatch_queue_t queue = context; + high_count++; + if (high_count < HIGH_WORK_COUNT) { + dispatch_async_f(dispatch_get_global_queue( + DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), NULL, default_work); + dispatch_async_f(queue, queue, high_work); + } +} + +static void +low_work(void *context) +{ + (void)context; + low_seen_after = high_count; +} + +static void +seed_work(void *context) +{ + (void)context; + dispatch_queue_t low = dispatch_get_global_queue( + DISPATCH_QUEUE_PRIORITY_LOW, 0); + dispatch_queue_t high = dispatch_get_global_queue( + DISPATCH_QUEUE_PRIORITY_HIGH, 0); + dispatch_async_f(low, NULL, low_work); + dispatch_async_f(high, high, high_work); +} + +int +main(void) +{ + dispatch_async_f(dispatch_get_global_queue( + DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), NULL, seed_work); + if (high_count != HIGH_WORK_COUNT || low_seen_after <= 0 || + low_seen_after >= HIGH_WORK_COUNT) { + printf("high_count=%d low_seen_after=%d\n", high_count, + low_seen_after); + return 1; + } + puts("root fairness OK"); + return 0; +} diff --git a/tests/wasm/run-wasi-test.mjs b/tests/wasm/run-wasi-test.mjs new file mode 100644 index 000000000..e11d878f1 --- /dev/null +++ b/tests/wasm/run-wasi-test.mjs @@ -0,0 +1,62 @@ +import { WASI } from 'node:wasi'; +import { spawn } from 'node:child_process'; +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; + +const runner = fileURLToPath(import.meta.url); + +async function runGuest(binary) { + const wasi = new WASI({ version: 'preview1', args: [binary], env: {} }); + try { + const module = await WebAssembly.compile(await readFile(binary)); + const instance = await WebAssembly.instantiate(module, wasi.getImportObject()); + process.exitCode = wasi.start(instance); + } catch (error) { + console.error(`[trap] ${error.message}`); + process.exitCode = 134; + } +} + +async function runChecked(mode, binary, ...expected) { + if (!['success', 'crash'].includes(mode) || !binary || expected.length === 0) { + console.error('usage: run-wasi-test.mjs ...'); + return 2; + } + + const child = spawn(process.execPath, [runner, '--guest', binary], { + stdio: ['ignore', 'pipe', 'pipe'], + }); + let stdout = ''; + let stderr = ''; + child.stdout.setEncoding('utf8'); + child.stderr.setEncoding('utf8'); + child.stdout.on('data', data => { stdout += data; }); + child.stderr.on('data', data => { stderr += data; }); + + const result = await new Promise((resolve, reject) => { + child.on('error', reject); + child.on('close', (code, signal) => resolve({ code, signal })); + }); + process.stdout.write(stdout); + process.stderr.write(stderr); + + const crashed = result.signal !== null || result.code !== 0; + const output = stdout + stderr; + if ((mode === 'crash') !== crashed) { + console.error(`expected ${mode}, observed exit=${result.code} signal=${result.signal}`); + return 1; + } + for (const text of expected) { + if (!output.includes(text)) { + console.error(`expected output was not found: ${text}`); + return 1; + } + } + return 0; +} + +if (process.argv[2] === '--guest') { + await runGuest(process.argv[3]); +} else { + process.exitCode = await runChecked(...process.argv.slice(2)); +} diff --git a/tests/wasm/signal-source.c b/tests/wasm/signal-source.c new file mode 100644 index 000000000..d0e03812b --- /dev/null +++ b/tests/wasm/signal-source.c @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 Apple Inc. All rights reserved. + * + * @APPLE_APACHE_LICENSE_HEADER_START@ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @APPLE_APACHE_LICENSE_HEADER_END@ + */ + +#include +#include +#include + +int +main(void) +{ + dispatch_queue_t queue = dispatch_queue_create("wasi.signal", NULL); + dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, + SIGTERM, 0, queue); + if (!source) return 1; + dispatch_source_set_event_handler(source, ^{ puts("unexpected handler"); }); + dispatch_resume(source); + puts("signal source did not crash"); + return 0; +} diff --git a/tests/wasm/smoke.c b/tests/wasm/smoke.c new file mode 100644 index 000000000..bb6abfd1c --- /dev/null +++ b/tests/wasm/smoke.c @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2026 Apple Inc. All rights reserved. + * + * @APPLE_APACHE_LICENSE_HEADER_START@ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @APPLE_APACHE_LICENSE_HEADER_END@ + */ + +#include +#include +#include +#include +#include + +static int failures; +#define CHECK(value) do { if (!(value)) { failures++; \ + printf("check failed: %s:%d: %s\n", __FILE__, __LINE__, #value); } } while (0) + +static uint64_t +now_ms(void) +{ + struct timespec time; + clock_gettime(CLOCK_MONOTONIC, &time); + return (uint64_t)time.tv_sec * 1000 + (uint64_t)time.tv_nsec / 1000000; +} + +static int once_count; +static void once(void *context) { (void)context; once_count++; } +static char specific_key, specific_value; + +static void +test_once_and_specifics(void) +{ + static dispatch_once_t predicate; + dispatch_once_f(&predicate, NULL, once); + dispatch_once_f(&predicate, NULL, once); + CHECK(once_count == 1); + dispatch_queue_t queue = dispatch_queue_create("wasi.specifics", NULL); + dispatch_queue_set_specific(queue, &specific_key, &specific_value, NULL); + CHECK(dispatch_get_specific(&specific_key) == NULL); + __block void *value; + dispatch_sync(queue, ^{ value = dispatch_get_specific(&specific_key); }); + CHECK(value == &specific_value); + dispatch_release(queue); +} + +static void +test_fifo(void) +{ + __block int count = 0; + static int order[10]; + dispatch_queue_t queue = dispatch_queue_create("wasi.fifo", NULL); + for (int i = 0; i < 10; i++) { + dispatch_async(queue, ^{ order[count++] = i; }); + } + CHECK(count == 10); + for (int i = 0; i < 10; i++) CHECK(order[i] == i); + dispatch_release(queue); +} + +static void +test_nested_fifo(void) +{ + __block int count = 0; + static int order[11]; + dispatch_queue_t queue = dispatch_queue_create("wasi.fifo", NULL); + dispatch_async(queue, ^{ + for (int i = 0; i < 10; i++) { + dispatch_async(queue, ^{ order[count++] = i; }); + } + order[count++] = 100; + }); + CHECK(count == 11 && order[0] == 100); + for (int i = 0; i < 10; i++) CHECK(order[i + 1] == i); + dispatch_release(queue); +} + +static void +test_sync_and_barrier(void) +{ + __block int value = 0; + dispatch_queue_t serial = dispatch_queue_create("wasi.sync", NULL); + dispatch_async(serial, ^{ value = 1; }); + dispatch_sync(serial, ^{ CHECK(value == 1); value = 2; }); + CHECK(value == 2); + dispatch_queue_t concurrent = dispatch_queue_create("wasi.barrier", + DISPATCH_QUEUE_CONCURRENT); + dispatch_barrier_sync(concurrent, ^{ value = 3; }); + CHECK(value == 3); + dispatch_release(concurrent); + dispatch_release(serial); +} + +static void +test_semaphore(void) +{ + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + uint64_t start = now_ms(); + long result = dispatch_semaphore_wait(semaphore, + dispatch_time(DISPATCH_TIME_NOW, 100 * NSEC_PER_MSEC)); + uint64_t elapsed = now_ms() - start; + CHECK(result != 0 && elapsed >= 100); + dispatch_semaphore_signal(semaphore); + CHECK(dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) == 0); + dispatch_release(semaphore); +} + +static void +test_group(void) +{ + __block int notified = 0; + dispatch_queue_t queue = dispatch_queue_create("wasi.group", NULL); + dispatch_group_t group = dispatch_group_create(); + dispatch_group_enter(group); + dispatch_group_notify(group, queue, ^{ notified++; }); + dispatch_async(queue, ^{ dispatch_group_leave(group); }); + CHECK(dispatch_group_wait(group, DISPATCH_TIME_FOREVER) == 0); + CHECK(notified == 1); + dispatch_release(group); + dispatch_release(queue); +} + +static void +test_timers(void) +{ + __block int fires = 0; + dispatch_queue_t queue = dispatch_queue_create("wasi.timer", NULL); + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 20 * NSEC_PER_MSEC), queue, ^{ + dispatch_semaphore_signal(semaphore); + }); + CHECK(dispatch_semaphore_wait(semaphore, + dispatch_time(DISPATCH_TIME_NOW, 500 * NSEC_PER_MSEC)) == 0); + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, + 0, 0, queue); + dispatch_source_set_timer(timer, + dispatch_time(DISPATCH_TIME_NOW, 20 * NSEC_PER_MSEC), + 20 * NSEC_PER_MSEC, 0); + dispatch_source_set_event_handler(timer, ^{ + if (++fires == 3) dispatch_source_cancel(timer); + }); + dispatch_source_set_cancel_handler(timer, ^{ + dispatch_semaphore_signal(semaphore); + }); + dispatch_resume(timer); + CHECK(dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) == 0); + CHECK(fires >= 3); + dispatch_release(timer); + dispatch_release(semaphore); + dispatch_release(queue); +} + +static void +test_apply(void) +{ + __block size_t sum = 0; + dispatch_apply(8, dispatch_get_global_queue(0, 0), ^(size_t index) { + sum += index; + }); + CHECK(sum == 28); +} + +static void +test_data(void) +{ + static const char first[] = "01234567"; + static const char second[] = "abcdefgh"; + dispatch_data_t one = dispatch_data_create(first, 8, NULL, + DISPATCH_DATA_DESTRUCTOR_DEFAULT); + dispatch_data_t two = dispatch_data_create(second, 8, NULL, + DISPATCH_DATA_DESTRUCTOR_DEFAULT); + dispatch_data_t joined = dispatch_data_create_concat(one, two); + const void *bytes; + size_t size; + dispatch_data_t mapped = dispatch_data_create_map(joined, &bytes, &size); + CHECK(size == 16 && memcmp(bytes, "01234567abcdefgh", 16) == 0); + dispatch_release(mapped); + dispatch_release(joined); + dispatch_release(two); + dispatch_release(one); +} + +static void +run(const char *name, void (*test)(void), int *passed) +{ + int before = failures; + test(); + if (before == failures) { printf("PASS %s\n", name); (*passed)++; } +} + +int +main(void) +{ + int passed = 0; + run("once_and_specifics", test_once_and_specifics, &passed); + run("fifo", test_fifo, &passed); + run("fifo_nested_backlog", test_nested_fifo, &passed); + run("sync_and_barrier", test_sync_and_barrier, &passed); + run("semaphore_timing", test_semaphore, &passed); + run("group_wait_notify", test_group, &passed); + run("after_timers_cancel", test_timers, &passed); + run("apply", test_apply, &passed); + run("data", test_data, &passed); + if (failures) return 1; + printf("ALL PASS (%d/9)\n", passed); + return passed == 9 ? 0 : 1; +} diff --git a/tests/wasm/unsupported-source.c b/tests/wasm/unsupported-source.c new file mode 100644 index 000000000..559d94331 --- /dev/null +++ b/tests/wasm/unsupported-source.c @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2026 Apple Inc. All rights reserved. + * + * @APPLE_APACHE_LICENSE_HEADER_START@ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @APPLE_APACHE_LICENSE_HEADER_END@ + */ + +#include +#include + +int +main(void) +{ + dispatch_queue_t queue = dispatch_queue_create("wasi.unsupported", NULL); + dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, + 0, 0, queue); + if (!source) return 1; + dispatch_source_set_event_handler(source, ^{ puts("unexpected handler"); }); + dispatch_resume(source); + puts("unsupported source did not crash"); + return 0; +} diff --git a/tests/wasm/wall-timer.c b/tests/wasm/wall-timer.c new file mode 100644 index 000000000..c90aca94f --- /dev/null +++ b/tests/wasm/wall-timer.c @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026 Apple Inc. All rights reserved. + * + * @APPLE_APACHE_LICENSE_HEADER_START@ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @APPLE_APACHE_LICENSE_HEADER_END@ + */ + +#include +#include +#include +#include + +static uint64_t +now_ms(void) +{ + struct timespec time; + clock_gettime(CLOCK_MONOTONIC, &time); + return (uint64_t)time.tv_sec * 1000 + (uint64_t)time.tv_nsec / 1000000; +} + +int +main(void) +{ + dispatch_queue_t queue = dispatch_queue_create("wasi.wall.timer", NULL); + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + uint64_t start = now_ms(); + dispatch_after(dispatch_walltime(NULL, 20 * NSEC_PER_MSEC), queue, ^{ + dispatch_semaphore_signal(semaphore); + }); + long result = dispatch_semaphore_wait(semaphore, + dispatch_time(DISPATCH_TIME_NOW, 500 * NSEC_PER_MSEC)); + uint64_t elapsed = now_ms() - start; + dispatch_release(semaphore); + dispatch_release(queue); + if (result != 0 || elapsed < 20) return 1; + puts("wall timer OK"); + return 0; +} From 7474fffb578c998cd0fdd982428087846a9524db Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Fri, 14 Aug 2026 15:31:34 +0200 Subject: [PATCH 4/4] Run the libdispatch test suite on WASI Register the single-thread-compatible subset of the upstream bsdtests suite for WASI instead of relying only on the bespoke tests/wasm suite. Test binaries are executed by a new WASI_TEST_RUNNER cache variable (default wasmtime); when the runner is absent the tests are registered but disabled so configuration still succeeds. bsdtestharness is not built for WASI because posix_spawn does not exist there; the runner propagates the guest exit code instead. Compiling bsdtests and the tests for wasm32-wasip1 needs __wasi__ arms next to the existing __unix__ guards (WASI clang does not define __unix__), the generic_unix_port.h shims, a WASI-safe failure exit status (WASI rejects 0xff), and stubs for the large-file helpers since wasi-libc has no mkstemp. 11 of the 20 default DISPATCH_C_TESTS plus dispatch_c99 and dispatch_plusplus pass under wasmtime. The remaining 9 need concurrent worker threads or file-descriptor sources and are excluded with the reason documented in tests/CMakeLists.txt. --- tests/CMakeLists.txt | 110 ++++++++++++++++++++++++------- tests/bsdtests.c | 7 +- tests/bsdtests.h | 2 +- tests/dispatch_after.c | 2 +- tests/dispatch_context_for_key.c | 2 +- tests/dispatch_overcommit.c | 2 +- tests/dispatch_queue_finalizer.c | 2 +- tests/dispatch_test.c | 11 +++- tests/dispatch_test.h | 2 +- tests/dispatch_timer_bit31.c | 2 +- tests/dispatch_timer_bit63.c | 2 +- tests/dispatch_timer_set_time.c | 2 +- tests/dispatch_timer_timeout.c | 2 +- tests/wasm/README.md | 23 +++++-- 14 files changed, 129 insertions(+), 42 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0d92fd7d8..7a5118770 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,6 +1,17 @@ if(CMAKE_SYSTEM_NAME STREQUAL "WASI") + # Command used to execute .wasm test binaries; may be a semicolon-separated + # list (e.g. "wasmtime;--some-flag"). The runner must propagate the guest's + # exit code, which any WASI CLI runtime does. + set(WASI_TEST_RUNNER "wasmtime" CACHE STRING + "Command (semicolon-separated list) used to execute WASI test binaries") + list(GET WASI_TEST_RUNNER 0 _wasi_test_runner_program) + find_program(WASI_TEST_RUNNER_EXECUTABLE "${_wasi_test_runner_program}") + if(NOT WASI_TEST_RUNNER_EXECUTABLE) + message(STATUS + "WASI test runner '${_wasi_test_runner_program}' not found; " + "WASI unit tests will be registered but disabled") + endif() add_subdirectory(wasm) - return() endif() if(WIN32) @@ -51,17 +62,21 @@ if (WIN32) bcrypt) endif () -add_executable(bsdtestharness - bsdtestharness.c) -target_include_directories(bsdtestharness - PRIVATE - ${CMAKE_CURRENT_BINARY_DIR} - ${CMAKE_CURRENT_SOURCE_DIR} - ${PROJECT_SOURCE_DIR}) -target_link_libraries(bsdtestharness - PRIVATE - bsdtests - dispatch) +# bsdtestharness requires posix_spawn, which does not exist on WASI; the +# WASI unit tests are executed directly by ${WASI_TEST_RUNNER} instead. +if(NOT CMAKE_SYSTEM_NAME STREQUAL "WASI") + add_executable(bsdtestharness + bsdtestharness.c) + target_include_directories(bsdtestharness + PRIVATE + ${CMAKE_CURRENT_BINARY_DIR} + ${CMAKE_CURRENT_SOURCE_DIR} + ${PROJECT_SOURCE_DIR}) + target_link_libraries(bsdtestharness + PRIVATE + bsdtests + dispatch) +endif() function(add_unit_test name) set(options DISABLED_TEST) @@ -107,13 +122,34 @@ function(add_unit_test name) Threads::Threads BlocksRuntime::BlocksRuntime) target_link_libraries(${name} PRIVATE bsdtests) - add_test(NAME ${name} - COMMAND bsdtestharness $) - set_tests_properties(${name} - PROPERTIES - TIMEOUT 120 - DEPENDS bsdtestharness - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + if(CMAKE_SYSTEM_NAME STREQUAL "WASI") + # The toolchain's default compiler runtime is not built for wasm32; + # link libc and the WASI builtins archive explicitly, mirroring the + # link strategy of the dispatch library itself (see tests/wasm). + target_link_options(${name} PRIVATE + -nodefaultlibs + "$<$:-lc++>" + "$<$:-lc++abi>" + -lc + "${DISPATCH_WASI_BUILTINS}") + add_test(NAME ${name} + COMMAND ${WASI_TEST_RUNNER} $) + set_tests_properties(${name} + PROPERTIES + TIMEOUT 120 + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + if(NOT WASI_TEST_RUNNER_EXECUTABLE) + set_tests_properties(${name} PROPERTIES DISABLED TRUE) + endif() + else() + add_test(NAME ${name} + COMMAND bsdtestharness $) + set_tests_properties(${name} + PROPERTIES + TIMEOUT 120 + DEPENDS bsdtestharness + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + endif() if(NOT leaks_EXECUTABLE) set_tests_properties(${name} PROPERTIES @@ -121,6 +157,31 @@ function(add_unit_test name) endif() endfunction() +if(CMAKE_SYSTEM_NAME STREQUAL "WASI") + # The subset of the default C test suite that runs on single-threaded + # WASI. The other default tests are incompatible with a single thread: + # apply - keeps worker threads spinning concurrently with dispatch_apply + # overcommit - spins (while(1)) in every work item but the last, so it + # needs concurrent worker threads to reach the one that stops it + # context_for_key - blocks in dispatch_group_wait() inside a work item, + # which the single-threaded port rejects as a guaranteed deadlock + # timer_timeout - blocks the sole thread in sleep() and expects timers + # to fire concurrently on worker threads meanwhile + # io_muxed, io_net, io_pipe, io_pipe_close, select - file-descriptor + # dispatch sources are unsupported on single-threaded WASI + set(DISPATCH_C_TESTS + api + debug + queue_finalizer + after + timer + timer_short + sema + timer_bit31 + timer_bit63 + timer_set_time + data) +else() # Tests that reliably pass on all platforms set(DISPATCH_C_TESTS apply @@ -143,6 +204,7 @@ set(DISPATCH_C_TESTS io_pipe io_pipe_close select) +endif() # Tests that usually pass, but occasionally fail. # Excluded by default for purposes of Swift CI @@ -185,14 +247,16 @@ foreach(test ${DISPATCH_C_TESTS}) dispatch_${test}.c) endforeach() -set_tests_properties(dispatch_io_pipe PROPERTIES TIMEOUT 15) -set_tests_properties(dispatch_io_pipe_close PROPERTIES TIMEOUT 5) +if(NOT CMAKE_SYSTEM_NAME STREQUAL "WASI") + set_tests_properties(dispatch_io_pipe PROPERTIES TIMEOUT 15) + set_tests_properties(dispatch_io_pipe_close PROPERTIES TIMEOUT 5) +endif() # test dispatch API for various C/CXX language variants add_unit_test(dispatch_c99 SOURCES dispatch_c99.c) add_unit_test(dispatch_plusplus SOURCES dispatch_plusplus.cpp) -if (DISPATCH_USE_INTERNAL_WORKQUEUE) +if (DISPATCH_USE_INTERNAL_WORKQUEUE AND NOT CMAKE_SYSTEM_NAME STREQUAL "WASI") add_unit_test(dispatch_workqueue SOURCES dispatch_workqueue.c) @@ -202,7 +266,7 @@ endif() if(WIN32) target_link_libraries(dispatch_io_muxed PRIVATE WS2_32) target_link_libraries(dispatch_io_net PRIVATE WS2_32) -else() +elseif(NOT CMAKE_SYSTEM_NAME STREQUAL "WASI") # When dispatch_group is reenabled above, remove this if(EXTENDED_TEST_SUITE) target_link_libraries(dispatch_group PRIVATE m) diff --git a/tests/bsdtests.c b/tests/bsdtests.c index 3ea91a381..398abe5f2 100644 --- a/tests/bsdtests.c +++ b/tests/bsdtests.c @@ -25,7 +25,7 @@ #include #include #include -#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) +#if defined(__unix__) || defined(__wasi__) || (defined(__APPLE__) && defined(__MACH__)) #include #endif #include @@ -540,7 +540,12 @@ test_stop_after_delay(void *delay) #endif fflush(stdout); +#if defined(__wasi__) + // WASI requires exit statuses in [0..126); 0xff would trap in proc_exit + _exit(_test_exit_code ? EXIT_FAILURE : EXIT_SUCCESS); +#else _exit(_test_exit_code); +#endif } void diff --git a/tests/bsdtests.h b/tests/bsdtests.h index 3437a51ed..a936384f0 100644 --- a/tests/bsdtests.h +++ b/tests/bsdtests.h @@ -49,7 +49,7 @@ #include #endif -#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) +#if defined(__unix__) || defined(__wasi__) || (defined(__APPLE__) && defined(__MACH__)) #include #endif #include diff --git a/tests/dispatch_after.c b/tests/dispatch_after.c index 2b46dc903..dea17e63f 100644 --- a/tests/dispatch_after.c +++ b/tests/dispatch_after.c @@ -20,7 +20,7 @@ #include #include -#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) +#if defined(__unix__) || defined(__wasi__) || (defined(__APPLE__) && defined(__MACH__)) #include #endif #include diff --git a/tests/dispatch_context_for_key.c b/tests/dispatch_context_for_key.c index cecf48c56..893c4a678 100644 --- a/tests/dispatch_context_for_key.c +++ b/tests/dispatch_context_for_key.c @@ -21,7 +21,7 @@ #include #include #include -#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) +#if defined(__unix__) || defined(__wasi__) || (defined(__APPLE__) && defined(__MACH__)) #include #endif #include diff --git a/tests/dispatch_overcommit.c b/tests/dispatch_overcommit.c index d2fca3b5c..dd4fd8eb6 100644 --- a/tests/dispatch_overcommit.c +++ b/tests/dispatch_overcommit.c @@ -25,7 +25,7 @@ #include #include #include -#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) +#if defined(__unix__) || defined(__wasi__) || (defined(__APPLE__) && defined(__MACH__)) #include #endif #include diff --git a/tests/dispatch_queue_finalizer.c b/tests/dispatch_queue_finalizer.c index acd2275ff..e72e47c02 100644 --- a/tests/dispatch_queue_finalizer.c +++ b/tests/dispatch_queue_finalizer.c @@ -19,7 +19,7 @@ */ #include -#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) +#if defined(__unix__) || defined(__wasi__) || (defined(__APPLE__) && defined(__MACH__)) #include #endif #include diff --git a/tests/dispatch_test.c b/tests/dispatch_test.c index 5c2aef948..2d67cb1f6 100644 --- a/tests/dispatch_test.c +++ b/tests/dispatch_test.c @@ -28,7 +28,7 @@ #include #include #include -#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) +#if defined(__unix__) || defined(__wasi__) || (defined(__APPLE__) && defined(__MACH__)) #include #if __has_include() #define HAS_SYS_EVENT_H 1 @@ -107,6 +107,11 @@ dispatch_test_get_large_file(void) { #if defined(__APPLE__) return strdup("/usr/bin/vi"); +#elif defined(__wasi__) + // wasi-libc has no mkstemp ("WASI has no temp directories") and the + // dispatch IO tests that need a large file cannot run on WASI anyway + fprintf(stderr, "dispatch_test_get_large_file is unsupported on WASI\n"); + abort(); #elif defined(__unix__) || defined(_WIN32) // Depending on /usr/bin/vi being present is unreliable (especially on // Android), so fill up a large-enough temp file with random bytes @@ -199,6 +204,10 @@ dispatch_test_release_large_file(const char *path) #if defined(__APPLE__) // The path is fixed to a system file - do nothing (void)path; +#elif defined(__wasi__) + (void)path; + fprintf(stderr, "dispatch_test_release_large_file is unsupported on WASI\n"); + abort(); #elif defined(__unix__) || defined(_WIN32) if (unlink(path) < 0) { perror("unlink"); diff --git a/tests/dispatch_test.h b/tests/dispatch_test.h index 99094ebbf..a3b122937 100644 --- a/tests/dispatch_test.h +++ b/tests/dispatch_test.h @@ -21,7 +21,7 @@ #include #include -#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) +#if defined(__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__wasi__) #include #elif defined(_WIN32) #include diff --git a/tests/dispatch_timer_bit31.c b/tests/dispatch_timer_bit31.c index a70c4f6d0..9defec8ac 100644 --- a/tests/dispatch_timer_bit31.c +++ b/tests/dispatch_timer_bit31.c @@ -21,7 +21,7 @@ #include #include #include -#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) +#if defined(__unix__) || defined(__wasi__) || (defined(__APPLE__) && defined(__MACH__)) #include #endif diff --git a/tests/dispatch_timer_bit63.c b/tests/dispatch_timer_bit63.c index f01ca5183..4e9fecc76 100644 --- a/tests/dispatch_timer_bit63.c +++ b/tests/dispatch_timer_bit63.c @@ -21,7 +21,7 @@ #include #include #include -#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) +#if defined(__unix__) || defined(__wasi__) || (defined(__APPLE__) && defined(__MACH__)) #include #endif diff --git a/tests/dispatch_timer_set_time.c b/tests/dispatch_timer_set_time.c index 6f30b0c98..dd28f7ba9 100644 --- a/tests/dispatch_timer_set_time.c +++ b/tests/dispatch_timer_set_time.c @@ -21,7 +21,7 @@ #include #include #include -#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) +#if defined(__unix__) || defined(__wasi__) || (defined(__APPLE__) && defined(__MACH__)) #include #endif diff --git a/tests/dispatch_timer_timeout.c b/tests/dispatch_timer_timeout.c index 109bbff37..b52f93eac 100644 --- a/tests/dispatch_timer_timeout.c +++ b/tests/dispatch_timer_timeout.c @@ -21,7 +21,7 @@ #include #include #include -#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) +#if defined(__unix__) || defined(__wasi__) || (defined(__APPLE__) && defined(__MACH__)) #include #endif diff --git a/tests/wasm/README.md b/tests/wasm/README.md index e8e160d60..911e2f4f4 100644 --- a/tests/wasm/README.md +++ b/tests/wasm/README.md @@ -7,7 +7,10 @@ - CMake 3.31 or newer. This is the first release whose documentation recognizes `CMAKE_SYSTEM_NAME=WASI`. - Ninja 1.10 or newer -- Node.js 19.8 or newer +- Node.js 19.8 or newer (runs the tests in this directory) +- wasmtime, or another WASI runtime named with `-DWASI_TEST_RUNNER=...` + (runs the WASI subset of the upstream test suite in `tests/`; when the + runner is missing those tests are registered but disabled) Official Swift toolchains installed for the current user normally live under `~/Library/Developer/Toolchains`. Swift SDK artifact bundles installed with @@ -61,12 +64,18 @@ cmake -S . -B build-wasi -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/WASI.c ``` Add `-DENABLE_SWIFT=YES` to the configure step to build and test the Dispatch -Swift overlay. CTest runs every WebAssembly executable under Node WASI with a -watchdog. The runner captures guest output and passes only when the guest exit -mode and every expected output marker or diagnostic match. Focused tests cover -deferred barrier ordering, initial root-queue QoS order, fairness across -self-replenishing roots, uptime and wall timers, signal and file-descriptor -source failures, and both useful and immediately idle `dispatch_main()` paths. +Swift overlay. + +CTest runs two groups of WASI tests. The single-thread-compatible subset of +the upstream `tests/` suite (see `DISPATCH_C_TESTS` in `tests/CMakeLists.txt`) +runs under `WASI_TEST_RUNNER` (default `wasmtime`), which only needs to +propagate the guest exit code. The focused tests in this directory run under +Node WASI with a watchdog; that runner captures guest output and passes only +when the guest exit mode and every expected output marker or diagnostic +match. Focused tests cover deferred barrier ordering, initial root-queue QoS +order, fairness across self-replenishing roots, uptime and wall timers, signal +and file-descriptor source failures, and both useful and immediately idle +`dispatch_main()` paths. WASI selects `dispatch/wasi/module.modulemap` so static Swift clients autolink BlocksRuntime and the WASI emulation archives without changing the generic