From ebdca64e0dbaeaf21e095f5cc89b4078fbfc57ac Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:43:14 +0000 Subject: [PATCH 1/4] napi: implement uv_async_t, uv_queue_work and uv_default_loop on posix On Linux and macOS every loop-related uv_* symbol an addon can link against is a stub that aborts the process with "unsupported uv function". Addons that use libuv for cross-thread wakeups (uv_async_init on uv_default_loop() or on the loop from napi_get_uv_event_loop) or for pool work (uv_queue_work) die in process.dlopen or in a constructor. src/runtime/napi/uv_posix.rs implements the loop-backed functions on top of the VM's event loop: - A uv_loop_t* is a UvLoop, one per VM, embedded in its RuntimeState. Its first word is the addon's data slot, as in uv_loop_t. napi_get_uv_event_loop returns the env's VM's loop (it used to return a pointer to Bun's internal EventLoop struct, whose first bytes an addon writing loop->data would have overwritten); uv_default_loop() returns the main thread's from any thread. - uv_async_init, uv_async_send, uv_close, uv_ref, uv_unref, uv_has_ref, uv_is_active, uv_is_closing. The handle's public fields are where uv.h puts them; the private fields hold a KeepAlive (the ref), libuv's pending flag and busy counter. uv_async_send sets pending and posts at most one dispatch task per loop through the VM's VmHandle; the task walks the loop's live handles on the JS thread. uv_close unregisters the handle, waits out senders, and delivers close_cb from a later loop turn. - uv_queue_work and uv_cancel, as a bun_jsc::Job whose off-thread half is the request itself; a state word in the request's private area decides between after_work_cb(0) and after_work_cb(UV_ECANCELED). uv-posix-polyfills.c gains the functions that need only the headers: uv_version, uv_version_string, uv_handle_size, uv_req_size, the type names, the handle, request and loop data getters and setters, and uv_get_osfhandle / uv_open_osfhandle. process.versions.uv now comes from uv_version_string() on every platform (1.51.0, the version of the vendored headers and of the libuv linked on Windows; posix used to report 1.48.0). The 28 implemented symbols are removed from the generated stubs, the generator's list and the stub test addon. Tests: test/napi/uv.test.ts (uv-stub-stuff/uv_impl.c), each in a child process since the process exit is part of what is checked; test-resolve-async.js of Node's test_callback_scope suite now runs on posix. --- src/jsc/VirtualMachine.rs | 6 +- src/jsc/bindings/BunProcess.cpp | 9 +- .../bindings/libuv/generate_uv_posix_stubs.ts | 2 +- .../generate_uv_posix_stubs_constants.ts | 80 +- src/jsc/bindings/uv-posix-polyfills.c | 147 ++++ src/jsc/bindings/uv-posix-stubs.c | 173 ---- src/runtime/jsc_hooks.rs | 12 + src/runtime/napi/mod.rs | 2 + src/runtime/napi/napi_body.rs | 45 +- src/runtime/napi/uv_posix.rs | 783 ++++++++++++++++++ .../test_async_cleanup_hook/do.test.ts | 5 +- .../node-api/test_callback_scope/do.test.ts | 17 +- test/napi/uv-stub-stuff/plugin.c | 202 ----- test/napi/uv-stub-stuff/uv_impl.c | 509 ++++++++++++ test/napi/uv.test.ts | 199 +++++ 15 files changed, 1737 insertions(+), 454 deletions(-) create mode 100644 src/runtime/napi/uv_posix.rs diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index e60d65ad940f..9b9af75338ee 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -798,7 +798,11 @@ impl VirtualMachine { VM.get() } - pub(crate) fn get_main_thread_vm() -> Option<*mut VirtualMachine> { + /// The main thread's VM, from any thread; `None` before it exists. It is + /// never freed, but off its own thread only what is thread-safe may be + /// reached through it: a wakeup of its loop (`PosixSignalHandle`), a field + /// written once at init (`uv_default_loop`). + pub fn get_main_thread_vm() -> Option<*mut VirtualMachine> { let p = MAIN_THREAD_VM.load(core::sync::atomic::Ordering::Acquire); if p.is_null() { None } else { Some(p) } } diff --git a/src/jsc/bindings/BunProcess.cpp b/src/jsc/bindings/BunProcess.cpp index f5aead014a1c..6133965bb196 100644 --- a/src/jsc/bindings/BunProcess.cpp +++ b/src/jsc/bindings/BunProcess.cpp @@ -80,6 +80,9 @@ #include #include #include +// uv-posix-polyfills.c: the version of the libuv headers the posix uv_* +// polyfills implement, for process.versions.uv. +extern "C" const char* uv_version_string(void); #else #include #include @@ -260,18 +263,14 @@ static JSValue constructVersions(VM& vm, JSObject* processObject) // Use commit hash for zstd (semantic version extraction not working yet) { "zstd", BUN_VERSION_ZSTD_HASH }, { "v8", REPORTED_NODEJS_V8_VERSION }, -#if !OS(WINDOWS) - { "uv", "1.48.0" }, -#endif }; auto putVersion = [&](const char* name, const char* version) { object->putDirect(vm, JSC::Identifier::fromString(vm, ASCIILiteral::fromLiteralUnsafe(name)), JSC::jsOwnedString(vm, String(ASCIILiteral::fromLiteralUnsafe(version))), 0); }; for (auto& entry : versions) putVersion(entry.name, entry.version); -#if OS(WINDOWS) + // Windows links libuv; posix defines it in uv-posix-polyfills.c. putDirectNamed(vm, object, "uv"_s, JSValue(JSC::jsOwnedString(vm, String::fromLatin1(uv_version_string())))); -#endif putVersion("napi", "10"); putVersion("icu", U_ICU_VERSION); putVersion("unicode", U_UNICODE_VERSION); diff --git a/src/jsc/bindings/libuv/generate_uv_posix_stubs.ts b/src/jsc/bindings/libuv/generate_uv_posix_stubs.ts index 2315903d6d13..f1dbb8a94f76 100644 --- a/src/jsc/bindings/libuv/generate_uv_posix_stubs.ts +++ b/src/jsc/bindings/libuv/generate_uv_posix_stubs.ts @@ -290,7 +290,7 @@ const final_contents = `// GENERATED CODE - DO NOT MODIFY BY HAND #include "uv-posix-polyfills.h" -#if OS(LINUX) || OS(DARWIN) +#if OS(LINUX) || OS(DARWIN) || OS(FREEBSD) ${parts.map(([stub, _]) => stub).join("\n\n")} #endif diff --git a/src/jsc/bindings/libuv/generate_uv_posix_stubs_constants.ts b/src/jsc/bindings/libuv/generate_uv_posix_stubs_constants.ts index b28f652ed91c..516969a3f12d 100644 --- a/src/jsc/bindings/libuv/generate_uv_posix_stubs_constants.ts +++ b/src/jsc/bindings/libuv/generate_uv_posix_stubs_constants.ts @@ -14,8 +14,9 @@ export const test_skipped = [ export const symbols = [ "uv_accept", - "uv_async_init", - "uv_async_send", + // Defined in src/runtime/napi/uv_posix.rs + // "uv_async_init", + // "uv_async_send", "uv_available_parallelism", "uv_backend_fd", "uv_backend_timeout", @@ -23,13 +24,15 @@ export const symbols = [ "uv_barrier_init", "uv_barrier_wait", "uv_buf_init", - "uv_cancel", + // Defined in src/runtime/napi/uv_posix.rs + // "uv_cancel", "uv_chdir", "uv_check_init", "uv_check_start", "uv_check_stop", "uv_clock_gettime", - "uv_close", + // Defined in src/runtime/napi/uv_posix.rs + // "uv_close", "uv_cond_broadcast", "uv_cond_destroy", "uv_cond_init", @@ -39,7 +42,8 @@ export const symbols = [ "uv_cpu_info", "uv_cpumask_size", "uv_cwd", - "uv_default_loop", + // Defined in src/runtime/napi/uv_posix.rs + // "uv_default_loop", "uv_disable_stdio_inheritance", "uv_dlclose", "uv_dlerror", @@ -107,7 +111,8 @@ export const symbols = [ "uv_get_available_memory", "uv_get_constrained_memory", "uv_get_free_memory", - "uv_get_osfhandle", + // Defined in uv-posix-polyfills.c + // "uv_get_osfhandle", "uv_get_process_title", "uv_get_total_memory", "uv_getaddrinfo", @@ -116,14 +121,16 @@ export const symbols = [ "uv_getrusage_thread", "uv_gettimeofday", "uv_guess_handle", - "uv_handle_get_data", - "uv_handle_get_loop", - "uv_handle_get_type", - "uv_handle_set_data", - "uv_handle_size", - "uv_handle_type_name", - "uv_has_ref", - // Defined in uv-posix-polyfills.cpp + // Defined in uv-posix-polyfills.c + // "uv_handle_get_data", + // "uv_handle_get_loop", + // "uv_handle_get_type", + // "uv_handle_set_data", + // "uv_handle_size", + // "uv_handle_type_name", + // Defined in src/runtime/napi/uv_posix.rs + // "uv_has_ref", + // Defined in uv-posix-polyfills.c // "uv_hrtime", "uv_idle_init", "uv_idle_start", @@ -138,8 +145,9 @@ export const symbols = [ "uv_ip6_addr", "uv_ip6_name", "uv_ip_name", - "uv_is_active", - "uv_is_closing", + // Defined in src/runtime/napi/uv_posix.rs + // "uv_is_active", + // "uv_is_closing", "uv_is_readable", "uv_is_writable", "uv_key_create", @@ -155,14 +163,16 @@ export const symbols = [ "uv_loop_configure", "uv_loop_delete", "uv_loop_fork", - "uv_loop_get_data", + // Defined in uv-posix-polyfills.c + // "uv_loop_get_data", "uv_loop_init", "uv_loop_new", - "uv_loop_set_data", + // Defined in uv-posix-polyfills.c + // "uv_loop_set_data", "uv_loop_size", "uv_metrics_idle_time", "uv_metrics_info", - // Defined in uv-posix-polyfills.cpp + // Defined in uv-posix-polyfills.c // "uv_mutex_destroy", // "uv_mutex_init", // "uv_mutex_init_recursive", @@ -170,9 +180,9 @@ export const symbols = [ // "uv_mutex_trylock", // "uv_mutex_unlock", "uv_now", - // Defined in uv-posix-polyfills.cpp + // Defined in uv-posix-polyfills.c // "uv_once", - "uv_open_osfhandle", + // "uv_open_osfhandle", "uv_os_environ", "uv_os_free_environ", "uv_os_free_group", @@ -182,9 +192,8 @@ export const symbols = [ "uv_os_get_passwd2", "uv_os_getenv", "uv_os_gethostname", - // Defined in uv-posix-polyfills.cpp + // Defined in uv-posix-polyfills.c // "uv_os_getpid", - // Defined in uv-posix-polyfills.cpp // "uv_os_getppid", "uv_os_getpriority", "uv_os_homedir", @@ -217,18 +226,21 @@ export const symbols = [ "uv_print_all_handles", "uv_process_get_pid", "uv_process_kill", - "uv_queue_work", + // Defined in src/runtime/napi/uv_posix.rs + // "uv_queue_work", "uv_random", "uv_read_start", "uv_read_stop", "uv_recv_buffer_size", - "uv_ref", + // Defined in src/runtime/napi/uv_posix.rs + // "uv_ref", "uv_replace_allocator", - "uv_req_get_data", - "uv_req_get_type", - "uv_req_set_data", - "uv_req_size", - "uv_req_type_name", + // Defined in uv-posix-polyfills.c + // "uv_req_get_data", + // "uv_req_get_type", + // "uv_req_set_data", + // "uv_req_size", + // "uv_req_type_name", "uv_resident_set_memory", "uv_run", "uv_rwlock_destroy", @@ -323,13 +335,15 @@ export const symbols = [ "uv_udp_try_send", "uv_udp_try_send2", "uv_udp_using_recvmmsg", - "uv_unref", + // Defined in src/runtime/napi/uv_posix.rs + // "uv_unref", "uv_update_time", "uv_uptime", "uv_utf16_length_as_wtf8", "uv_utf16_to_wtf8", - "uv_version", - "uv_version_string", + // Defined in uv-posix-polyfills.c + // "uv_version", + // "uv_version_string", "uv_walk", "uv_write", "uv_write2", diff --git a/src/jsc/bindings/uv-posix-polyfills.c b/src/jsc/bindings/uv-posix-polyfills.c index 19ae7e5040c8..78a1e29da49d 100644 --- a/src/jsc/bindings/uv-posix-polyfills.c +++ b/src/jsc/bindings/uv-posix-polyfills.c @@ -130,4 +130,151 @@ UV_EXTERN void uv_mutex_unlock(uv_mutex_t* mutex) abort(); } +// The functions below need nothing but the headers in ./libuv, so they are +// libuv's own definitions (src/version.c, src/uv-common.c, +// src/uv-data-getter-setters.c, src/unix/core.c). The loop-backed functions +// for the same handles and requests (uv_async_*, uv_close, uv_queue_work, ...) +// are in src/runtime/napi/uv_posix.rs. + +#define UV_STRINGIFY(v) UV_STRINGIFY_HELPER(v) +#define UV_STRINGIFY_HELPER(v) #v + +#define UV_VERSION_STRING_BASE UV_STRINGIFY(UV_VERSION_MAJOR) "." UV_STRINGIFY(UV_VERSION_MINOR) "." UV_STRINGIFY(UV_VERSION_PATCH) + +#if UV_VERSION_IS_RELEASE +#define UV_VERSION_STRING UV_VERSION_STRING_BASE +#else +#define UV_VERSION_STRING UV_VERSION_STRING_BASE "-" UV_VERSION_SUFFIX +#endif + +// The version of the headers these polyfills implement the ABI of, which is +// also the libuv Bun links on Windows. BunProcess.cpp reports it as +// process.versions.uv on every platform. +UV_EXTERN unsigned int uv_version(void) +{ + return UV_VERSION_HEX; +} + +UV_EXTERN const char* uv_version_string(void) +{ + return UV_VERSION_STRING; +} + +UV_EXTERN size_t uv_handle_size(uv_handle_type type) +{ + switch (type) { +#define XX(uc, lc) \ + case UV_##uc: \ + return sizeof(uv_##lc##_t); + UV_HANDLE_TYPE_MAP(XX) +#undef XX + default: + return (size_t)-1; + } +} + +UV_EXTERN size_t uv_req_size(uv_req_type type) +{ + switch (type) { +#define XX(uc, lc) \ + case UV_##uc: \ + return sizeof(uv_##lc##_t); + UV_REQ_TYPE_MAP(XX) +#undef XX + default: + return (size_t)-1; + } +} + +UV_EXTERN const char* uv_handle_type_name(uv_handle_type type) +{ + switch (type) { +#define XX(uc, lc) \ + case UV_##uc: \ + return #lc; + UV_HANDLE_TYPE_MAP(XX) +#undef XX + case UV_FILE: + return "file"; + case UV_HANDLE_TYPE_MAX: + case UV_UNKNOWN_HANDLE: + return NULL; + } + return NULL; +} + +UV_EXTERN const char* uv_req_type_name(uv_req_type type) +{ + switch (type) { +#define XX(uc, lc) \ + case UV_##uc: \ + return #lc; + UV_REQ_TYPE_MAP(XX) +#undef XX + case UV_REQ_TYPE_MAX: + case UV_UNKNOWN_REQ: + default: /* UV_REQ_TYPE_PRIVATE */ + break; + } + return NULL; +} + +UV_EXTERN uv_handle_type uv_handle_get_type(const uv_handle_t* handle) +{ + return handle->type; +} + +UV_EXTERN void* uv_handle_get_data(const uv_handle_t* handle) +{ + return handle->data; +} + +UV_EXTERN uv_loop_t* uv_handle_get_loop(const uv_handle_t* handle) +{ + return handle->loop; +} + +UV_EXTERN void uv_handle_set_data(uv_handle_t* handle, void* data) +{ + handle->data = data; +} + +UV_EXTERN uv_req_type uv_req_get_type(const uv_req_t* req) +{ + return req->type; +} + +UV_EXTERN void* uv_req_get_data(const uv_req_t* req) +{ + return req->data; +} + +UV_EXTERN void uv_req_set_data(uv_req_t* req, void* data) +{ + req->data = data; +} + +// A uv_loop_t* is a UvLoop (uv_posix.rs), whose first field is `data` like +// uv_loop_t's, so these are the libuv definitions. +UV_EXTERN void* uv_loop_get_data(const uv_loop_t* loop) +{ + return loop->data; +} + +UV_EXTERN void uv_loop_set_data(uv_loop_t* loop, void* data) +{ + loop->data = data; +} + +// On unix a uv_os_fd_t is an int: both directions are the identity. +UV_EXTERN uv_os_fd_t uv_get_osfhandle(int fd) +{ + return fd; +} + +UV_EXTERN int uv_open_osfhandle(uv_os_fd_t os_fd) +{ + return os_fd; +} + #endif diff --git a/src/jsc/bindings/uv-posix-stubs.c b/src/jsc/bindings/uv-posix-stubs.c index 04868a91cf9e..24f576595340 100644 --- a/src/jsc/bindings/uv-posix-stubs.c +++ b/src/jsc/bindings/uv-posix-stubs.c @@ -8,20 +8,6 @@ UV_EXTERN int uv_accept(uv_stream_t* server, uv_stream_t* client) __builtin_unreachable(); } -UV_EXTERN int uv_async_init(uv_loop_t*, - uv_async_t* async, - uv_async_cb async_cb) -{ - __bun_throw_not_implemented("uv_async_init"); - __builtin_unreachable(); -} - -UV_EXTERN int uv_async_send(uv_async_t* async) -{ - __bun_throw_not_implemented("uv_async_send"); - __builtin_unreachable(); -} - UV_EXTERN unsigned int uv_available_parallelism(void) { __bun_throw_not_implemented("uv_available_parallelism"); @@ -64,12 +50,6 @@ UV_EXTERN uv_buf_t uv_buf_init(char* base, unsigned int len) __builtin_unreachable(); } -UV_EXTERN int uv_cancel(uv_req_t* req) -{ - __bun_throw_not_implemented("uv_cancel"); - __builtin_unreachable(); -} - UV_EXTERN int uv_chdir(const char* dir) { __bun_throw_not_implemented("uv_chdir"); @@ -100,12 +80,6 @@ UV_EXTERN int uv_clock_gettime(uv_clock_id clock_id, uv_timespec64_t* ts) __builtin_unreachable(); } -UV_EXTERN void uv_close(uv_handle_t* handle, uv_close_cb close_cb) -{ - __bun_throw_not_implemented("uv_close"); - __builtin_unreachable(); -} - UV_EXTERN void uv_cond_broadcast(uv_cond_t* cond) { __bun_throw_not_implemented("uv_cond_broadcast"); @@ -162,12 +136,6 @@ UV_EXTERN int uv_cwd(char* buffer, size_t* size) __builtin_unreachable(); } -UV_EXTERN uv_loop_t* uv_default_loop(void) -{ - __bun_throw_not_implemented("uv_default_loop"); - __builtin_unreachable(); -} - UV_EXTERN void uv_disable_stdio_inheritance(void) { __bun_throw_not_implemented("uv_disable_stdio_inheritance"); @@ -725,12 +693,6 @@ UV_EXTERN uint64_t uv_get_free_memory(void) __builtin_unreachable(); } -UV_EXTERN uv_os_fd_t uv_get_osfhandle(int fd) -{ - __bun_throw_not_implemented("uv_get_osfhandle"); - __builtin_unreachable(); -} - UV_EXTERN int uv_get_process_title(char* buffer, size_t size) { __bun_throw_not_implemented("uv_get_process_title"); @@ -788,48 +750,6 @@ UV_EXTERN uv_handle_type uv_guess_handle(uv_file file) __builtin_unreachable(); } -UV_EXTERN void* uv_handle_get_data(const uv_handle_t* handle) -{ - __bun_throw_not_implemented("uv_handle_get_data"); - __builtin_unreachable(); -} - -UV_EXTERN uv_loop_t* uv_handle_get_loop(const uv_handle_t* handle) -{ - __bun_throw_not_implemented("uv_handle_get_loop"); - __builtin_unreachable(); -} - -UV_EXTERN uv_handle_type uv_handle_get_type(const uv_handle_t* handle) -{ - __bun_throw_not_implemented("uv_handle_get_type"); - __builtin_unreachable(); -} - -UV_EXTERN void uv_handle_set_data(uv_handle_t* handle, void* data) -{ - __bun_throw_not_implemented("uv_handle_set_data"); - __builtin_unreachable(); -} - -UV_EXTERN size_t uv_handle_size(uv_handle_type type) -{ - __bun_throw_not_implemented("uv_handle_size"); - __builtin_unreachable(); -} - -UV_EXTERN const char* uv_handle_type_name(uv_handle_type type) -{ - __bun_throw_not_implemented("uv_handle_type_name"); - __builtin_unreachable(); -} - -UV_EXTERN int uv_has_ref(const uv_handle_t*) -{ - __bun_throw_not_implemented("uv_has_ref"); - __builtin_unreachable(); -} - UV_EXTERN int uv_idle_init(uv_loop_t*, uv_idle_t* idle) { __bun_throw_not_implemented("uv_idle_init"); @@ -913,18 +833,6 @@ UV_EXTERN int uv_ip_name(const struct sockaddr* src, char* dst, size_t size) __builtin_unreachable(); } -UV_EXTERN int uv_is_active(const uv_handle_t* handle) -{ - __bun_throw_not_implemented("uv_is_active"); - __builtin_unreachable(); -} - -UV_EXTERN int uv_is_closing(const uv_handle_t* handle) -{ - __bun_throw_not_implemented("uv_is_closing"); - __builtin_unreachable(); -} - UV_EXTERN int uv_is_readable(const uv_stream_t* handle) { __bun_throw_not_implemented("uv_is_readable"); @@ -1015,12 +923,6 @@ UV_EXTERN int uv_loop_fork(uv_loop_t* loop) __builtin_unreachable(); } -UV_EXTERN void* uv_loop_get_data(const uv_loop_t*) -{ - __bun_throw_not_implemented("uv_loop_get_data"); - __builtin_unreachable(); -} - UV_EXTERN int uv_loop_init(uv_loop_t* loop) { __bun_throw_not_implemented("uv_loop_init"); @@ -1033,12 +935,6 @@ UV_EXTERN uv_loop_t* uv_loop_new(void) __builtin_unreachable(); } -UV_EXTERN void uv_loop_set_data(uv_loop_t*, void* data) -{ - __bun_throw_not_implemented("uv_loop_set_data"); - __builtin_unreachable(); -} - UV_EXTERN size_t uv_loop_size(void) { __bun_throw_not_implemented("uv_loop_size"); @@ -1063,12 +959,6 @@ UV_EXTERN uint64_t uv_now(const uv_loop_t*) __builtin_unreachable(); } -UV_EXTERN int uv_open_osfhandle(uv_os_fd_t os_fd) -{ - __bun_throw_not_implemented("uv_open_osfhandle"); - __builtin_unreachable(); -} - UV_EXTERN int uv_os_environ(uv_env_item_t** envitems, int* count) { __bun_throw_not_implemented("uv_os_environ"); @@ -1326,15 +1216,6 @@ UV_EXTERN int uv_process_kill(uv_process_t*, int signum) __builtin_unreachable(); } -UV_EXTERN int uv_queue_work(uv_loop_t* loop, - uv_work_t* req, - uv_work_cb work_cb, - uv_after_work_cb after_work_cb) -{ - __bun_throw_not_implemented("uv_queue_work"); - __builtin_unreachable(); -} - UV_EXTERN int uv_random(uv_loop_t* loop, uv_random_t* req, void* buf, @@ -1366,12 +1247,6 @@ UV_EXTERN int uv_recv_buffer_size(uv_handle_t* handle, int* value) __builtin_unreachable(); } -UV_EXTERN void uv_ref(uv_handle_t*) -{ - __bun_throw_not_implemented("uv_ref"); - __builtin_unreachable(); -} - UV_EXTERN int uv_replace_allocator(uv_malloc_func malloc_func, uv_realloc_func realloc_func, uv_calloc_func calloc_func, @@ -1381,36 +1256,6 @@ UV_EXTERN int uv_replace_allocator(uv_malloc_func malloc_func, __builtin_unreachable(); } -UV_EXTERN void* uv_req_get_data(const uv_req_t* req) -{ - __bun_throw_not_implemented("uv_req_get_data"); - __builtin_unreachable(); -} - -UV_EXTERN uv_req_type uv_req_get_type(const uv_req_t* req) -{ - __bun_throw_not_implemented("uv_req_get_type"); - __builtin_unreachable(); -} - -UV_EXTERN void uv_req_set_data(uv_req_t* req, void* data) -{ - __bun_throw_not_implemented("uv_req_set_data"); - __builtin_unreachable(); -} - -UV_EXTERN size_t uv_req_size(uv_req_type type) -{ - __bun_throw_not_implemented("uv_req_size"); - __builtin_unreachable(); -} - -UV_EXTERN const char* uv_req_type_name(uv_req_type type) -{ - __bun_throw_not_implemented("uv_req_type_name"); - __builtin_unreachable(); -} - UV_EXTERN int uv_resident_set_memory(size_t* rss) { __bun_throw_not_implemented("uv_resident_set_memory"); @@ -2031,12 +1876,6 @@ UV_EXTERN int uv_udp_using_recvmmsg(const uv_udp_t* handle) __builtin_unreachable(); } -UV_EXTERN void uv_unref(uv_handle_t*) -{ - __bun_throw_not_implemented("uv_unref"); - __builtin_unreachable(); -} - UV_EXTERN void uv_update_time(uv_loop_t*) { __bun_throw_not_implemented("uv_update_time"); @@ -2065,18 +1904,6 @@ UV_EXTERN int uv_utf16_to_wtf8(const uint16_t* utf16, __builtin_unreachable(); } -UV_EXTERN unsigned int uv_version(void) -{ - __bun_throw_not_implemented("uv_version"); - __builtin_unreachable(); -} - -UV_EXTERN const char* uv_version_string(void) -{ - __bun_throw_not_implemented("uv_version_string"); - __builtin_unreachable(); -} - UV_EXTERN void uv_walk(uv_loop_t* loop, uv_walk_cb walk_cb, void* arg) { __bun_throw_not_implemented("uv_walk"); diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 248e0bfe178d..214d74a49cb1 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -102,6 +102,11 @@ pub(crate) struct RuntimeState { /// The resolver's PackageManager wake-handler context (module queue + VM /// handle); the resolver holds a raw pointer to it. Freed with the state. pub(crate) wake_ctx: Option>, + /// The `uv_loop_t` N-API addons get for this VM (`napi_get_uv_event_loop`, + /// and `uv_default_loop()` for the main thread's). Embedded here because + /// addons keep its address for as long as the VM lives. + #[cfg(unix)] + pub(crate) uv_loop: crate::napi::uv_posix::UvLoop, } #[derive(Clone, Copy, PartialEq, Eq, Hash)] @@ -399,6 +404,13 @@ unsafe fn init_runtime_state( }, active_handles: ActiveHandles::default(), wake_ctx: None, + // SAFETY: `vm` is the live VM being initialised; `handle` is one of + // the fields `VirtualMachine::init` wrote before calling this hook + // (the wake context below reads it the same way). + #[cfg(unix)] + uv_loop: unsafe { + crate::napi::uv_posix::UvLoop::new(ptr::NonNull::new_unchecked(vm), (*vm).handle()) + }, })); RUNTIME_STATE.with(|c| c.set(state)); diff --git a/src/runtime/napi/mod.rs b/src/runtime/napi/mod.rs index 00ec7ae369e2..f0e7ff762ec3 100644 --- a/src/runtime/napi/mod.rs +++ b/src/runtime/napi/mod.rs @@ -13,6 +13,8 @@ pub(crate) use napi_body::{ }; pub(crate) mod libc_check; +#[cfg(unix)] +pub(crate) mod uv_posix; // ─── compiling free items ──────────────────────────────────────────────────── diff --git a/src/runtime/napi/napi_body.rs b/src/runtime/napi/napi_body.rs index d3d28da3e700..d3ef55f38749 100644 --- a/src/runtime/napi/napi_body.rs +++ b/src/runtime/napi/napi_body.rs @@ -2282,8 +2282,8 @@ extern "C" fn napi_get_node_version( #[cfg(windows)] type napi_event_loop = *mut bun_sys::windows::libuv::Loop; -#[cfg(not(windows))] -type napi_event_loop = *mut EventLoop; +#[cfg(unix)] +type napi_event_loop = *mut super::uv_posix::UvLoop; #[unsafe(no_mangle)] extern "C" fn napi_get_uv_event_loop(env_: napi_env, loop_: *mut napi_event_loop) -> napi_status { @@ -2296,13 +2296,16 @@ extern "C" fn napi_get_uv_event_loop(env_: napi_env, loop_: *mut napi_event_loop // TODO(@190n) investigate *loop_out = VirtualMachine::get().uv_loop(); } - #[cfg(not(windows))] + #[cfg(unix)] { - // there is no uv event loop on posix, we use our event loop handle. - // SAFETY: `VirtualMachine::event_loop` already yields `*mut EventLoop`; - // no const→mut cast needed. - // SAFETY: bun_vm() never null for a Bun-owned global. - *loop_out = env.to_js().bun_vm().event_loop(); + // No libuv loop on posix: the addon gets this VM's `UvLoop`, which the + // uv_* functions of `uv_posix` drive from this VM's event loop. + // SAFETY: the env's VM is alive while the env is. + let uv_loop = unsafe { super::uv_posix::UvLoop::of_vm(env.to_js().bun_vm()) }; + if uv_loop.is_null() { + return env.generic_failure(); + } + *loop_out = uv_loop; } env.ok() } @@ -4060,12 +4063,13 @@ mod posix_platform_specific_v8_apis { // uv_* symbol references (posix DCE suppression) // ────────────────────────────────────────────────────────────────────────── +/// The uv_* symbols defined in C (uv-posix-stubs.c, uv-posix-polyfills.c) and +/// C++ (uv_tty_reset_mode). The ones defined in Rust are kept by +/// `uv_posix::fix_dead_code_elimination`. #[cfg(unix)] mod uv_functions_to_export { unsafe extern "C" { pub(super) fn uv_accept(); - pub(super) fn uv_async_init(); - pub(super) fn uv_async_send(); pub(super) fn uv_available_parallelism(); pub(super) fn uv_backend_fd(); pub(super) fn uv_backend_timeout(); @@ -4073,13 +4077,11 @@ mod uv_functions_to_export { pub(super) fn uv_barrier_init(); pub(super) fn uv_barrier_wait(); pub(super) fn uv_buf_init(); - pub(super) fn uv_cancel(); pub(super) fn uv_chdir(); pub(super) fn uv_check_init(); pub(super) fn uv_check_start(); pub(super) fn uv_check_stop(); pub(super) fn uv_clock_gettime(); - pub(super) fn uv_close(); pub(super) fn uv_cond_broadcast(); pub(super) fn uv_cond_destroy(); pub(super) fn uv_cond_init(); @@ -4089,7 +4091,6 @@ mod uv_functions_to_export { pub(super) fn uv_cpu_info(); pub(super) fn uv_cpumask_size(); pub(super) fn uv_cwd(); - pub(super) fn uv_default_loop(); pub(super) fn uv_disable_stdio_inheritance(); pub(super) fn uv_dlclose(); pub(super) fn uv_dlerror(); @@ -4172,7 +4173,6 @@ mod uv_functions_to_export { pub(super) fn uv_handle_set_data(); pub(super) fn uv_handle_size(); pub(super) fn uv_handle_type_name(); - pub(super) fn uv_has_ref(); pub(super) fn uv_hrtime(); pub(super) fn uv_idle_init(); pub(super) fn uv_idle_start(); @@ -4187,8 +4187,6 @@ mod uv_functions_to_export { pub(super) fn uv_ip4_name(); pub(super) fn uv_ip6_addr(); pub(super) fn uv_ip6_name(); - pub(super) fn uv_is_active(); - pub(super) fn uv_is_closing(); pub(super) fn uv_is_readable(); pub(super) fn uv_is_writable(); pub(super) fn uv_key_create(); @@ -4262,12 +4260,10 @@ mod uv_functions_to_export { pub(super) fn uv_print_all_handles(); pub(super) fn uv_process_get_pid(); pub(super) fn uv_process_kill(); - pub(super) fn uv_queue_work(); pub(super) fn uv_random(); pub(super) fn uv_read_start(); pub(super) fn uv_read_stop(); pub(super) fn uv_recv_buffer_size(); - pub(super) fn uv_ref(); pub(super) fn uv_replace_allocator(); pub(super) fn uv_req_get_data(); pub(super) fn uv_req_get_type(); @@ -4367,7 +4363,6 @@ mod uv_functions_to_export { pub(super) fn uv_udp_try_send(); pub(super) fn uv_udp_try_send2(); pub(super) fn uv_udp_using_recvmmsg(); - pub(super) fn uv_unref(); pub(super) fn uv_update_time(); pub(super) fn uv_uptime(); pub(super) fn uv_utf16_length_as_wtf8(); @@ -4547,11 +4542,10 @@ pub(crate) fn fix_dead_code_elimination() { // `uv_functions_to_export` module above. #[cfg(unix)] { + super::uv_posix::fix_dead_code_elimination(); use uv_functions_to_export::*; keep_symbols!( uv_accept, - uv_async_init, - uv_async_send, uv_available_parallelism, uv_backend_fd, uv_backend_timeout, @@ -4559,13 +4553,11 @@ pub(crate) fn fix_dead_code_elimination() { uv_barrier_init, uv_barrier_wait, uv_buf_init, - uv_cancel, uv_chdir, uv_check_init, uv_check_start, uv_check_stop, uv_clock_gettime, - uv_close, uv_cond_broadcast, uv_cond_destroy, uv_cond_init, @@ -4575,7 +4567,6 @@ pub(crate) fn fix_dead_code_elimination() { uv_cpu_info, uv_cpumask_size, uv_cwd, - uv_default_loop, uv_disable_stdio_inheritance, uv_dlclose, uv_dlerror, @@ -4658,7 +4649,6 @@ pub(crate) fn fix_dead_code_elimination() { uv_handle_set_data, uv_handle_size, uv_handle_type_name, - uv_has_ref, uv_hrtime, uv_idle_init, uv_idle_start, @@ -4673,8 +4663,6 @@ pub(crate) fn fix_dead_code_elimination() { uv_ip4_name, uv_ip6_addr, uv_ip6_name, - uv_is_active, - uv_is_closing, uv_is_readable, uv_is_writable, uv_key_create, @@ -4748,12 +4736,10 @@ pub(crate) fn fix_dead_code_elimination() { uv_print_all_handles, uv_process_get_pid, uv_process_kill, - uv_queue_work, uv_random, uv_read_start, uv_read_stop, uv_recv_buffer_size, - uv_ref, uv_replace_allocator, uv_req_get_data, uv_req_get_type, @@ -4853,7 +4839,6 @@ pub(crate) fn fix_dead_code_elimination() { uv_udp_try_send, uv_udp_try_send2, uv_udp_using_recvmmsg, - uv_unref, uv_update_time, uv_uptime, uv_utf16_length_as_wtf8, diff --git a/src/runtime/napi/uv_posix.rs b/src/runtime/napi/uv_posix.rs new file mode 100644 index 000000000000..823d27249747 --- /dev/null +++ b/src/runtime/napi/uv_posix.rs @@ -0,0 +1,783 @@ +//! The loop-backed part of libuv's API for N-API addons on posix: `uv_async_t`, +//! `uv_queue_work`, `uv_default_loop`, and the `uv_handle_t` functions for the +//! handle type an addon can create here. +//! +//! Bun does not run a libuv loop on posix. Every other `uv_*` symbol is a crash +//! stub (`src/jsc/bindings/uv-posix-stubs.c`) or a loop-free polyfill +//! (`src/jsc/bindings/uv-posix-polyfills.c`). The functions here keep libuv's +//! ABI and its threading contract, and map the loop onto the VM's event loop: +//! +//! - The addon allocates the `uv_async_t` / `uv_work_t` and reads `data`, +//! `loop` and `type` from it, so those fields sit where `uv.h` puts them +//! ([`UvHandle`], [`UvReq`]). The private fields behind them are Bun's. +//! - As in libuv, only `uv_async_send` and `uv_cancel` may be called from any +//! thread. Everything else runs on the loop's JS thread, the handle memory +//! stays valid until `close_cb` has run, and the request until +//! `after_work_cb` has run. +//! - A `uv_loop_t*` is a [`UvLoop`]: one per VM, embedded in its +//! `RuntimeState`. Only its first word, `data`, is part of the ABI. +//! - `uv_async_send` sets the handle's `pending` flag and posts at most one +//! dispatch task per loop through the VM's [`VmHandle`], coalescing sends +//! the way libuv's eventfd does. The task walks the loop's live handles on +//! the JS thread, as libuv's `uv__async_io` does. +//! - `uv_queue_work` is a [`Job`]: `work_cb` on the work pool, `after_work_cb` +//! from the job's completion on the JS thread. + +use core::cell::Cell; +use core::ffi::{CStr, c_char, c_int, c_uint, c_void}; +use core::ptr::NonNull; +use core::sync::atomic::{AtomicI32, AtomicU8, AtomicU32, Ordering}; + +use bun_io::KeepAlive; +use bun_jsc::event_loop::ConcurrentTaskItem as ConcurrentTask; +use bun_jsc::virtual_machine::VirtualMachine; +use bun_jsc::{ + Completion, JSGlobalObject, Job, JobContext, JsCell, JsError, JsPtr, JsResult, JsThread, + LoopKind, Posted, VmHandle, +}; + +use crate::jsc_hooks::RuntimeState; + +bun_output::declare_scope!(uv, hidden); + +unsafe extern "C" { + /// Crashes with the report the stubs in `uv-posix-stubs.c` produce. `name` + /// is kept by pointer, so it must be static. Does not return. + fn CrashHandler__unsupportedUVFunction(name: *const c_char); +} + +#[cold] +fn unsupported(function: &'static CStr) -> ! { + // SAFETY: `function` is a static NUL-terminated string. + unsafe { CrashHandler__unsupportedUVFunction(function.as_ptr()) }; + unreachable!("CrashHandler__unsupportedUVFunction returned"); +} + +// `UV_E*` on posix is `-errno` (uv/errno.h). +const UV_EINVAL: c_int = -libc::EINVAL; +const UV_EBUSY: c_int = -libc::EBUSY; +const UV_ECANCELED: c_int = -libc::ECANCELED; + +/// `uv_handle_type` / `uv_req_type` members (`UV_HANDLE_TYPE_MAP` / +/// `UV_REQ_TYPE_MAP` in uv.h). +const UV_ASYNC: c_uint = 1; +const UV_WORK: c_uint = 7; + +/// The `uv_handle_t.flags` bits libuv itself uses for these states +/// (src/uv-common.h). Whether the handle refs the loop is the [`KeepAlive`] +/// in [`UvAsync`], not a flag. +const UV_HANDLE_CLOSING: c_uint = 0x01; +const UV_HANDLE_CLOSED: c_uint = 0x02; + +/// `sizeof(uv_async_t)` and `sizeof(uv_work_t)` on 64-bit unix: the addon +/// allocates both, so Bun's private fields must fit behind the public ones. +const UV_ASYNC_T_SIZE: usize = 128; +const UV_WORK_T_SIZE: usize = 128; + +type UvAsyncCb = unsafe extern "C" fn(*mut UvAsync); +type UvCloseCb = unsafe extern "C" fn(*mut UvHandle); +type UvWorkCb = unsafe extern "C" fn(*mut UvWork); +type UvAfterWorkCb = unsafe extern "C" fn(*mut UvWork, c_int); + +// ────────────────────────────────────────────────────────────────────────── +// uv_loop_t +// ────────────────────────────────────────────────────────────────────────── + +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq)] +enum DispatchState { + /// No dispatch task is queued. + Idle = 0, + /// A dispatch task is queued, or the running one will go around again. + Pending = 1, + /// The dispatch task is walking the handles. + Running = 2, +} + +/// What a `uv_loop_t*` points at on posix. One per VM, owned by its +/// `RuntimeState`, so it lives exactly as long as the VM: the main thread's +/// for the whole process (like libuv's default loop), a Worker's until the +/// Worker exits (by when, as in libuv, the addon must have closed its handles). +#[repr(C)] +pub(crate) struct UvLoop { + /// `uv_loop_t.data`, at offset 0 as in uv.h. The addon reads and writes it, + /// directly or through `uv_loop_{get,set}_data` (uv-posix-polyfills.c); + /// Bun never touches it. + data: Cell<*mut c_void>, + vm: NonNull, + /// How `uv_async_send`, on any thread, reaches the JS thread. Uncounted: + /// the loop is VM-owned, so it must not be something the VM waits for. + handle: VmHandle, + dispatch_state: AtomicU8, + /// The initialised, not yet closed async handles. JS thread. + asyncs: JsCell>>, + /// While a dispatch pass runs, the handles it has not visited yet (in + /// reverse order). `uv_close` removes from both lists, so a handle closed + /// by a callback is never visited afterwards. JS thread. + dispatching: JsCell>>, +} + +const _: () = assert!(core::mem::offset_of!(UvLoop, data) == 0); + +impl UvLoop { + /// JS thread, from `init_runtime_state`. `handle` is `vm`'s [`VmHandle`]. + pub(crate) fn new(vm: NonNull, handle: VmHandle) -> UvLoop { + UvLoop { + data: Cell::new(core::ptr::null_mut()), + vm, + handle, + dispatch_state: AtomicU8::new(DispatchState::Idle as u8), + asyncs: JsCell::new(Vec::new()), + dispatching: JsCell::new(Vec::new()), + } + } + + /// The loop of `vm`, or null when `vm` has no `RuntimeState` (a `bun_jsc` + /// unit test, or a Worker's VM after its teardown). + /// + /// # Safety + /// `vm` points at a live or never-freed `VirtualMachine`. Any thread: this + /// reads one field, `runtime_state`, which is written before the VM runs + /// any script (so before an addon exists) and again only by a Worker's + /// teardown, after which no conforming addon uses that Worker's loop. + pub(crate) unsafe fn of_vm(vm: *const VirtualMachine) -> *mut UvLoop { + // SAFETY: fn contract. + let state = unsafe { (*vm).runtime_state }.cast::(); + if state.is_null() { + return core::ptr::null_mut(); + } + // SAFETY: `runtime_state` is the boxed `RuntimeState` of `vm`; + // projecting to a field forms no reference. + unsafe { &raw mut (*state).uv_loop } + } + + /// JS thread only. + fn js_thread(&self) -> JsThread<'static> { + // SAFETY: the VM owns the `RuntimeState` this loop is embedded in, so it + // is alive; `global()` is the JS-thread accessor every host function uses. + unsafe { self.vm.as_ref() }.global().js_thread() + } + + /// Any thread. Makes sure a dispatch pass runs after this point; at most + /// one task is queued per loop however many handles are sent + /// (`ThreadSafeFunction::schedule_dispatch` has the same state machine). + fn schedule_dispatch(&self) { + let prev = self + .dispatch_state + .swap(DispatchState::Pending as u8, Ordering::SeqCst); + if prev != DispatchState::Idle as u8 { + // Queued already, or the running pass will go around again. + return; + } + let this: *const UvLoop = self; + let task = ConcurrentTask::from_callback(this.cast_mut(), UvLoop::dispatch); + if let Posted::Refused(task) = self.handle.post(LoopKind::Regular, task) { + // The VM is gone. As with a send to a closed libuv loop, the + // callback is lost; stay consistent so a later send does not + // believe a task is queued. + // SAFETY: refused ⇒ the task was never queued and is ours to free. + unsafe { ConcurrentTask::release_refused(task) }; + self.dispatch_state + .store(DispatchState::Idle as u8, Ordering::SeqCst); + } + } + + /// JS thread, the task `schedule_dispatch` posted. + fn dispatch(this: *mut UvLoop) -> JsResult<()> { + // SAFETY: the task was dispatched by this loop's VM, which owns the + // `RuntimeState` the loop is embedded in, so `this` is live. Other + // threads only touch `dispatch_state` and `handle` through their own + // shared references, which this one may coexist with. + let this = unsafe { &*this }; + let global = this.js_thread().global(); + loop { + this.dispatch_state + .store(DispatchState::Running as u8, Ordering::SeqCst); + // A stopping VM takes no more callbacks, like a threadsafe + // function's; close callbacks still run (`UvAsync::run_close`). + if global.bun_vm().script_allowed() { + this.run_pass(global); + } + // A send that arrived during the pass found `Running`, so it did not + // post a task: it set `Pending`, this exchange fails, and the next + // pass picks its handle up. (A plain store of `Idle` would lose it.) + if this + .dispatch_state + .compare_exchange( + DispatchState::Running as u8, + DispatchState::Idle as u8, + Ordering::SeqCst, + Ordering::SeqCst, + ) + .is_ok() + { + return Ok(()); + } + } + } + + /// One walk over the handles that were live when it started: libuv's + /// `uv__async_io`. Like libuv, a handle goes back into the live list before + /// its callback runs, so a `uv_close` from inside any callback finds it. + /// A handle initialised by a callback lands in the live list and, if it is + /// sent, in the next pass. A callback's uncaught exception is reported and + /// the walk goes on; the VM's termination ends it. + fn run_pass(&self, global: &JSGlobalObject) { + self.asyncs.with_mut(|live| { + self.dispatching.with_mut(|todo| { + debug_assert!(todo.is_empty()); + core::mem::swap(live, todo); + todo.reverse(); + }) + }); + while let Some(async_) = self.dispatching.with_mut(Vec::pop) { + self.asyncs.with_mut(|live| live.push(async_)); + // SAFETY: a registered handle is initialised and not closed: + // `uv_close` unregisters it before returning, and the addon keeps an + // unclosed handle's memory alive (libuv's contract). + if !unsafe { UvAsync::take_pending(async_) } { + continue; + } + bun_output::scoped_log!(uv, "uv_async_t {:?}: async_cb", async_); + // SAFETY: as above. `async_cb` is read before the call because the + // callback may close and free the handle. + if let Some(async_cb) = unsafe { (*async_.as_ptr()).async_cb } { + // SAFETY: the addon's callback, on the loop thread, with the + // handle it initialised. + unsafe { async_cb(async_.as_ptr()) }; + } + if global.has_exception() + && bun_jsc::task::report_error_or_terminate(global, JsError::Thrown).is_err() + { + break; + } + } + // Only a termination leaves anything here. Put it back, in order. + self.dispatching.with_mut(|todo| { + if !todo.is_empty() { + self.asyncs + .with_mut(|live| live.extend(todo.drain(..).rev())); + } + }); + } + + /// JS thread. + fn register(&self, async_: NonNull) { + self.asyncs.with_mut(|live| live.push(async_)); + } + + /// JS thread. A handle may be in either list (a close from inside a + /// dispatch pass) and, if an addon initialised it twice, more than once. + fn unregister(&self, async_: NonNull) { + self.asyncs.with_mut(|live| live.retain(|h| *h != async_)); + self.dispatching + .with_mut(|todo| todo.retain(|h| *h != async_)); + } +} + +/// `uv_loop_t* uv_default_loop(void)`: the main thread's loop, from any +/// thread, like libuv's process-wide default loop. Null when there is no main +/// JS thread (libuv returns null when the default loop cannot be set up). +/// +/// An addon loaded in a Worker gets the main thread's loop from this, as it +/// does in Node; `napi_get_uv_event_loop` is how it gets its own. +#[unsafe(no_mangle)] +pub(crate) extern "C" fn uv_default_loop() -> *mut UvLoop { + let Some(vm) = VirtualMachine::get_main_thread_vm() else { + return core::ptr::null_mut(); + }; + // SAFETY: the main thread's VM is never freed. + unsafe { UvLoop::of_vm(vm) } +} + +// ────────────────────────────────────────────────────────────────────────── +// uv_handle_t / uv_async_t +// ────────────────────────────────────────────────────────────────────────── + +/// `UV_HANDLE_FIELDS` (uv.h) and `UV_HANDLE_PRIVATE_FIELDS` (uv/unix.h): the +/// common prefix of every handle type, and the view `uv_close` and friends +/// get. `data`, `loop` and `type` are read by addons and must stay where uv.h +/// puts them; the rest is libuv's private state, used here as libuv uses it. +#[repr(C)] +pub(crate) struct UvHandle { + data: *mut c_void, + loop_: *mut UvLoop, + type_: c_uint, + close_cb: Option, + handle_queue: [*mut c_void; 2], + u: [*mut c_void; 4], + next_closing: *mut c_void, + flags: c_uint, +} + +const _: () = assert!(core::mem::offset_of!(UvHandle, data) == 0); +const _: () = assert!(core::mem::offset_of!(UvHandle, loop_) == 8); +const _: () = assert!(core::mem::offset_of!(UvHandle, type_) == 16); +const _: () = assert!(core::mem::offset_of!(UvHandle, close_cb) == 24); +const _: () = assert!(core::mem::offset_of!(UvHandle, flags) == 88); +const _: () = assert!(core::mem::size_of::() == 96); + +impl UvHandle { + /// Sets up the common prefix as libuv's `uv__handle_init` does. `data` is + /// the addon's and is left alone: addons commonly set it before init. + fn init(this: *mut UvHandle, loop_: *mut UvLoop, type_: c_uint) { + // SAFETY: `this` is the addon's handle memory, on the loop thread; + // field-wise writes because the memory is uninitialised. + unsafe { + (&raw mut (*this).loop_).write(loop_); + (&raw mut (*this).type_).write(type_); + (&raw mut (*this).close_cb).write(None); + (&raw mut (*this).handle_queue).write([core::ptr::null_mut(); 2]); + (&raw mut (*this).u).write([core::ptr::null_mut(); 4]); + (&raw mut (*this).next_closing).write(core::ptr::null_mut()); + (&raw mut (*this).flags).write(0); + } + } +} + +/// `struct uv_async_s`: the [`UvHandle`] prefix, then libuv's +/// `UV_ASYNC_PRIVATE_FIELDS`, of which Bun keeps `async_cb` and `pending` +/// and uses the rest for its own state. +#[repr(C)] +pub(crate) struct UvAsync { + handle: UvHandle, + async_cb: Option, + /// Whether the handle keeps the process alive (`uv_ref` / `uv_unref`). + /// JS thread. + keep_alive: KeepAlive, + /// Threads inside `uv_async_send`. libuv's busy counter: `uv_close` waits + /// for it to reach zero, so once it returns no thread is still reading the + /// handle and `close_cb` may free it. + busy: AtomicI32, + /// Set by `uv_async_send`, taken by the dispatch pass. + pending: AtomicI32, +} + +const _: () = assert!(core::mem::offset_of!(UvAsync, handle) == 0); +const _: () = assert!(core::mem::offset_of!(UvAsync, async_cb) == 96); +const _: () = assert!(core::mem::size_of::() <= UV_ASYNC_T_SIZE); + +// No function below forms a reference to a whole handle: while the loop +// thread is inside one of them, other threads may be in `uv_async_send` on the +// same handle, which is fine for its atomics but not under a `&UvAsync` or +// `&mut UvAsync` covering them. Fields are read and written through the raw +// pointer, and a reference is formed to one field at a time. +impl UvAsync { + /// Any thread. Clears `pending`; true if it was set. Then, as libuv's + /// `uv__async_spin`, waits until no thread is inside `uv_async_send` on + /// this handle. That window is a flag exchange and a queue push, so the + /// wait is short; the yield is for a sender preempted inside it. + /// + /// # Safety + /// `this` is an initialised, not yet closed handle. + unsafe fn take_pending(this: NonNull) -> bool { + // SAFETY: fn contract. + let (pending, busy) = unsafe { (&(*this.as_ptr()).pending, &(*this.as_ptr()).busy) }; + let was_pending = pending.swap(0, Ordering::SeqCst) != 0; + let mut spins = 0u32; + while busy.load(Ordering::SeqCst) != 0 { + spins += 1; + if spins.is_multiple_of(1000) { + std::thread::yield_now(); + } else { + core::hint::spin_loop(); + } + } + was_pending + } + + /// JS thread, the task `uv_close` posted: `close_cb`, one loop turn later. + /// The handle is the addon's again once the callback returns (it usually + /// frees it), so nothing touches it afterwards. + fn run_close(this: *mut UvAsync) -> JsResult<()> { + // SAFETY: `uv_close` posted this for a handle the addon keeps alive + // until `close_cb` has run. + let (close_cb, loop_) = unsafe { + (*this).handle.flags |= UV_HANDLE_CLOSED; + ((*this).handle.close_cb, (*this).handle.loop_) + }; + bun_output::scoped_log!(uv, "uv_async_t {:?}: close_cb", this); + if let Some(close_cb) = close_cb { + // SAFETY: the addon's callback, on the loop thread, with its handle. + unsafe { close_cb(this.cast::()) }; + } + // SAFETY: the loop outlives its handles (it is the VM's). + let global = unsafe { &*loop_ }.js_thread().global(); + if global.has_exception() { + return Err(JsError::Thrown); + } + Ok(()) + } +} + +/// `int uv_async_init(uv_loop_t*, uv_async_t*, uv_async_cb)`. Loop thread. +/// The handle starts active and ref'd, as in libuv, so it keeps the process +/// alive until it is unref'd or closed. +/// +/// # Safety +/// `loop_` is null or a loop this VM handed out; `handle` points at +/// `sizeof(uv_async_t)` bytes the addon keeps alive until its `close_cb` ran. +#[unsafe(no_mangle)] +pub(crate) unsafe extern "C" fn uv_async_init( + loop_: *mut UvLoop, + handle: *mut UvAsync, + async_cb: Option, +) -> c_int { + let (Some(loop_ref), Some(async_)) = (NonNull::new(loop_), NonNull::new(handle)) else { + return UV_EINVAL; + }; + bun_output::scoped_log!(uv, "uv_async_t {:?}: uv_async_init", handle); + UvHandle::init(handle.cast::(), loop_, UV_ASYNC); + // SAFETY: fn contract; field-wise writes into uninitialised addon memory. + unsafe { + (&raw mut (*handle).async_cb).write(async_cb); + (&raw mut (*handle).keep_alive).write(KeepAlive::default()); + (&raw mut (*handle).busy).write(AtomicI32::new(0)); + (&raw mut (*handle).pending).write(AtomicI32::new(0)); + (*handle).keep_alive.ref_(bun_io::js_vm_ctx()); + } + // SAFETY: the loop is alive (fn contract); JS thread. + unsafe { loop_ref.as_ref() }.register(async_); + 0 +} + +/// `int uv_async_send(uv_async_t*)`. Any thread; the first send after a +/// dispatch schedules one, later ones until then are coalesced into it. +/// Legal until `close_cb` runs: after `uv_close` it sets flags nobody reads. +/// +/// # Safety +/// `handle` was initialised by `uv_async_init` and its `close_cb` has not run. +#[unsafe(no_mangle)] +pub(crate) unsafe extern "C" fn uv_async_send(handle: *mut UvAsync) -> c_int { + // The loop thread may be in `uv_close` on this handle, writing its plain + // fields; the ones used here are the atomics and `loop`, which only + // `uv_async_init` writes, before the handle can reach another thread. + // SAFETY: fn contract. + let (pending, busy, loop_) = + unsafe { (&(*handle).pending, &(*handle).busy, (*handle).handle.loop_) }; + if pending.load(Ordering::SeqCst) != 0 { + return 0; + } + let _ = busy.fetch_add(1, Ordering::SeqCst); + if pending.swap(1, Ordering::SeqCst) == 0 { + // SAFETY: the loop outlives the handle (see `UvLoop`); only its + // thread-safe fields are used here. + unsafe { &*loop_ }.schedule_dispatch(); + } + let _ = busy.fetch_sub(1, Ordering::SeqCst); + 0 +} + +/// The async handle behind a `uv_handle_t*`, for the functions that take any +/// handle type. Only async handles can be initialised on posix, so anything +/// else is memory no `uv_*_init` here wrote: crash the way the stub for +/// `function` did, with its name. +/// +/// # Safety +/// `handle` points at an initialised handle. +unsafe fn as_async(handle: *mut UvHandle, function: &'static CStr) -> NonNull { + // SAFETY: fn contract. + if unsafe { (*handle).type_ } != UV_ASYNC { + unsupported(function); + } + // `type` says `uv_async_init` initialised this memory as a `UvAsync`. + NonNull::new(handle.cast::()).expect("dereferenced above") +} + +/// `void uv_close(uv_handle_t*, uv_close_cb)`. Loop thread. Stops the handle +/// at once (no callback runs after this returns, and no `uv_async_send` is +/// still inside it) and runs `close_cb` from the loop later, as libuv does, +/// since addons free the handle there. Closing twice does nothing. +/// +/// # Safety +/// `handle` was initialised by a `uv_*_init` of this module. +#[unsafe(no_mangle)] +pub(crate) unsafe extern "C" fn uv_close(handle: *mut UvHandle, close_cb: Option) { + // SAFETY: fn contract. + let this = unsafe { as_async(handle, c"uv_close") }; + let async_ = this.as_ptr(); + // SAFETY: initialised (`as_async`); loop thread, so nothing else writes + // the plain fields or uses the keep-alive. + let loop_ = unsafe { + if (*async_).handle.flags & UV_HANDLE_CLOSING != 0 { + return; + } + (*async_).handle.flags |= UV_HANDLE_CLOSING; + (*async_).handle.close_cb = close_cb; + (*async_).keep_alive.unref(bun_io::js_vm_ctx()); + // The loop outlives its handles. + &*(*async_).handle.loop_ + }; + bun_output::scoped_log!(uv, "uv_async_t {:?}: uv_close", handle); + loop_.unregister(this); + // SAFETY: initialised and, until this line, not closed. + let _ = unsafe { UvAsync::take_pending(this) }; + // The queued task keeps the loop alive until `close_cb` has run, as + // libuv's closing list does. Refused means the VM is already gone, and + // with it the turn `close_cb` would have run on. + let task = ConcurrentTask::from_callback(async_, UvAsync::run_close); + if let Posted::Refused(task) = loop_.handle.post(LoopKind::Regular, task) { + // SAFETY: refused ⇒ never queued, ours to free. + unsafe { ConcurrentTask::release_refused(task) }; + } +} + +/// `void uv_ref(uv_handle_t*)`. Loop thread. Idempotent; nothing after +/// `uv_close`, whose unref is final. +/// +/// # Safety +/// As [`uv_close`]. +#[unsafe(no_mangle)] +pub(crate) unsafe extern "C" fn uv_ref(handle: *mut UvHandle) { + // SAFETY: fn contract. + let async_ = unsafe { as_async(handle, c"uv_ref") }.as_ptr(); + // SAFETY: as in `uv_close`. + unsafe { + if (*async_).handle.flags & UV_HANDLE_CLOSING == 0 { + (*async_).keep_alive.ref_(bun_io::js_vm_ctx()); + } + } +} + +/// `void uv_unref(uv_handle_t*)`. Loop thread. Idempotent. +/// +/// # Safety +/// As [`uv_close`]. +#[unsafe(no_mangle)] +pub(crate) unsafe extern "C" fn uv_unref(handle: *mut UvHandle) { + // SAFETY: fn contract. + let async_ = unsafe { as_async(handle, c"uv_unref") }.as_ptr(); + // SAFETY: as in `uv_close`. + unsafe { (*async_).keep_alive.unref(bun_io::js_vm_ctx()) }; +} + +/// `int uv_has_ref(const uv_handle_t*)`. Loop thread. +/// +/// # Safety +/// As [`uv_close`]. +#[unsafe(no_mangle)] +pub(crate) unsafe extern "C" fn uv_has_ref(handle: *mut UvHandle) -> c_int { + // SAFETY: fn contract. + let async_ = unsafe { as_async(handle, c"uv_has_ref") }.as_ptr(); + // SAFETY: as in `uv_close`. + c_int::from(unsafe { (*async_).keep_alive.is_active() }) +} + +/// `int uv_is_active(const uv_handle_t*)`. Loop thread. An async handle is +/// active from `uv_async_init` until `uv_close` (libuv starts it in init). +/// +/// # Safety +/// As [`uv_close`]. +#[unsafe(no_mangle)] +pub(crate) unsafe extern "C" fn uv_is_active(handle: *mut UvHandle) -> c_int { + // SAFETY: fn contract. + let async_ = unsafe { as_async(handle, c"uv_is_active") }.as_ptr(); + // SAFETY: as in `uv_close`. + c_int::from(unsafe { (*async_).handle.flags } & UV_HANDLE_CLOSING == 0) +} + +/// `int uv_is_closing(const uv_handle_t*)`. Loop thread. True from `uv_close` +/// on, `close_cb` included. +/// +/// # Safety +/// As [`uv_close`]. +#[unsafe(no_mangle)] +pub(crate) unsafe extern "C" fn uv_is_closing(handle: *mut UvHandle) -> c_int { + // SAFETY: fn contract. + let async_ = unsafe { as_async(handle, c"uv_is_closing") }.as_ptr(); + // SAFETY: as in `uv_close`. + let flags = unsafe { (*async_).handle.flags }; + c_int::from(flags & (UV_HANDLE_CLOSING | UV_HANDLE_CLOSED) != 0) +} + +// ────────────────────────────────────────────────────────────────────────── +// uv_req_t / uv_work_t +// ────────────────────────────────────────────────────────────────────────── + +/// `UV_REQ_FIELDS` (uv.h): the common prefix of every request type, and the +/// view `uv_cancel` gets. `data` and `type` are the addon's to read. +#[repr(C)] +pub(crate) struct UvReq { + data: *mut c_void, + type_: c_uint, + reserved: [*mut c_void; 6], +} + +const _: () = assert!(core::mem::offset_of!(UvReq, data) == 0); +const _: () = assert!(core::mem::offset_of!(UvReq, type_) == 8); +const _: () = assert!(core::mem::size_of::() == 64); + +#[repr(u32)] +#[derive(Clone, Copy, PartialEq, Eq)] +enum WorkState { + /// On the pool's queue: `uv_cancel` can still take it. + Queued = 0, + /// `work_cb` started (or finished): too late to cancel. + Running = 1, + /// `uv_cancel` took it: `after_work_cb` gets `UV_ECANCELED`. + Cancelled = 2, +} + +/// `struct uv_work_s`: the [`UvReq`] prefix and the three fields uv.h +/// declares after it (`loop`, `work_cb`, `after_work_cb` are read by addons), +/// then Bun's state where libuv keeps its `struct uv__work`. +#[repr(C)] +pub(crate) struct UvWork { + req: UvReq, + loop_: *mut UvLoop, + work_cb: Option, + after_work_cb: Option, + /// A [`WorkState`]. The pool thread and `uv_cancel` race for it. + state: AtomicU32, +} + +const _: () = assert!(core::mem::offset_of!(UvWork, req) == 0); +const _: () = assert!(core::mem::offset_of!(UvWork, loop_) == 64); +const _: () = assert!(core::mem::offset_of!(UvWork, work_cb) == 72); +const _: () = assert!(core::mem::offset_of!(UvWork, after_work_cb) == 80); +const _: () = assert!(core::mem::size_of::() <= UV_WORK_T_SIZE); + +/// The [`Job`] behind one `uv_queue_work`. Its off-thread half is the request +/// itself: the addon keeps it alive until `after_work_cb` has run, which is +/// the job's whole life, so the pool may read it through the [`JsPtr`]. +struct UvWorkJob; + +impl JobContext for UvWorkJob { + type OffThread = JsPtr; + type Js = (); + + // As with the handles above, no reference to a whole request is formed: + // `uv_cancel` may be running against it on another thread. Fields are + // read through the raw pointer, `state` borrowed on its own. + + fn run(req: &mut JsPtr, done: Completion) -> Option> { + let req = req.as_ptr(); + // SAFETY: the request is alive for the job's life (see `UvWorkJob`); + // `work_cb` was written before the job was scheduled. + let started = unsafe { &(*req).state } + .compare_exchange( + WorkState::Queued as u32, + WorkState::Running as u32, + Ordering::SeqCst, + Ordering::SeqCst, + ) + .is_ok(); + // SAFETY: as above. + if started && let Some(work_cb) = unsafe { (*req).work_cb } { + // SAFETY: the addon's callback, on a pool thread, as uv_queue_work + // documents. + unsafe { work_cb(req) }; + } + Some(done) + } + + fn then(req: JsPtr, _: (), cx: &JsThread<'_>) -> JsResult<()> { + let req = req.as_ptr(); + // SAFETY: as in `run`. `state` is final: the pool thread took it, or + // `uv_cancel` did. + let (state, after_work_cb) = + unsafe { ((*req).state.load(Ordering::SeqCst), (*req).after_work_cb) }; + let status = if state == WorkState::Cancelled as u32 { + UV_ECANCELED + } else { + 0 + }; + bun_output::scoped_log!(uv, "uv_work_t {:?}: after_work_cb({})", req, status); + // The request is the addon's again once the callback returns (it + // usually frees it), so nothing touches it afterwards. + if let Some(after_work_cb) = after_work_cb { + // SAFETY: the addon's callback, on the loop thread. + unsafe { after_work_cb(req, status) }; + } + if cx.global().has_exception() { + return Err(JsError::Thrown); + } + Ok(()) + } +} + +/// `int uv_queue_work(uv_loop_t*, uv_work_t*, uv_work_cb, uv_after_work_cb)`. +/// Loop thread. The request keeps the process alive until `after_work_cb` has +/// run, as an active libuv request does. +/// +/// # Safety +/// `loop_` is null or a loop this VM handed out; `req` points at +/// `sizeof(uv_work_t)` bytes the addon keeps alive until `after_work_cb` ran. +#[unsafe(no_mangle)] +pub(crate) unsafe extern "C" fn uv_queue_work( + loop_: *mut UvLoop, + req: *mut UvWork, + work_cb: Option, + after_work_cb: Option, +) -> c_int { + let (Some(loop_ref), Some(req_ref), Some(_)) = ( + NonNull::new(loop_), + NonNull::new(req), + work_cb, // libuv: UV_EINVAL too + ) else { + return UV_EINVAL; + }; + bun_output::scoped_log!(uv, "uv_work_t {:?}: uv_queue_work", req); + // SAFETY: fn contract; field-wise writes into uninitialised addon memory. + // `data` is the addon's and is left alone, as libuv leaves it. + unsafe { + (&raw mut (*req).req.type_).write(UV_WORK); + (&raw mut (*req).req.reserved).write([core::ptr::null_mut(); 6]); + (&raw mut (*req).loop_).write(loop_); + (&raw mut (*req).work_cb).write(work_cb); + (&raw mut (*req).after_work_cb).write(after_work_cb); + (&raw mut (*req).state).write(AtomicU32::new(WorkState::Queued as u32)); + } + // SAFETY: the loop is alive (fn contract); JS thread. + let cx = unsafe { loop_ref.as_ref() }.js_thread(); + // SAFETY: the request outlives the job (fn contract, see `UvWorkJob`). + Job::::schedule(&cx, unsafe { JsPtr::new(req_ref) }, ()); + 0 +} + +/// `int uv_cancel(uv_req_t*)`. Any thread. Only work requests exist here; +/// libuv also answers `UV_EINVAL` for a request type it cannot cancel. +/// `0` means `work_cb` will not run and `after_work_cb` gets `UV_ECANCELED`; +/// `UV_EBUSY` means the work already started (or finished, or was cancelled). +/// +/// # Safety +/// `req` is null or a request queued by `uv_queue_work` whose `after_work_cb` +/// has not returned. +#[unsafe(no_mangle)] +pub(crate) unsafe extern "C" fn uv_cancel(req: *mut UvReq) -> c_int { + if req.is_null() { + return UV_EINVAL; + } + // SAFETY: fn contract; `type` is written before the request is queued. + if unsafe { (*req).type_ } != UV_WORK { + return UV_EINVAL; + } + // SAFETY: `type` says `uv_queue_work` initialised this memory as a + // `UvWork`; only its atomic is touched, as the pool thread may be racing. + let state = unsafe { &(*req.cast::()).state }; + match state.compare_exchange( + WorkState::Queued as u32, + WorkState::Cancelled as u32, + Ordering::SeqCst, + Ordering::SeqCst, + ) { + Ok(_) => 0, + Err(_) => UV_EBUSY, + } +} + +pub(crate) fn fix_dead_code_elimination() { + bun_core::keep_symbols!( + uv_async_init, + uv_async_send, + uv_cancel, + uv_close, + uv_default_loop, + uv_has_ref, + uv_is_active, + uv_is_closing, + uv_queue_work, + uv_ref, + uv_unref, + ); +} diff --git a/test/napi/node-napi-tests/test/node-api/test_async_cleanup_hook/do.test.ts b/test/napi/node-napi-tests/test/node-api/test_async_cleanup_hook/do.test.ts index 00c73b924f14..5fe811d00f8a 100644 --- a/test/napi/node-napi-tests/test/node-api/test_async_cleanup_hook/do.test.ts +++ b/test/napi/node-napi-tests/test/node-api/test_async_cleanup_hook/do.test.ts @@ -6,7 +6,10 @@ test("build", async () => { }); for (const file of Array.from(new Bun.Glob("*.js").scanSync(import.meta.dir))) { - // crash inside uv_async_init + // Env teardown calls the async cleanup hooks but does not turn the loop + // until they call napi_remove_async_cleanup_hook (Node does), so the + // uv_async_t each hook sends never gets its callbacks before the finalizer + // asserts on the count. test.todoIf(["test.js"].includes(file))(file, () => { run(dirname(import.meta.dir), basename(import.meta.dir) + sep + file); }); diff --git a/test/napi/node-napi-tests/test/node-api/test_callback_scope/do.test.ts b/test/napi/node-napi-tests/test/node-api/test_callback_scope/do.test.ts index b415716c512f..5e4faf615a48 100644 --- a/test/napi/node-napi-tests/test/node-api/test_callback_scope/do.test.ts +++ b/test/napi/node-napi-tests/test/node-api/test_callback_scope/do.test.ts @@ -7,12 +7,13 @@ test("build", async () => { }); for (const file of Array.from(new Bun.Glob("*.js").scanSync(import.meta.dir))) { - // crash inside uv_queue_work - // https://github.com/oven-sh/bun/issues/12827 is the latter - test.todoIf(["test-resolve-async.js", "test-async-hooks.js"].includes(file) || (file === "test.js" && isWindows))( - file, - () => { - run(dirname(import.meta.dir), basename(import.meta.dir) + sep + file); - }, - ); + // test-async-hooks.js: https://github.com/oven-sh/bun/issues/12827 + // test-resolve-async.js on Windows: the process exits before the + // after_work_cb of a uv_queue_work on napi_get_uv_event_loop's loop runs + // ("Mismatched noop function calls. Expected exactly 1, actual 0"). + test.todoIf( + file === "test-async-hooks.js" || (["test.js", "test-resolve-async.js"].includes(file) && isWindows), + )(file, () => { + run(dirname(import.meta.dir), basename(import.meta.dir) + sep + file); + }); } diff --git a/test/napi/uv-stub-stuff/plugin.c b/test/napi/uv-stub-stuff/plugin.c index 932e776ec352..4b2fdebe3fd6 100644 --- a/test/napi/uv-stub-stuff/plugin.c +++ b/test/napi/uv-stub-stuff/plugin.c @@ -45,22 +45,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) { return NULL; } - if (strcmp(buffer, "uv_async_init") == 0) { - uv_loop_t *arg0 = {0}; - uv_async_t *arg1 = {0}; - uv_async_cb arg2 = NULL; - - uv_async_init(arg0, arg1, arg2); - return NULL; - } - - if (strcmp(buffer, "uv_async_send") == 0) { - uv_async_t *arg0 = {0}; - - uv_async_send(arg0); - return NULL; - } - if (strcmp(buffer, "uv_available_parallelism") == 0) { uv_available_parallelism(); @@ -111,13 +95,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) { return NULL; } - if (strcmp(buffer, "uv_cancel") == 0) { - uv_req_t *arg0 = {0}; - - uv_cancel(arg0); - return NULL; - } - if (strcmp(buffer, "uv_chdir") == 0) { const char *arg0 = {0}; @@ -156,14 +133,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) { return NULL; } - if (strcmp(buffer, "uv_close") == 0) { - uv_handle_t *arg0 = {0}; - uv_close_cb arg1 = NULL; - - uv_close(arg0, arg1); - return NULL; - } - if (strcmp(buffer, "uv_cond_broadcast") == 0) { uv_cond_t *arg0 = {0}; @@ -231,12 +200,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) { return NULL; } - if (strcmp(buffer, "uv_default_loop") == 0) { - - uv_default_loop(); - return NULL; - } - if (strcmp(buffer, "uv_disable_stdio_inheritance") == 0) { uv_disable_stdio_inheritance(); @@ -867,13 +830,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) { return NULL; } - if (strcmp(buffer, "uv_get_osfhandle") == 0) { - int arg0 = {0}; - - uv_get_osfhandle(arg0); - return NULL; - } - if (strcmp(buffer, "uv_get_process_title") == 0) { char *arg0 = {0}; size_t arg1 = {0}; @@ -932,56 +888,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) { return NULL; } - if (strcmp(buffer, "uv_handle_get_data") == 0) { - const uv_handle_t *arg0 = {0}; - - uv_handle_get_data(arg0); - return NULL; - } - - if (strcmp(buffer, "uv_handle_get_loop") == 0) { - const uv_handle_t *arg0 = {0}; - - uv_handle_get_loop(arg0); - return NULL; - } - - if (strcmp(buffer, "uv_handle_get_type") == 0) { - const uv_handle_t *arg0 = {0}; - - uv_handle_get_type(arg0); - return NULL; - } - - if (strcmp(buffer, "uv_handle_set_data") == 0) { - uv_handle_t *arg0 = {0}; - void *arg1 = {0}; - - uv_handle_set_data(arg0, arg1); - return NULL; - } - - if (strcmp(buffer, "uv_handle_size") == 0) { - uv_handle_type arg0 = {0}; - - uv_handle_size(arg0); - return NULL; - } - - if (strcmp(buffer, "uv_handle_type_name") == 0) { - uv_handle_type arg0 = {0}; - - uv_handle_type_name(arg0); - return NULL; - } - - if (strcmp(buffer, "uv_has_ref") == 0) { - const uv_handle_t *arg0 = {0}; - - uv_has_ref(arg0); - return NULL; - } - if (strcmp(buffer, "uv_idle_init") == 0) { uv_loop_t *arg0 = {0}; uv_idle_t *arg1 = {0}; @@ -1095,20 +1001,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) { return NULL; } - if (strcmp(buffer, "uv_is_active") == 0) { - const uv_handle_t *arg0 = {0}; - - uv_is_active(arg0); - return NULL; - } - - if (strcmp(buffer, "uv_is_closing") == 0) { - const uv_handle_t *arg0 = {0}; - - uv_is_closing(arg0); - return NULL; - } - if (strcmp(buffer, "uv_is_readable") == 0) { const uv_stream_t *arg0 = {0}; @@ -1218,13 +1110,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) { return NULL; } - if (strcmp(buffer, "uv_loop_get_data") == 0) { - const uv_loop_t *arg0 = {0}; - - uv_loop_get_data(arg0); - return NULL; - } - if (strcmp(buffer, "uv_loop_init") == 0) { uv_loop_t *arg0 = {0}; @@ -1238,14 +1123,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) { return NULL; } - if (strcmp(buffer, "uv_loop_set_data") == 0) { - uv_loop_t *arg0 = {0}; - void *arg1 = {0}; - - uv_loop_set_data(arg0, arg1); - return NULL; - } - if (strcmp(buffer, "uv_loop_size") == 0) { uv_loop_size(); @@ -1274,13 +1151,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) { return NULL; } - if (strcmp(buffer, "uv_open_osfhandle") == 0) { - uv_os_fd_t arg0 = {0}; - - uv_open_osfhandle(arg0); - return NULL; - } - if (strcmp(buffer, "uv_os_environ") == 0) { uv_env_item_t **arg0 = NULL; int *arg1 = {0}; @@ -1607,16 +1477,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) { return NULL; } - if (strcmp(buffer, "uv_queue_work") == 0) { - uv_loop_t *arg0 = {0}; - uv_work_t *arg1 = {0}; - uv_work_cb arg2 = NULL; - uv_after_work_cb arg3 = NULL; - - uv_queue_work(arg0, arg1, arg2, arg3); - return NULL; - } - if (strcmp(buffer, "uv_random") == 0) { uv_loop_t *arg0 = {0}; uv_random_t *arg1 = {0}; @@ -1653,13 +1513,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) { return NULL; } - if (strcmp(buffer, "uv_ref") == 0) { - uv_handle_t *arg0 = {0}; - - uv_ref(arg0); - return NULL; - } - if (strcmp(buffer, "uv_replace_allocator") == 0) { uv_malloc_func arg0 = {0}; uv_realloc_func arg1 = {0}; @@ -1670,42 +1523,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) { return NULL; } - if (strcmp(buffer, "uv_req_get_data") == 0) { - const uv_req_t *arg0 = {0}; - - uv_req_get_data(arg0); - return NULL; - } - - if (strcmp(buffer, "uv_req_get_type") == 0) { - const uv_req_t *arg0 = {0}; - - uv_req_get_type(arg0); - return NULL; - } - - if (strcmp(buffer, "uv_req_set_data") == 0) { - uv_req_t *arg0 = {0}; - void *arg1 = {0}; - - uv_req_set_data(arg0, arg1); - return NULL; - } - - if (strcmp(buffer, "uv_req_size") == 0) { - uv_req_type arg0 = {0}; - - uv_req_size(arg0); - return NULL; - } - - if (strcmp(buffer, "uv_req_type_name") == 0) { - uv_req_type arg0 = {0}; - - uv_req_type_name(arg0); - return NULL; - } - if (strcmp(buffer, "uv_resident_set_memory") == 0) { size_t *arg0 = {0}; @@ -2406,13 +2223,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) { return NULL; } - if (strcmp(buffer, "uv_unref") == 0) { - uv_handle_t *arg0 = {0}; - - uv_unref(arg0); - return NULL; - } - if (strcmp(buffer, "uv_update_time") == 0) { uv_loop_t *arg0 = {0}; @@ -2427,18 +2237,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) { return NULL; } - if (strcmp(buffer, "uv_version") == 0) { - - uv_version(); - return NULL; - } - - if (strcmp(buffer, "uv_version_string") == 0) { - - uv_version_string(); - return NULL; - } - if (strcmp(buffer, "uv_walk") == 0) { uv_loop_t *arg0 = {0}; uv_walk_cb arg1 = NULL; diff --git a/test/napi/uv-stub-stuff/uv_impl.c b/test/napi/uv-stub-stuff/uv_impl.c index 6e75a5816866..bf3949830eb5 100644 --- a/test/napi/uv-stub-stuff/uv_impl.c +++ b/test/napi/uv-stub-stuff/uv_impl.c @@ -2,7 +2,11 @@ #include #include +#include +#include +#include #include +#include #include #include #include @@ -187,10 +191,515 @@ static napi_value test_tty_reset_mode_concurrent(napi_env env, return obj; } +// --------------------------------------------------------------------------- +// The loop-backed functions: uv_default_loop, uv_async_t, uv_queue_work, and +// the header-only helpers around them. Every test below reports back through +// a JS callback `(event, a, b)` so the test can see the order of the +// callbacks, and which thread and loop turn they ran on. The tests run these +// in a child process: on a bun whose uv_* are still stubs they abort it. +// --------------------------------------------------------------------------- + +struct reporter { + napi_env env; + napi_ref callback; +}; + +static void reporter_init(struct reporter *r, napi_env env, + napi_value callback) { + r->env = env; + napi_create_reference(env, callback, 1, &r->callback); +} + +// Calls the JS callback. The status of the call is ignored on purpose: one +// test makes the callback throw, to check the exception surfaces as uncaught. +static void report(struct reporter *r, const char *event, int32_t a, + int32_t b) { + napi_env env = r->env; + napi_handle_scope scope; + napi_open_handle_scope(env, &scope); + napi_value callback, undefined, argv[3]; + napi_get_reference_value(env, r->callback, &callback); + napi_get_undefined(env, &undefined); + napi_create_string_utf8(env, event, NAPI_AUTO_LENGTH, &argv[0]); + napi_create_int32(env, a, &argv[1]); + napi_create_int32(env, b, &argv[2]); + napi_call_function(env, undefined, callback, 3, argv, NULL); + napi_close_handle_scope(env, scope); +} + +static void reporter_destroy(struct reporter *r) { + napi_delete_reference(r->env, r->callback); +} + +static uv_loop_t *get_loop(napi_env env, bool use_default_loop) { + if (use_default_loop) + return uv_default_loop(); + uv_loop_t *loop = NULL; + napi_get_uv_event_loop(env, &loop); + return loop; +} + +static napi_value make_int32_array(napi_env env, const int32_t *values, + size_t count) { + napi_value array; + napi_create_array_with_length(env, count, &array); + for (size_t i = 0; i < count; i++) { + napi_value value; + napi_create_int32(env, values[i], &value); + napi_set_element(env, array, (uint32_t)i, value); + } + return array; +} + +static void get_args(napi_env env, napi_callback_info info, napi_value *args, + size_t count) { + size_t argc = count; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); +} + +static bool as_bool(napi_env env, napi_value value) { + bool result = false; + napi_get_value_bool(env, value, &result); + return result; +} + +// testVersion(): { version, versionString } +static napi_value test_version(napi_env env, napi_callback_info info) { + napi_value result, version, version_string; + napi_create_object(env, &result); + napi_create_uint32(env, uv_version(), &version); + napi_create_string_utf8(env, uv_version_string(), NAPI_AUTO_LENGTH, + &version_string); + napi_set_named_property(env, result, "version", version); + napi_set_named_property(env, result, "versionString", version_string); + return result; +} + +// testSizesAndNames(): the sizeof table and type names from the headers this +// addon was compiled against, which is what an addon relies on when it +// allocates handles dynamically. +static napi_value test_sizes_and_names(napi_env env, napi_callback_info info) { + napi_value result, value; + napi_create_object(env, &result); + + napi_get_boolean(env, uv_handle_size(UV_ASYNC) == sizeof(uv_async_t), &value); + napi_set_named_property(env, result, "asyncSizeMatches", value); + napi_get_boolean(env, uv_handle_size(UV_TIMER) == sizeof(uv_timer_t), &value); + napi_set_named_property(env, result, "timerSizeMatches", value); + napi_get_boolean(env, uv_handle_size(UV_UNKNOWN_HANDLE) == (size_t)-1, + &value); + napi_set_named_property(env, result, "unknownHandleSizeIsMinusOne", value); + napi_get_boolean(env, uv_req_size(UV_WORK) == sizeof(uv_work_t), &value); + napi_set_named_property(env, result, "workSizeMatches", value); + napi_get_boolean(env, uv_req_size(UV_UNKNOWN_REQ) == (size_t)-1, &value); + napi_set_named_property(env, result, "unknownReqSizeIsMinusOne", value); + napi_create_uint32(env, (uint32_t)sizeof(uv_async_t), &value); + napi_set_named_property(env, result, "asyncSize", value); + + napi_create_string_utf8(env, uv_handle_type_name(UV_ASYNC), NAPI_AUTO_LENGTH, + &value); + napi_set_named_property(env, result, "asyncName", value); + napi_create_string_utf8(env, uv_handle_type_name(UV_NAMED_PIPE), + NAPI_AUTO_LENGTH, &value); + napi_set_named_property(env, result, "pipeName", value); + napi_create_string_utf8(env, uv_req_type_name(UV_WORK), NAPI_AUTO_LENGTH, + &value); + napi_set_named_property(env, result, "workName", value); + napi_get_boolean(env, uv_handle_type_name(UV_UNKNOWN_HANDLE) == NULL, &value); + napi_set_named_property(env, result, "unknownHandleNameIsNull", value); + napi_get_boolean(env, uv_req_type_name(UV_UNKNOWN_REQ) == NULL, &value); + napi_set_named_property(env, result, "unknownReqNameIsNull", value); + return result; +} + +// testOsfhandle(fd): uv_open_osfhandle(uv_get_osfhandle(fd)), the identity +// on unix. +static napi_value test_osfhandle(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value args[1]; + napi_get_cb_info(env, info, &argc, args, NULL, NULL); + int32_t fd = -1; + napi_get_value_int32(env, args[0], &fd); + napi_value result; + napi_create_int32(env, uv_open_osfhandle(uv_get_osfhandle(fd)), &result); + return result; +} + +static void *call_uv_default_loop(void *out) { + *(uv_loop_t **)out = uv_default_loop(); + return NULL; +} + +// testLoops(): how the loops relate. On the main thread the loop +// napi_get_uv_event_loop returns is uv_default_loop(); in a Worker it is the +// Worker's own. uv_default_loop() is the same loop from any thread, and its +// `data` slot belongs to the addon. +static napi_value test_loops(napi_env env, napi_callback_info info) { + uv_loop_t *napi_loop = get_loop(env, false); + uv_loop_t *default_loop = uv_default_loop(); + uv_loop_t *default_loop_from_thread = NULL; + pthread_t thread; + if (pthread_create(&thread, NULL, call_uv_default_loop, + &default_loop_from_thread) != 0) { + napi_throw_error(env, NULL, "pthread_create failed"); + return NULL; + } + pthread_join(thread, NULL); + + int marker; + uv_loop_set_data(napi_loop, &marker); + bool data_round_trips = + uv_loop_get_data(napi_loop) == &marker && napi_loop->data == ▮ + uv_loop_set_data(napi_loop, NULL); + + napi_value result, value; + napi_create_object(env, &result); + napi_get_boolean(env, napi_loop != NULL, &value); + napi_set_named_property(env, result, "napiLoopIsSet", value); + napi_get_boolean(env, napi_loop == default_loop, &value); + napi_set_named_property(env, result, "napiLoopIsDefaultLoop", value); + napi_get_boolean(env, default_loop_from_thread == default_loop, &value); + napi_set_named_property(env, result, "defaultLoopIsSameFromThread", value); + napi_get_boolean(env, data_round_trips, &value); + napi_set_named_property(env, result, "loopDataRoundTrips", value); + return result; +} + +// testErrors(): [uv_async_init without a loop, uv_queue_work without a +// work_cb, uv_cancel of a request that is not a work request], all UV_EINVAL. +static void unused_async_cb(uv_async_t *handle) { (void)handle; } + +static napi_value test_errors(napi_env env, napi_callback_info info) { + uv_async_t async; + uv_work_t work; + uv_req_t not_work; + memset(¬_work, 0, sizeof(not_work)); + int32_t results[3] = { + uv_async_init(NULL, &async, unused_async_cb), + uv_queue_work(get_loop(env, false), &work, NULL, NULL), + uv_cancel(¬_work), + }; + return make_int32_array(env, results, 3); +} + +// --- uv_async_t ------------------------------------------------------------- + +struct async_test { + uv_async_t handle; + struct reporter reporter; + int calls; +}; + +static void async_test_close_cb(uv_handle_t *handle) { + struct async_test *test = handle->data; + report(&test->reporter, "close", uv_is_closing(handle), + uv_handle_get_data(handle) == test); + reporter_destroy(&test->reporter); + free(test); +} + +static void async_test_cb(uv_async_t *handle) { + struct async_test *test = handle->data; + test->calls++; + report(&test->reporter, "async", test->calls, + uv_is_active((uv_handle_t *)handle)); + if (test->calls == 1) { + // A send from inside the callback must produce another callback. + uv_async_send(handle); + return; + } + uv_close((uv_handle_t *)handle, async_test_close_cb); + // Closing is immediate, the callback is not. + report(&test->reporter, "closing", uv_is_closing((uv_handle_t *)handle), + uv_is_active((uv_handle_t *)handle)); +} + +static void *send_after_a_while(void *arg) { + usleep(30 * 1000); + uv_async_send(arg); + return NULL; +} + +// testAsync(useDefaultLoop, sendFromThread, callback): events +// async 1 1 first callback; the three sends coalesced into it +// async 2 1 the callback's own send +// closing 1 0 right after uv_close +// close 1 1 the deferred close callback +// The script that calls this returns right away: the ref'd handle is the +// only thing keeping the process alive until the events have happened. +// Returns [is_active, is_closing, has_ref] as observed right after init. +static napi_value test_async(napi_env env, napi_callback_info info) { + napi_value args[3]; + get_args(env, info, args, 3); + bool use_default_loop = as_bool(env, args[0]); + bool send_from_thread = as_bool(env, args[1]); + + struct async_test *test = calloc(1, sizeof(*test)); + reporter_init(&test->reporter, env, args[2]); + test->handle.data = test; // set before init, as addons commonly do + int rc = uv_async_init(get_loop(env, use_default_loop), &test->handle, + async_test_cb); + if (rc != 0 || test->handle.data != test || + uv_handle_get_loop((uv_handle_t *)&test->handle) != + get_loop(env, use_default_loop) || + uv_handle_get_type((uv_handle_t *)&test->handle) != UV_ASYNC) { + napi_throw_error(env, NULL, "uv_async_init did not set the handle up"); + return NULL; + } + int32_t observed[3] = { + uv_is_active((uv_handle_t *)&test->handle), + uv_is_closing((uv_handle_t *)&test->handle), + uv_has_ref((uv_handle_t *)&test->handle), + }; + + if (send_from_thread) { + pthread_t thread; + if (pthread_create(&thread, NULL, send_after_a_while, &test->handle) != 0) { + napi_throw_error(env, NULL, "pthread_create failed"); + return NULL; + } + pthread_detach(thread); + } else { + // Nothing can run the callback between these, so they coalesce into one. + uv_async_send(&test->handle); + uv_async_send(&test->handle); + uv_async_send(&test->handle); + } + return make_int32_array(env, observed, 3); +} + +// testAsyncCloseWithSendPending(callback): a handle that is sent and then +// closed before the loop turns gets its close callback and nothing else. +// Returns [is_active, is_closing] as observed right after uv_close. +static napi_value test_async_close_with_send_pending(napi_env env, + napi_callback_info info) { + napi_value args[1]; + get_args(env, info, args, 1); + struct async_test *test = calloc(1, sizeof(*test)); + reporter_init(&test->reporter, env, args[0]); + uv_async_init(get_loop(env, false), &test->handle, async_test_cb); + uv_handle_set_data((uv_handle_t *)&test->handle, test); + uv_async_send(&test->handle); + uv_close((uv_handle_t *)&test->handle, async_test_close_cb); + uv_close((uv_handle_t *)&test->handle, async_test_close_cb); // ignored + int32_t observed[2] = { + uv_is_active((uv_handle_t *)&test->handle), + uv_is_closing((uv_handle_t *)&test->handle), + }; + return make_int32_array(env, observed, 2); +} + +// --- a thread and the loop thread taking turns, then racing ------------------ + +#define STRESS_ROUNDS 200 +#define STRESS_BURST 2000 + +struct stress_test { + uv_async_t handle; + struct reporter reporter; + pthread_t sender; + atomic_int callbacks; +}; + +static void *stress_sender(void *arg) { + struct stress_test *test = arg; + // Each round is one send and one callback: a send after the dispatch took + // the previous one must wake the loop again. A lost wakeup hangs here, and + // the test times out. + for (int round = 1; round <= STRESS_ROUNDS; round++) { + uv_async_send(&test->handle); + while (atomic_load(&test->callbacks) < round) + sched_yield(); + } + // The pending flag was cleared before the last round's callback ran, so + // these produce exactly one more callback, which closes the handle; the + // rest land while it is closing or after uv_close, which is allowed until + // close_cb has run. close_cb joins this thread before it frees the handle. + for (int i = 0; i < STRESS_BURST; i++) + uv_async_send(&test->handle); + return NULL; +} + +static void stress_close_cb(uv_handle_t *handle) { + struct stress_test *test = handle->data; + pthread_join(test->sender, NULL); + report(&test->reporter, "done", atomic_load(&test->callbacks), + uv_is_closing(handle)); + reporter_destroy(&test->reporter); + free(test); +} + +static void stress_async_cb(uv_async_t *handle) { + struct stress_test *test = handle->data; + if (atomic_fetch_add(&test->callbacks, 1) + 1 > STRESS_ROUNDS) + uv_close((uv_handle_t *)handle, stress_close_cb); +} + +// testAsyncStress(callback): event `done 1`. Exactly one +// callback per round, and one for the burst, which closes the handle. +static napi_value test_async_stress(napi_env env, napi_callback_info info) { + napi_value args[1]; + get_args(env, info, args, 1); + struct stress_test *test = calloc(1, sizeof(*test)); + reporter_init(&test->reporter, env, args[0]); + test->handle.data = test; + uv_async_init(get_loop(env, false), &test->handle, stress_async_cb); + if (pthread_create(&test->sender, NULL, stress_sender, test) != 0) { + napi_throw_error(env, NULL, "pthread_create failed"); + return NULL; + } + return NULL; +} + +static uv_async_t unref_test_handle; + +// testAsyncRef(): uv_ref / uv_unref are idempotent toggles; returns +// uv_has_ref after each step. Leaves the handle open and unref'd, so the +// process must exit although the handle is never closed. +static napi_value test_async_ref(napi_env env, napi_callback_info info) { + uv_handle_t *handle = (uv_handle_t *)&unref_test_handle; + uv_async_init(get_loop(env, false), &unref_test_handle, unused_async_cb); + int32_t observed[6]; + observed[0] = uv_has_ref(handle); + uv_unref(handle); + observed[1] = uv_has_ref(handle); + uv_unref(handle); + observed[2] = uv_has_ref(handle); + uv_ref(handle); + observed[3] = uv_has_ref(handle); + uv_ref(handle); + observed[4] = uv_has_ref(handle); + uv_unref(handle); + observed[5] = uv_has_ref(handle); + return make_int32_array(env, observed, 6); +} + +// --- uv_queue_work ---------------------------------------------------------- + +struct work_test { + uv_work_t req; + struct reporter reporter; + pthread_t loop_thread; + int marker; + int work_ran; + int work_ran_off_the_loop_thread; +}; + +static void work_test_work_cb(uv_work_t *req) { + struct work_test *test = req->data; + test->work_ran = 1; + test->work_ran_off_the_loop_thread = + !pthread_equal(pthread_self(), test->loop_thread); + // The script has returned by now; the request must keep the process alive. + usleep(20 * 1000); +} + +// Reports "after" with the status and a bit set per property that held: +// 1 work_cb ran 2 work_cb ran off the loop thread +// 4 after_work_cb is on the loop thread +// 8 data survived 16 req->loop is the loop it was queued on +// 32 uv_req_get_type says UV_WORK +// 64 uv_cancel of a finished request is UV_EBUSY +static void work_test_after_work_cb(uv_work_t *req, int status) { + struct work_test *test = uv_req_get_data((uv_req_t *)req); + uv_loop_t *loop = NULL; + napi_get_uv_event_loop(test->reporter.env, &loop); + int32_t held = 0; + if (test->work_ran) + held |= 1; + if (test->work_ran_off_the_loop_thread) + held |= 2; + if (pthread_equal(pthread_self(), test->loop_thread)) + held |= 4; + if (req->data == test) + held |= 8; + if (req->loop == loop) + held |= 16; + if (uv_req_get_type((uv_req_t *)req) == UV_WORK) + held |= 32; + if (uv_cancel((uv_req_t *)req) == UV_EBUSY) + held |= 64; + report(&test->reporter, "after", status, held); + reporter_destroy(&test->reporter); + free(test); +} + +static struct work_test *queue_work(napi_env env, napi_value callback) { + struct work_test *test = calloc(1, sizeof(*test)); + reporter_init(&test->reporter, env, callback); + test->loop_thread = pthread_self(); + uv_req_set_data((uv_req_t *)&test->req, test); // set before queueing + int rc = uv_queue_work(get_loop(env, false), &test->req, work_test_work_cb, + work_test_after_work_cb); + if (rc != 0) { + napi_throw_error(env, NULL, "uv_queue_work failed"); + return NULL; + } + return test; +} + +// testQueueWork(callback): events +// after 0 127 +static napi_value test_queue_work(napi_env env, napi_callback_info info) { + napi_value args[1]; + get_args(env, info, args, 1); + queue_work(env, args[0]); + return NULL; +} + +// testCancelWork(callback): queues work and cancels it at once. Returns what +// uv_cancel said: 0 when the request was still queued (the event is then +// `after UV_ECANCELED` with the work_cb bits clear), UV_EBUSY when a pool +// thread had already taken it (then `after 0` with the work_cb bits set). +static napi_value test_cancel_work(napi_env env, napi_callback_info info) { + napi_value args[1]; + get_args(env, info, args, 1); + struct work_test *test = queue_work(env, args[0]); + if (test == NULL) + return NULL; + napi_value result; + napi_create_int32(env, uv_cancel((uv_req_t *)&test->req), &result); + return result; +} + napi_value Init(napi_env env, napi_value exports) { // Register all test functions napi_value fn; + napi_create_function(env, NULL, 0, test_version, NULL, &fn); + napi_set_named_property(env, exports, "testVersion", fn); + + napi_create_function(env, NULL, 0, test_sizes_and_names, NULL, &fn); + napi_set_named_property(env, exports, "testSizesAndNames", fn); + + napi_create_function(env, NULL, 0, test_osfhandle, NULL, &fn); + napi_set_named_property(env, exports, "testOsfhandle", fn); + + napi_create_function(env, NULL, 0, test_loops, NULL, &fn); + napi_set_named_property(env, exports, "testLoops", fn); + + napi_create_function(env, NULL, 0, test_errors, NULL, &fn); + napi_set_named_property(env, exports, "testErrors", fn); + + napi_create_function(env, NULL, 0, test_async, NULL, &fn); + napi_set_named_property(env, exports, "testAsync", fn); + + napi_create_function(env, NULL, 0, test_async_close_with_send_pending, NULL, + &fn); + napi_set_named_property(env, exports, "testAsyncCloseWithSendPending", fn); + + napi_create_function(env, NULL, 0, test_async_stress, NULL, &fn); + napi_set_named_property(env, exports, "testAsyncStress", fn); + + napi_create_function(env, NULL, 0, test_async_ref, NULL, &fn); + napi_set_named_property(env, exports, "testAsyncRef", fn); + + napi_create_function(env, NULL, 0, test_queue_work, NULL, &fn); + napi_set_named_property(env, exports, "testQueueWork", fn); + + napi_create_function(env, NULL, 0, test_cancel_work, NULL, &fn); + napi_set_named_property(env, exports, "testCancelWork", fn); + napi_create_function(env, NULL, 0, test_mutex_init_destroy, NULL, &fn); napi_set_named_property(env, exports, "testMutexInitDestroy", fn); diff --git a/test/napi/uv.test.ts b/test/napi/uv.test.ts index bd26f07c80d5..a2767c70ac84 100644 --- a/test/napi/uv.test.ts +++ b/test/napi/uv.test.ts @@ -183,4 +183,203 @@ describe.if(!isWindows)("uv stubs", () => { }); expect(exitCode).toBe(0); }); + + // The loop-backed functions (uv_default_loop, uv_async_t, uv_queue_work) and + // the header-only ones around them run in a child process: the child's exit + // is part of what is tested (a ref'd handle or a queued request must keep it + // alive, an unref'd handle must not), and on a bun where these are still + // stubs the first call aborts the process. `script` sees `addon` and + // `report`, which prints one line per event the addon reports. + async function runInChild(script: string) { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const addon = require(${JSON.stringify(addonPath)}); + const report = (event, a, b) => console.log(event, a, b); + ${script} + `, + ], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr, exitCode }; + } + + test.concurrent("uv_version and uv_version_string", async () => { + const { stdout, stderr, exitCode } = await runInChild(` + console.log(JSON.stringify({ ...addon.testVersion(), reported: process.versions.uv })); + `); + expect(stderr).toBe(""); + const { version, versionString, reported } = JSON.parse(stdout); + expect(versionString).toMatch(/^\d+\.\d+\.\d+$/); + const [major, minor, patch] = versionString.split(".").map(Number); + expect(version).toBe((major << 16) | (minor << 8) | patch); + // process.versions.uv is read from the same place. + expect(reported).toBe(versionString); + expect(exitCode).toBe(0); + }); + + test.concurrent("uv_handle_size, uv_req_size and the type names", async () => { + const { stdout, stderr, exitCode } = await runInChild(`console.log(JSON.stringify(addon.testSizesAndNames()));`); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual({ + asyncSizeMatches: true, + timerSizeMatches: true, + unknownHandleSizeIsMinusOne: true, + workSizeMatches: true, + unknownReqSizeIsMinusOne: true, + // sizeof(uv_async_t) on 64-bit unix; bun's private fields must fit in it. + asyncSize: 128, + asyncName: "async", + pipeName: "pipe", + workName: "work", + unknownHandleNameIsNull: true, + unknownReqNameIsNull: true, + }); + expect(exitCode).toBe(0); + }); + + test.concurrent("uv_get_osfhandle and uv_open_osfhandle", async () => { + const { stdout, stderr, exitCode } = await runInChild( + `console.log(addon.testOsfhandle(2), addon.testOsfhandle(41));`, + ); + expect(stderr).toBe(""); + expect(stdout).toBe("2 41\n"); + expect(exitCode).toBe(0); + }); + + test.concurrent("uv_default_loop is the main thread's napi_get_uv_event_loop", async () => { + const { stdout, stderr, exitCode } = await runInChild(`console.log(JSON.stringify(addon.testLoops()));`); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual({ + napiLoopIsSet: true, + napiLoopIsDefaultLoop: true, + defaultLoopIsSameFromThread: true, + loopDataRoundTrips: true, + }); + expect(exitCode).toBe(0); + }); + + test.concurrent("UV_EINVAL for a missing loop, a missing work_cb and a non-work request", async () => { + const { stdout, stderr, exitCode } = await runInChild(`console.log(JSON.stringify(addon.testErrors()));`); + expect(stderr).toBe(""); + const EINVAL = -constants.errno.EINVAL; + expect(JSON.parse(stdout)).toEqual([EINVAL, EINVAL, EINVAL]); + expect(exitCode).toBe(0); + }); + + // Three sends before the loop turns produce one callback; a send from inside + // the callback produces another; uv_close stops the handle at once and runs + // close_cb on a later turn. The script returns right after the call, so the + // ref'd handle is what keeps the process alive until the events happen. + const asyncEvents = ["async 1 1", "async 2 1", "closing 1 0", "close 1 1", ""].join("\n"); + + test.concurrent.each([ + ["napi_get_uv_event_loop", "false"], + ["uv_default_loop", "true"], + ])("uv_async_t on the loop from %s, sent from the JS thread", async (_, useDefaultLoop) => { + const { stdout, stderr, exitCode } = await runInChild(` + const observed = addon.testAsync(${useDefaultLoop}, false, report); + console.log("after init", JSON.stringify(observed)); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("after init [1,0,1]\n" + asyncEvents); + expect(exitCode).toBe(0); + }); + + test.concurrent("uv_async_t sent from another thread after the script has returned", async () => { + const { stdout, stderr, exitCode } = await runInChild(`addon.testAsync(false, true, report);`); + expect(stderr).toBe(""); + expect(stdout).toBe(asyncEvents); + expect(exitCode).toBe(0); + }); + + test.concurrent("uv_async_t in a Worker uses the Worker's own loop", async () => { + const { stdout, stderr, exitCode } = await runInChild(` + const { Worker } = require("node:worker_threads"); + const worker = new Worker( + \` + const { parentPort } = require("node:worker_threads"); + const addon = require(${JSON.stringify(addonPath)}); + parentPort.postMessage(JSON.stringify(addon.testLoops())); + addon.testAsync(false, true, (event, a, b) => parentPort.postMessage([event, a, b].join(" "))); + \`, + { eval: true }, + ); + worker.on("message", message => console.log(message)); + worker.on("exit", code => console.log("worker exited", code)); + `); + expect(stderr).toBe(""); + expect(stdout).toBe( + JSON.stringify({ + napiLoopIsSet: true, + napiLoopIsDefaultLoop: false, + defaultLoopIsSameFromThread: true, + loopDataRoundTrips: true, + }) + + "\n" + + asyncEvents + + "worker exited 0\n", + ); + expect(exitCode).toBe(0); + }); + + test.concurrent("200 rounds of send and callback with a thread, then a burst of sends racing uv_close", async () => { + // A lost wakeup makes the sender thread wait forever, so the child hangs + // and this times out. STRESS_ROUNDS + 1 callbacks: see uv_impl.c. + const { stdout, stderr, exitCode } = await runInChild(`addon.testAsyncStress(report);`); + expect(stderr).toBe(""); + expect(stdout).toBe("done 201 1\n"); + expect(exitCode).toBe(0); + }); + + test.concurrent("uv_close with a send pending runs only close_cb, and a second uv_close is ignored", async () => { + const { stdout, stderr, exitCode } = await runInChild(` + console.log("after close", JSON.stringify(addon.testAsyncCloseWithSendPending(report))); + `); + expect(stderr).toBe(""); + expect(stdout).toBe("after close [0,1]\nclose 1 1\n"); + expect(exitCode).toBe(0); + }); + + test.concurrent("uv_ref and uv_unref toggle uv_has_ref; an unref'd handle lets the process exit", async () => { + // The handle is left open and unref'd: the child hanging here is the failure. + const { stdout, stderr, exitCode } = await runInChild(`console.log(JSON.stringify(addon.testAsyncRef()));`); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual([1, 0, 0, 1, 1, 0]); + expect(exitCode).toBe(0); + }); + + test.concurrent("an exception thrown by JS called from async_cb is uncaught", async () => { + const { stdout, stderr, exitCode } = await runInChild(` + addon.testAsync(false, false, () => { throw new Error("thrown from inside async_cb"); }); + `); + expect(stdout).toBe(""); + expect(stderr).toContain("thrown from inside async_cb"); + expect(exitCode).toBe(1); + }); + + test.concurrent("uv_queue_work runs work_cb on the pool and after_work_cb on the loop thread", async () => { + const { stdout, stderr, exitCode } = await runInChild(`addon.testQueueWork(report);`); + expect(stderr).toBe(""); + // 127: every property work_test_after_work_cb in uv_impl.c checks held. + expect(stdout).toBe("after 0 127\n"); + expect(exitCode).toBe(0); + }); + + test.concurrent("uv_cancel", async () => { + const { stdout, stderr, exitCode } = await runInChild(`console.log("cancel", addon.testCancelWork(report));`); + expect(stderr).toBe(""); + // Whether uv_cancel wins the race against the pool picking the request up + // is not deterministic, but both outcomes have exactly one shape. 124 is + // 127 without the two work_cb bits. + const cancelled = `cancel 0\nafter ${-constants.errno.ECANCELED} 124\n`; + const tooLate = `cancel ${-constants.errno.EBUSY}\nafter 0 127\n`; + expect([cancelled, tooLate]).toContain(stdout); + expect(exitCode).toBe(0); + }); }); From d2870de9465b0b9661b3c0e084e1e67ec840d67a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:31:00 +0000 Subject: [PATCH 2/4] uv_close: leave pending set so a later uv_async_send returns at its first check libuv's uv__async_spin stores pending = 1 before it waits for the busy counter. A uv_async_send that starts after the close then returns at its first load and never touches the handle again. uv_close used the dispatch pass's take_pending, which cleared the flag, so such a send went on to increment busy, post a dispatch task and decrement busy, and the decrement could land after close_cb had freed the handle. Split the two paths: take_pending for the dispatch pass, stop_sends for uv_close. The close test now also sends after the close and expects 0 and no callback. Drop an unused field from the work test struct. --- src/runtime/napi/uv_posix.rs | 66 ++++++++++++++++++++----------- test/napi/uv-stub-stuff/uv_impl.c | 12 +++--- test/napi/uv.test.ts | 4 +- 3 files changed, 51 insertions(+), 31 deletions(-) diff --git a/src/runtime/napi/uv_posix.rs b/src/runtime/napi/uv_posix.rs index 823d27249747..a351e588853f 100644 --- a/src/runtime/napi/uv_posix.rs +++ b/src/runtime/napi/uv_posix.rs @@ -10,10 +10,10 @@ //! - The addon allocates the `uv_async_t` / `uv_work_t` and reads `data`, //! `loop` and `type` from it, so those fields sit where `uv.h` puts them //! ([`UvHandle`], [`UvReq`]). The private fields behind them are Bun's. -//! - As in libuv, only `uv_async_send` and `uv_cancel` may be called from any -//! thread. Everything else runs on the loop's JS thread, the handle memory -//! stays valid until `close_cb` has run, and the request until -//! `after_work_cb` has run. +//! - As in libuv, only `uv_async_send` may be called from any thread. +//! Everything else runs on the loop's JS thread, the handle memory stays +//! valid until `close_cb` has run, and the request until `after_work_cb` +//! has run. //! - A `uv_loop_t*` is a [`UvLoop`]: one per VM, embedded in its //! `RuntimeState`. Only its first word, `data`, is part of the ABI. //! - `uv_async_send` sets the handle's `pending` flag and posts at most one @@ -345,11 +345,13 @@ pub(crate) struct UvAsync { /// Whether the handle keeps the process alive (`uv_ref` / `uv_unref`). /// JS thread. keep_alive: KeepAlive, - /// Threads inside `uv_async_send`. libuv's busy counter: `uv_close` waits - /// for it to reach zero, so once it returns no thread is still reading the - /// handle and `close_cb` may free it. + /// Threads past the first check of `uv_async_send`. libuv's busy counter: + /// `uv_close` waits for it to reach zero, so once it returns no thread is + /// still touching the handle and `close_cb` may free it. busy: AtomicI32, - /// Set by `uv_async_send`, taken by the dispatch pass. + /// Set by `uv_async_send`, cleared by the dispatch pass, and set for good + /// by `uv_close`, so that a send after the close returns at its first + /// check (libuv's `uv__async_spin` does the same). pending: AtomicI32, } @@ -363,17 +365,29 @@ const _: () = assert!(core::mem::size_of::() <= UV_ASYNC_T_SIZE); // `&mut UvAsync` covering them. Fields are read and written through the raw // pointer, and a reference is formed to one field at a time. impl UvAsync { - /// Any thread. Clears `pending`; true if it was set. Then, as libuv's - /// `uv__async_spin`, waits until no thread is inside `uv_async_send` on - /// this handle. That window is a flag exchange and a queue push, so the - /// wait is short; the yield is for a sender preempted inside it. + /// The dispatch pass: clears `pending`; true if it was set (libuv's + /// `uv__async_io`). A send from now on schedules a new pass. /// /// # Safety /// `this` is an initialised, not yet closed handle. unsafe fn take_pending(this: NonNull) -> bool { + // SAFETY: fn contract. + unsafe { &(*this.as_ptr()).pending }.swap(0, Ordering::SeqCst) != 0 + } + + /// `uv_close`: libuv's `uv__async_spin`. Sets `pending` so that every + /// later `uv_async_send` returns at its first check, then waits until no + /// thread is past that check any more. That window is a flag exchange and + /// a queue push, so the wait is short; the yield is for a sender preempted + /// inside it. Once this returns, nothing but the loop thread touches the + /// handle, so `close_cb` may free it. + /// + /// # Safety + /// As [`Self::take_pending`]. + unsafe fn stop_sends(this: NonNull) { // SAFETY: fn contract. let (pending, busy) = unsafe { (&(*this.as_ptr()).pending, &(*this.as_ptr()).busy) }; - let was_pending = pending.swap(0, Ordering::SeqCst) != 0; + pending.store(1, Ordering::SeqCst); let mut spins = 0u32; while busy.load(Ordering::SeqCst) != 0 { spins += 1; @@ -383,7 +397,6 @@ impl UvAsync { core::hint::spin_loop(); } } - was_pending } /// JS thread, the task `uv_close` posted: `close_cb`, one loop turn later. @@ -443,7 +456,8 @@ pub(crate) unsafe extern "C" fn uv_async_init( /// `int uv_async_send(uv_async_t*)`. Any thread; the first send after a /// dispatch schedules one, later ones until then are coalesced into it. -/// Legal until `close_cb` runs: after `uv_close` it sets flags nobody reads. +/// Legal until `close_cb` runs: after `uv_close` (`stop_sends`) it returns at +/// the first check, like libuv's, and touches nothing else. /// /// # Safety /// `handle` was initialised by `uv_async_init` and its `close_cb` has not run. @@ -456,6 +470,7 @@ pub(crate) unsafe extern "C" fn uv_async_send(handle: *mut UvAsync) -> c_int { let (pending, busy, loop_) = unsafe { (&(*handle).pending, &(*handle).busy, (*handle).handle.loop_) }; if pending.load(Ordering::SeqCst) != 0 { + // Already scheduled, or closed. return 0; } let _ = busy.fetch_add(1, Ordering::SeqCst); @@ -485,9 +500,10 @@ unsafe fn as_async(handle: *mut UvHandle, function: &'static CStr) -> NonNullhandle); uv_close((uv_handle_t *)&test->handle, async_test_close_cb); uv_close((uv_handle_t *)&test->handle, async_test_close_cb); // ignored - int32_t observed[2] = { + int32_t observed[3] = { uv_is_active((uv_handle_t *)&test->handle), uv_is_closing((uv_handle_t *)&test->handle), + uv_async_send(&test->handle), }; - return make_int32_array(env, observed, 2); + return make_int32_array(env, observed, 3); } // --- a thread and the loop thread taking turns, then racing ------------------ @@ -580,7 +583,6 @@ struct work_test { uv_work_t req; struct reporter reporter; pthread_t loop_thread; - int marker; int work_ran; int work_ran_off_the_loop_thread; }; diff --git a/test/napi/uv.test.ts b/test/napi/uv.test.ts index a2767c70ac84..055883f09c3d 100644 --- a/test/napi/uv.test.ts +++ b/test/napi/uv.test.ts @@ -337,12 +337,12 @@ describe.if(!isWindows)("uv stubs", () => { expect(exitCode).toBe(0); }); - test.concurrent("uv_close with a send pending runs only close_cb, and a second uv_close is ignored", async () => { + test.concurrent("uv_close with a send pending: close_cb only; a later close or send does nothing", async () => { const { stdout, stderr, exitCode } = await runInChild(` console.log("after close", JSON.stringify(addon.testAsyncCloseWithSendPending(report))); `); expect(stderr).toBe(""); - expect(stdout).toBe("after close [0,1]\nclose 1 1\n"); + expect(stdout).toBe("after close [0,1,0]\nclose 1 1\n"); expect(exitCode).toBe(0); }); From 261cb0608e940b61e30ea1197efe28223e14f849 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:41:39 +0000 Subject: [PATCH 3/4] uv posix: shorter comments --- src/jsc/VirtualMachine.rs | 6 +- src/jsc/bindings/BunProcess.cpp | 4 +- src/jsc/bindings/uv-posix-polyfills.c | 15 +- src/runtime/jsc_hooks.rs | 5 +- src/runtime/napi/napi_body.rs | 4 +- src/runtime/napi/uv_posix.rs | 279 ++++++++++---------------- 6 files changed, 112 insertions(+), 201 deletions(-) diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index 9b9af75338ee..d161d788d333 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -798,10 +798,8 @@ impl VirtualMachine { VM.get() } - /// The main thread's VM, from any thread; `None` before it exists. It is - /// never freed, but off its own thread only what is thread-safe may be - /// reached through it: a wakeup of its loop (`PosixSignalHandle`), a field - /// written once at init (`uv_default_loop`). + /// Any thread. Never freed, but off its own thread only thread-safe state is + /// reachable through it (a loop wakeup, a field written once at init). pub fn get_main_thread_vm() -> Option<*mut VirtualMachine> { let p = MAIN_THREAD_VM.load(core::sync::atomic::Ordering::Acquire); if p.is_null() { None } else { Some(p) } diff --git a/src/jsc/bindings/BunProcess.cpp b/src/jsc/bindings/BunProcess.cpp index 6133965bb196..eb1cc945ee22 100644 --- a/src/jsc/bindings/BunProcess.cpp +++ b/src/jsc/bindings/BunProcess.cpp @@ -80,9 +80,7 @@ #include #include #include -// uv-posix-polyfills.c: the version of the libuv headers the posix uv_* -// polyfills implement, for process.versions.uv. -extern "C" const char* uv_version_string(void); +extern "C" const char* uv_version_string(void); // uv-posix-polyfills.c #else #include #include diff --git a/src/jsc/bindings/uv-posix-polyfills.c b/src/jsc/bindings/uv-posix-polyfills.c index 78a1e29da49d..85d347e06af5 100644 --- a/src/jsc/bindings/uv-posix-polyfills.c +++ b/src/jsc/bindings/uv-posix-polyfills.c @@ -130,11 +130,9 @@ UV_EXTERN void uv_mutex_unlock(uv_mutex_t* mutex) abort(); } -// The functions below need nothing but the headers in ./libuv, so they are -// libuv's own definitions (src/version.c, src/uv-common.c, -// src/uv-data-getter-setters.c, src/unix/core.c). The loop-backed functions -// for the same handles and requests (uv_async_*, uv_close, uv_queue_work, ...) -// are in src/runtime/napi/uv_posix.rs. +// libuv's own definitions of the functions that need only the headers (its +// version.c, uv-common.c, uv-data-getter-setters.c, unix/core.c). The +// loop-backed functions are in src/runtime/napi/uv_posix.rs. #define UV_STRINGIFY(v) UV_STRINGIFY_HELPER(v) #define UV_STRINGIFY_HELPER(v) #v @@ -147,9 +145,7 @@ UV_EXTERN void uv_mutex_unlock(uv_mutex_t* mutex) #define UV_VERSION_STRING UV_VERSION_STRING_BASE "-" UV_VERSION_SUFFIX #endif -// The version of the headers these polyfills implement the ABI of, which is -// also the libuv Bun links on Windows. BunProcess.cpp reports it as -// process.versions.uv on every platform. +// The headers' version, which is also what process.versions.uv reports. UV_EXTERN unsigned int uv_version(void) { return UV_VERSION_HEX; @@ -254,8 +250,7 @@ UV_EXTERN void uv_req_set_data(uv_req_t* req, void* data) req->data = data; } -// A uv_loop_t* is a UvLoop (uv_posix.rs), whose first field is `data` like -// uv_loop_t's, so these are the libuv definitions. +// A uv_loop_t* is a UvLoop (uv_posix.rs); its first field is `data` too. UV_EXTERN void* uv_loop_get_data(const uv_loop_t* loop) { return loop->data; diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 214d74a49cb1..e5c78e846872 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -102,9 +102,8 @@ pub(crate) struct RuntimeState { /// The resolver's PackageManager wake-handler context (module queue + VM /// handle); the resolver holds a raw pointer to it. Freed with the state. pub(crate) wake_ctx: Option>, - /// The `uv_loop_t` N-API addons get for this VM (`napi_get_uv_event_loop`, - /// and `uv_default_loop()` for the main thread's). Embedded here because - /// addons keep its address for as long as the VM lives. + /// The `uv_loop_t` addons get for this VM; they keep its address as long as + /// the VM lives, hence embedded. #[cfg(unix)] pub(crate) uv_loop: crate::napi::uv_posix::UvLoop, } diff --git a/src/runtime/napi/napi_body.rs b/src/runtime/napi/napi_body.rs index d3ef55f38749..4bb40f882d18 100644 --- a/src/runtime/napi/napi_body.rs +++ b/src/runtime/napi/napi_body.rs @@ -4063,9 +4063,7 @@ mod posix_platform_specific_v8_apis { // uv_* symbol references (posix DCE suppression) // ────────────────────────────────────────────────────────────────────────── -/// The uv_* symbols defined in C (uv-posix-stubs.c, uv-posix-polyfills.c) and -/// C++ (uv_tty_reset_mode). The ones defined in Rust are kept by -/// `uv_posix::fix_dead_code_elimination`. +/// The uv_* symbols defined in C and C++; `uv_posix` keeps its own. #[cfg(unix)] mod uv_functions_to_export { unsafe extern "C" { diff --git a/src/runtime/napi/uv_posix.rs b/src/runtime/napi/uv_posix.rs index a351e588853f..8deef490d620 100644 --- a/src/runtime/napi/uv_posix.rs +++ b/src/runtime/napi/uv_posix.rs @@ -1,27 +1,19 @@ -//! The loop-backed part of libuv's API for N-API addons on posix: `uv_async_t`, -//! `uv_queue_work`, `uv_default_loop`, and the `uv_handle_t` functions for the -//! handle type an addon can create here. +//! libuv's loop-backed API for N-API addons on posix: `uv_default_loop`, +//! `uv_async_t`, `uv_queue_work` and the `uv_handle_t` functions. Every other +//! `uv_*` symbol is a crash stub (uv-posix-stubs.c) or a header-only polyfill +//! (uv-posix-polyfills.c). Bun has no libuv loop here, so these map onto the +//! VM's event loop and keep libuv's ABI and thread contract: //! -//! Bun does not run a libuv loop on posix. Every other `uv_*` symbol is a crash -//! stub (`src/jsc/bindings/uv-posix-stubs.c`) or a loop-free polyfill -//! (`src/jsc/bindings/uv-posix-polyfills.c`). The functions here keep libuv's -//! ABI and its threading contract, and map the loop onto the VM's event loop: -//! -//! - The addon allocates the `uv_async_t` / `uv_work_t` and reads `data`, -//! `loop` and `type` from it, so those fields sit where `uv.h` puts them -//! ([`UvHandle`], [`UvReq`]). The private fields behind them are Bun's. -//! - As in libuv, only `uv_async_send` may be called from any thread. -//! Everything else runs on the loop's JS thread, the handle memory stays -//! valid until `close_cb` has run, and the request until `after_work_cb` -//! has run. -//! - A `uv_loop_t*` is a [`UvLoop`]: one per VM, embedded in its -//! `RuntimeState`. Only its first word, `data`, is part of the ABI. +//! - The addon allocates the handle or request and reads `data`, `loop` and +//! `type` from it ([`UvHandle`], [`UvReq`]); the fields behind those are Bun's. +//! - Only `uv_async_send` may be called off the loop's JS thread. A handle is +//! valid until its `close_cb` has run, a request until its `after_work_cb`. +//! - A `uv_loop_t*` is a [`UvLoop`], one per VM; only its `data` word is ABI. //! - `uv_async_send` sets the handle's `pending` flag and posts at most one -//! dispatch task per loop through the VM's [`VmHandle`], coalescing sends -//! the way libuv's eventfd does. The task walks the loop's live handles on -//! the JS thread, as libuv's `uv__async_io` does. -//! - `uv_queue_work` is a [`Job`]: `work_cb` on the work pool, `after_work_cb` -//! from the job's completion on the JS thread. +//! dispatch task per loop through the VM's [`VmHandle`] (libuv: the eventfd). +//! The task walks the loop's handles on the JS thread (libuv: `uv__async_io`). +//! - `uv_queue_work` is a [`Job`]: `work_cb` on the pool, `after_work_cb` in +//! its completion. use core::cell::Cell; use core::ffi::{CStr, c_char, c_int, c_uint, c_void}; @@ -41,8 +33,7 @@ use crate::jsc_hooks::RuntimeState; bun_output::declare_scope!(uv, hidden); unsafe extern "C" { - /// Crashes with the report the stubs in `uv-posix-stubs.c` produce. `name` - /// is kept by pointer, so it must be static. Does not return. + /// The stubs' crash; keeps `name` by pointer, never returns. fn CrashHandler__unsupportedUVFunction(name: *const c_char); } @@ -58,19 +49,15 @@ const UV_EINVAL: c_int = -libc::EINVAL; const UV_EBUSY: c_int = -libc::EBUSY; const UV_ECANCELED: c_int = -libc::ECANCELED; -/// `uv_handle_type` / `uv_req_type` members (`UV_HANDLE_TYPE_MAP` / -/// `UV_REQ_TYPE_MAP` in uv.h). +/// Positions in uv.h's `UV_HANDLE_TYPE_MAP` and `UV_REQ_TYPE_MAP`. const UV_ASYNC: c_uint = 1; const UV_WORK: c_uint = 7; -/// The `uv_handle_t.flags` bits libuv itself uses for these states -/// (src/uv-common.h). Whether the handle refs the loop is the [`KeepAlive`] -/// in [`UvAsync`], not a flag. +/// libuv's own `flags` values for these two states (src/uv-common.h). const UV_HANDLE_CLOSING: c_uint = 0x01; const UV_HANDLE_CLOSED: c_uint = 0x02; -/// `sizeof(uv_async_t)` and `sizeof(uv_work_t)` on 64-bit unix: the addon -/// allocates both, so Bun's private fields must fit behind the public ones. +/// `sizeof` on 64-bit unix; the addon allocates both, so Bun's fields must fit. const UV_ASYNC_T_SIZE: usize = 128; const UV_WORK_T_SIZE: usize = 128; @@ -94,26 +81,20 @@ enum DispatchState { Running = 2, } -/// What a `uv_loop_t*` points at on posix. One per VM, owned by its -/// `RuntimeState`, so it lives exactly as long as the VM: the main thread's -/// for the whole process (like libuv's default loop), a Worker's until the -/// Worker exits (by when, as in libuv, the addon must have closed its handles). +/// What a `uv_loop_t*` points at. Lives in the VM's `RuntimeState`, so as long +/// as the VM: forever for the main thread's, like libuv's default loop. #[repr(C)] pub(crate) struct UvLoop { - /// `uv_loop_t.data`, at offset 0 as in uv.h. The addon reads and writes it, - /// directly or through `uv_loop_{get,set}_data` (uv-posix-polyfills.c); - /// Bun never touches it. + /// `uv_loop_t.data`: the addon's, read and written by it (offset 0 as in uv.h). data: Cell<*mut c_void>, vm: NonNull, - /// How `uv_async_send`, on any thread, reaches the JS thread. Uncounted: - /// the loop is VM-owned, so it must not be something the VM waits for. + /// How `uv_async_send` reaches the JS thread from any thread. handle: VmHandle, dispatch_state: AtomicU8, /// The initialised, not yet closed async handles. JS thread. asyncs: JsCell>>, - /// While a dispatch pass runs, the handles it has not visited yet (in - /// reverse order). `uv_close` removes from both lists, so a handle closed - /// by a callback is never visited afterwards. JS thread. + /// The handles the running dispatch pass has not visited yet, last first. + /// `uv_close` removes from both lists. JS thread. dispatching: JsCell>>, } @@ -132,14 +113,12 @@ impl UvLoop { } } - /// The loop of `vm`, or null when `vm` has no `RuntimeState` (a `bun_jsc` - /// unit test, or a Worker's VM after its teardown). + /// Null once a Worker's VM has torn its `RuntimeState` down. /// /// # Safety - /// `vm` points at a live or never-freed `VirtualMachine`. Any thread: this - /// reads one field, `runtime_state`, which is written before the VM runs - /// any script (so before an addon exists) and again only by a Worker's - /// teardown, after which no conforming addon uses that Worker's loop. + /// `vm` is live or never freed. Any thread: `runtime_state` is written before + /// the VM runs script and again only by a Worker's teardown, after which, as + /// with libuv, the addon must not use that loop. pub(crate) unsafe fn of_vm(vm: *const VirtualMachine) -> *mut UvLoop { // SAFETY: fn contract. let state = unsafe { (*vm).runtime_state }.cast::(); @@ -158,9 +137,8 @@ impl UvLoop { unsafe { self.vm.as_ref() }.global().js_thread() } - /// Any thread. Makes sure a dispatch pass runs after this point; at most - /// one task is queued per loop however many handles are sent - /// (`ThreadSafeFunction::schedule_dispatch` has the same state machine). + /// Any thread. One queued task however many handles are sent; the state + /// machine is `ThreadSafeFunction::schedule_dispatch`'s. fn schedule_dispatch(&self) { let prev = self .dispatch_state @@ -172,9 +150,7 @@ impl UvLoop { let this: *const UvLoop = self; let task = ConcurrentTask::from_callback(this.cast_mut(), UvLoop::dispatch); if let Posted::Refused(task) = self.handle.post(LoopKind::Regular, task) { - // The VM is gone. As with a send to a closed libuv loop, the - // callback is lost; stay consistent so a later send does not - // believe a task is queued. + // The VM is gone: the callback is lost, as with a closed libuv loop. // SAFETY: refused ⇒ the task was never queued and is ours to free. unsafe { ConcurrentTask::release_refused(task) }; self.dispatch_state @@ -193,14 +169,12 @@ impl UvLoop { loop { this.dispatch_state .store(DispatchState::Running as u8, Ordering::SeqCst); - // A stopping VM takes no more callbacks, like a threadsafe - // function's; close callbacks still run (`UvAsync::run_close`). + // A stopping VM takes no more callbacks (threadsafe functions agree). if global.bun_vm().script_allowed() { this.run_pass(global); } - // A send that arrived during the pass found `Running`, so it did not - // post a task: it set `Pending`, this exchange fails, and the next - // pass picks its handle up. (A plain store of `Idle` would lose it.) + // A send during the pass set `Pending` instead of posting a task; + // then this fails and the next pass picks its handle up. if this .dispatch_state .compare_exchange( @@ -216,12 +190,9 @@ impl UvLoop { } } - /// One walk over the handles that were live when it started: libuv's - /// `uv__async_io`. Like libuv, a handle goes back into the live list before - /// its callback runs, so a `uv_close` from inside any callback finds it. - /// A handle initialised by a callback lands in the live list and, if it is - /// sent, in the next pass. A callback's uncaught exception is reported and - /// the walk goes on; the VM's termination ends it. + /// libuv's `uv__async_io`: each handle goes back into the live list before + /// its callback runs, so a `uv_close` from any callback finds it in one of + /// the two lists. Handles initialised meanwhile are the next pass's. fn run_pass(&self, global: &JSGlobalObject) { self.asyncs.with_mut(|live| { self.dispatching.with_mut(|todo| { @@ -252,7 +223,7 @@ impl UvLoop { break; } } - // Only a termination leaves anything here. Put it back, in order. + // Left over only by a termination. self.dispatching.with_mut(|todo| { if !todo.is_empty() { self.asyncs @@ -266,8 +237,7 @@ impl UvLoop { self.asyncs.with_mut(|live| live.push(async_)); } - /// JS thread. A handle may be in either list (a close from inside a - /// dispatch pass) and, if an addon initialised it twice, more than once. + /// JS thread. `retain`: a handle an addon initialised twice is in twice. fn unregister(&self, async_: NonNull) { self.asyncs.with_mut(|live| live.retain(|h| *h != async_)); self.dispatching @@ -275,12 +245,8 @@ impl UvLoop { } } -/// `uv_loop_t* uv_default_loop(void)`: the main thread's loop, from any -/// thread, like libuv's process-wide default loop. Null when there is no main -/// JS thread (libuv returns null when the default loop cannot be set up). -/// -/// An addon loaded in a Worker gets the main thread's loop from this, as it -/// does in Node; `napi_get_uv_event_loop` is how it gets its own. +/// `uv_loop_t* uv_default_loop(void)`: the main thread's loop, from any thread, +/// also inside a Worker (as in Node; `napi_get_uv_event_loop` gives its own). #[unsafe(no_mangle)] pub(crate) extern "C" fn uv_default_loop() -> *mut UvLoop { let Some(vm) = VirtualMachine::get_main_thread_vm() else { @@ -294,10 +260,8 @@ pub(crate) extern "C" fn uv_default_loop() -> *mut UvLoop { // uv_handle_t / uv_async_t // ────────────────────────────────────────────────────────────────────────── -/// `UV_HANDLE_FIELDS` (uv.h) and `UV_HANDLE_PRIVATE_FIELDS` (uv/unix.h): the -/// common prefix of every handle type, and the view `uv_close` and friends -/// get. `data`, `loop` and `type` are read by addons and must stay where uv.h -/// puts them; the rest is libuv's private state, used here as libuv uses it. +/// `UV_HANDLE_FIELDS` + `UV_HANDLE_PRIVATE_FIELDS` (uv.h, uv/unix.h): every +/// handle's prefix. Addons read `data`, `loop` and `type`; the rest is private. #[repr(C)] pub(crate) struct UvHandle { data: *mut c_void, @@ -318,8 +282,7 @@ const _: () = assert!(core::mem::offset_of!(UvHandle, flags) == 88); const _: () = assert!(core::mem::size_of::() == 96); impl UvHandle { - /// Sets up the common prefix as libuv's `uv__handle_init` does. `data` is - /// the addon's and is left alone: addons commonly set it before init. + /// libuv's `uv__handle_init`; `data` is the addon's and is left alone. fn init(this: *mut UvHandle, loop_: *mut UvLoop, type_: c_uint) { // SAFETY: `this` is the addon's handle memory, on the loop thread; // field-wise writes because the memory is uninitialised. @@ -335,23 +298,16 @@ impl UvHandle { } } -/// `struct uv_async_s`: the [`UvHandle`] prefix, then libuv's -/// `UV_ASYNC_PRIVATE_FIELDS`, of which Bun keeps `async_cb` and `pending` -/// and uses the rest for its own state. +/// `struct uv_async_s`: the prefix, then Bun's use of `UV_ASYNC_PRIVATE_FIELDS`. #[repr(C)] pub(crate) struct UvAsync { handle: UvHandle, async_cb: Option, - /// Whether the handle keeps the process alive (`uv_ref` / `uv_unref`). - /// JS thread. + /// `uv_ref` / `uv_unref`. JS thread. keep_alive: KeepAlive, - /// Threads past the first check of `uv_async_send`. libuv's busy counter: - /// `uv_close` waits for it to reach zero, so once it returns no thread is - /// still touching the handle and `close_cb` may free it. + /// libuv's busy counter: senders past their first check; `uv_close` waits it out. busy: AtomicI32, - /// Set by `uv_async_send`, cleared by the dispatch pass, and set for good - /// by `uv_close`, so that a send after the close returns at its first - /// check (libuv's `uv__async_spin` does the same). + /// Set by a send, cleared by the dispatch pass, set for good by `uv_close`. pending: AtomicI32, } @@ -359,14 +315,11 @@ const _: () = assert!(core::mem::offset_of!(UvAsync, handle) == 0); const _: () = assert!(core::mem::offset_of!(UvAsync, async_cb) == 96); const _: () = assert!(core::mem::size_of::() <= UV_ASYNC_T_SIZE); -// No function below forms a reference to a whole handle: while the loop -// thread is inside one of them, other threads may be in `uv_async_send` on the -// same handle, which is fine for its atomics but not under a `&UvAsync` or -// `&mut UvAsync` covering them. Fields are read and written through the raw -// pointer, and a reference is formed to one field at a time. +// SAFETY: nothing below forms a `&UvAsync` or `&mut UvAsync`: other threads may be +// in `uv_async_send` on the handle, which is fine for its atomics but not under a +// reference covering them. Fields are accessed through the raw pointer, one at a time. impl UvAsync { - /// The dispatch pass: clears `pending`; true if it was set (libuv's - /// `uv__async_io`). A send from now on schedules a new pass. + /// The dispatch pass (libuv's `uv__async_io`): true if the handle was sent. /// /// # Safety /// `this` is an initialised, not yet closed handle. @@ -375,12 +328,9 @@ impl UvAsync { unsafe { &(*this.as_ptr()).pending }.swap(0, Ordering::SeqCst) != 0 } - /// `uv_close`: libuv's `uv__async_spin`. Sets `pending` so that every - /// later `uv_async_send` returns at its first check, then waits until no - /// thread is past that check any more. That window is a flag exchange and - /// a queue push, so the wait is short; the yield is for a sender preempted - /// inside it. Once this returns, nothing but the loop thread touches the - /// handle, so `close_cb` may free it. + /// `uv_close` (libuv's `uv__async_spin`): later sends return at their first + /// check, and the senders past it (a flag exchange and a queue push away from + /// done) are waited out, so afterwards `close_cb` may free the handle. /// /// # Safety /// As [`Self::take_pending`]. @@ -399,9 +349,7 @@ impl UvAsync { } } - /// JS thread, the task `uv_close` posted: `close_cb`, one loop turn later. - /// The handle is the addon's again once the callback returns (it usually - /// frees it), so nothing touches it afterwards. + /// The task `uv_close` posted. The callback usually frees the handle. fn run_close(this: *mut UvAsync) -> JsResult<()> { // SAFETY: `uv_close` posted this for a handle the addon keeps alive // until `close_cb` has run. @@ -423,13 +371,12 @@ impl UvAsync { } } -/// `int uv_async_init(uv_loop_t*, uv_async_t*, uv_async_cb)`. Loop thread. -/// The handle starts active and ref'd, as in libuv, so it keeps the process -/// alive until it is unref'd or closed. +/// `int uv_async_init(uv_loop_t*, uv_async_t*, uv_async_cb)`. Loop thread. The +/// handle starts active and ref'd, as in libuv. /// /// # Safety -/// `loop_` is null or a loop this VM handed out; `handle` points at -/// `sizeof(uv_async_t)` bytes the addon keeps alive until its `close_cb` ran. +/// `loop_` is null or one of this module's loops; `handle` is `sizeof(uv_async_t)` +/// bytes the addon keeps until its `close_cb` has run. #[unsafe(no_mangle)] pub(crate) unsafe extern "C" fn uv_async_init( loop_: *mut UvLoop, @@ -454,19 +401,15 @@ pub(crate) unsafe extern "C" fn uv_async_init( 0 } -/// `int uv_async_send(uv_async_t*)`. Any thread; the first send after a -/// dispatch schedules one, later ones until then are coalesced into it. -/// Legal until `close_cb` runs: after `uv_close` (`stop_sends`) it returns at -/// the first check, like libuv's, and touches nothing else. +/// `int uv_async_send(uv_async_t*)`. Any thread; sends before the next dispatch +/// coalesce into it. After `uv_close` it returns at the first check, like libuv's. /// /// # Safety /// `handle` was initialised by `uv_async_init` and its `close_cb` has not run. #[unsafe(no_mangle)] pub(crate) unsafe extern "C" fn uv_async_send(handle: *mut UvAsync) -> c_int { - // The loop thread may be in `uv_close` on this handle, writing its plain - // fields; the ones used here are the atomics and `loop`, which only - // `uv_async_init` writes, before the handle can reach another thread. - // SAFETY: fn contract. + // SAFETY: fn contract. The loop thread may be in `uv_close` writing the plain + // fields; these are the atomics and `loop`, which nothing writes after init. let (pending, busy, loop_) = unsafe { (&(*handle).pending, &(*handle).busy, (*handle).handle.loop_) }; if pending.load(Ordering::SeqCst) != 0 { @@ -475,18 +418,15 @@ pub(crate) unsafe extern "C" fn uv_async_send(handle: *mut UvAsync) -> c_int { } let _ = busy.fetch_add(1, Ordering::SeqCst); if pending.swap(1, Ordering::SeqCst) == 0 { - // SAFETY: the loop outlives the handle (see `UvLoop`); only its - // thread-safe fields are used here. + // SAFETY: the loop outlives the handle; `schedule_dispatch` is any-thread. unsafe { &*loop_ }.schedule_dispatch(); } let _ = busy.fetch_sub(1, Ordering::SeqCst); 0 } -/// The async handle behind a `uv_handle_t*`, for the functions that take any -/// handle type. Only async handles can be initialised on posix, so anything -/// else is memory no `uv_*_init` here wrote: crash the way the stub for -/// `function` did, with its name. +/// For the functions that take any handle type: only async handles exist, so +/// anything else crashes like `function`'s stub did. /// /// # Safety /// `handle` points at an initialised handle. @@ -499,11 +439,8 @@ unsafe fn as_async(handle: *mut UvHandle, function: &'static CStr) -> NonNull()).expect("dereferenced above") } -/// `void uv_close(uv_handle_t*, uv_close_cb)`. Loop thread. Stops the handle -/// at once (no callback runs after this returns, no `uv_async_send` is still -/// inside the handle, and later ones return at their first check) and runs -/// `close_cb` from the loop later, as libuv does, since addons free the -/// handle there. Closing twice does nothing. +/// `void uv_close(uv_handle_t*, uv_close_cb)`. Loop thread. Stops the handle now +/// and runs `close_cb` on a later turn, as libuv does. Closing twice does nothing. /// /// # Safety /// `handle` was initialised by a `uv_*_init` of this module. @@ -512,8 +449,7 @@ pub(crate) unsafe extern "C" fn uv_close(handle: *mut UvHandle, close_cb: Option // SAFETY: fn contract. let this = unsafe { as_async(handle, c"uv_close") }; let async_ = this.as_ptr(); - // SAFETY: initialised (`as_async`); loop thread, so nothing else writes - // the plain fields or uses the keep-alive. + // SAFETY: initialised (`as_async`); the loop thread alone uses these fields. let loop_ = unsafe { if (*async_).handle.flags & UV_HANDLE_CLOSING != 0 { return; @@ -528,9 +464,8 @@ pub(crate) unsafe extern "C" fn uv_close(handle: *mut UvHandle, close_cb: Option // SAFETY: initialised and, until this line, not closed. unsafe { UvAsync::stop_sends(this) }; loop_.unregister(this); - // The queued task keeps the loop alive until `close_cb` has run, as - // libuv's closing list does. Refused means the VM is already gone, and - // with it the turn `close_cb` would have run on. + // The queued task keeps the loop alive until `close_cb` has run (libuv: the + // closing list). Refused: the VM is gone, and with it that turn. let task = ConcurrentTask::from_callback(async_, UvAsync::run_close); if let Posted::Refused(task) = loop_.handle.post(LoopKind::Regular, task) { // SAFETY: refused ⇒ never queued, ours to free. @@ -538,8 +473,7 @@ pub(crate) unsafe extern "C" fn uv_close(handle: *mut UvHandle, close_cb: Option } } -/// `void uv_ref(uv_handle_t*)`. Loop thread. Idempotent; nothing after -/// `uv_close`, whose unref is final. +/// `void uv_ref(uv_handle_t*)`. Loop thread. Idempotent; `uv_close`'s unref is final. /// /// # Safety /// As [`uv_close`]. @@ -579,8 +513,8 @@ pub(crate) unsafe extern "C" fn uv_has_ref(handle: *mut UvHandle) -> c_int { c_int::from(unsafe { (*async_).keep_alive.is_active() }) } -/// `int uv_is_active(const uv_handle_t*)`. Loop thread. An async handle is -/// active from `uv_async_init` until `uv_close` (libuv starts it in init). +/// `int uv_is_active(const uv_handle_t*)`. Loop thread. libuv starts an async +/// handle in its init, so: not closed. /// /// # Safety /// As [`uv_close`]. @@ -592,8 +526,7 @@ pub(crate) unsafe extern "C" fn uv_is_active(handle: *mut UvHandle) -> c_int { c_int::from(unsafe { (*async_).handle.flags } & UV_HANDLE_CLOSING == 0) } -/// `int uv_is_closing(const uv_handle_t*)`. Loop thread. True from `uv_close` -/// on, `close_cb` included. +/// `int uv_is_closing(const uv_handle_t*)`. Loop thread. True from `uv_close` on. /// /// # Safety /// As [`uv_close`]. @@ -610,8 +543,7 @@ pub(crate) unsafe extern "C" fn uv_is_closing(handle: *mut UvHandle) -> c_int { // uv_req_t / uv_work_t // ────────────────────────────────────────────────────────────────────────── -/// `UV_REQ_FIELDS` (uv.h): the common prefix of every request type, and the -/// view `uv_cancel` gets. `data` and `type` are the addon's to read. +/// `UV_REQ_FIELDS` (uv.h): every request's prefix. Addons read `data` and `type`. #[repr(C)] pub(crate) struct UvReq { data: *mut c_void, @@ -634,9 +566,8 @@ enum WorkState { Cancelled = 2, } -/// `struct uv_work_s`: the [`UvReq`] prefix and the three fields uv.h -/// declares after it (`loop`, `work_cb`, `after_work_cb` are read by addons), -/// then Bun's state where libuv keeps its `struct uv__work`. +/// `struct uv_work_s`: the prefix, the three fields uv.h declares after it +/// (addons read them too), then Bun's state where libuv keeps `struct uv__work`. #[repr(C)] pub(crate) struct UvWork { req: UvReq, @@ -653,23 +584,22 @@ const _: () = assert!(core::mem::offset_of!(UvWork, work_cb) == 72); const _: () = assert!(core::mem::offset_of!(UvWork, after_work_cb) == 80); const _: () = assert!(core::mem::size_of::() <= UV_WORK_T_SIZE); -/// The [`Job`] behind one `uv_queue_work`. Its off-thread half is the request -/// itself: the addon keeps it alive until `after_work_cb` has run, which is -/// the job's whole life, so the pool may read it through the [`JsPtr`]. +/// The [`Job`] behind one `uv_queue_work`. Its off-thread half is the request: +/// the addon keeps it alive until `after_work_cb` has run, the job's whole life. struct UvWorkJob; impl JobContext for UvWorkJob { type OffThread = JsPtr; type Js = (); - // As with the handles above, no reference to a whole request is formed: - // `uv_cancel` may be running against it on another thread. Fields are - // read through the raw pointer, `state` borrowed on its own. + // SAFETY: as for the handles, no reference to a whole request is formed: the + // pool's `run` and the loop thread's `uv_cancel` overlap. Fields are accessed + // through the raw pointer, `state` borrowed on its own. fn run(req: &mut JsPtr, done: Completion) -> Option> { let req = req.as_ptr(); - // SAFETY: the request is alive for the job's life (see `UvWorkJob`); - // `work_cb` was written before the job was scheduled. + // SAFETY: alive for the job's life (`UvWorkJob`); `work_cb` was written + // before the job was scheduled. let started = unsafe { &(*req).state } .compare_exchange( WorkState::Queued as u32, @@ -680,8 +610,7 @@ impl JobContext for UvWorkJob { .is_ok(); // SAFETY: as above. if started && let Some(work_cb) = unsafe { (*req).work_cb } { - // SAFETY: the addon's callback, on a pool thread, as uv_queue_work - // documents. + // SAFETY: the addon's callback, on a pool thread as documented. unsafe { work_cb(req) }; } Some(done) @@ -689,8 +618,7 @@ impl JobContext for UvWorkJob { fn then(req: JsPtr, _: (), cx: &JsThread<'_>) -> JsResult<()> { let req = req.as_ptr(); - // SAFETY: as in `run`. `state` is final: the pool thread took it, or - // `uv_cancel` did. + // SAFETY: as in `run`; `state` is final once the job has been posted. let (state, after_work_cb) = unsafe { ((*req).state.load(Ordering::SeqCst), (*req).after_work_cb) }; let status = if state == WorkState::Cancelled as u32 { @@ -699,8 +627,7 @@ impl JobContext for UvWorkJob { 0 }; bun_output::scoped_log!(uv, "uv_work_t {:?}: after_work_cb({})", req, status); - // The request is the addon's again once the callback returns (it - // usually frees it), so nothing touches it afterwards. + // The callback usually frees the request. if let Some(after_work_cb) = after_work_cb { // SAFETY: the addon's callback, on the loop thread. unsafe { after_work_cb(req, status) }; @@ -713,12 +640,11 @@ impl JobContext for UvWorkJob { } /// `int uv_queue_work(uv_loop_t*, uv_work_t*, uv_work_cb, uv_after_work_cb)`. -/// Loop thread. The request keeps the process alive until `after_work_cb` has -/// run, as an active libuv request does. +/// Loop thread. The request keeps the process alive, as an active one does in libuv. /// /// # Safety -/// `loop_` is null or a loop this VM handed out; `req` points at -/// `sizeof(uv_work_t)` bytes the addon keeps alive until `after_work_cb` ran. +/// `loop_` is null or one of this module's loops; `req` is `sizeof(uv_work_t)` +/// bytes the addon keeps until its `after_work_cb` has run. #[unsafe(no_mangle)] pub(crate) unsafe extern "C" fn uv_queue_work( loop_: *mut UvLoop, @@ -734,8 +660,8 @@ pub(crate) unsafe extern "C" fn uv_queue_work( return UV_EINVAL; }; bun_output::scoped_log!(uv, "uv_work_t {:?}: uv_queue_work", req); - // SAFETY: fn contract; field-wise writes into uninitialised addon memory. - // `data` is the addon's and is left alone, as libuv leaves it. + // SAFETY: fn contract; field-wise writes into uninitialised addon memory, + // except `data`, which is the addon's. unsafe { (&raw mut (*req).req.type_).write(UV_WORK); (&raw mut (*req).req.reserved).write([core::ptr::null_mut(); 6]); @@ -751,16 +677,13 @@ pub(crate) unsafe extern "C" fn uv_queue_work( 0 } -/// `int uv_cancel(uv_req_t*)`. Loop thread, as in libuv: there it cannot -/// race the `after_work_cb` that hands the request back to the addon. Only -/// work requests exist here; libuv also answers `UV_EINVAL` for a request -/// type it cannot cancel. `0` means `work_cb` will not run and -/// `after_work_cb` gets `UV_ECANCELED`; `UV_EBUSY` means the work already -/// started (or finished, or was cancelled). +/// `int uv_cancel(uv_req_t*)`. Loop thread, as in libuv (so it cannot race the +/// `after_work_cb` that hands the request back). libuv's answers: `0` and +/// `after_work_cb` gets `UV_ECANCELED`; `UV_EBUSY` once the work started; +/// `UV_EINVAL` for a request type it cannot cancel, here every other type. /// /// # Safety -/// `req` is null or a request queued by `uv_queue_work` whose `after_work_cb` -/// has not returned. +/// `req` is null or a queued request whose `after_work_cb` has not returned. #[unsafe(no_mangle)] pub(crate) unsafe extern "C" fn uv_cancel(req: *mut UvReq) -> c_int { if req.is_null() { @@ -770,8 +693,8 @@ pub(crate) unsafe extern "C" fn uv_cancel(req: *mut UvReq) -> c_int { if unsafe { (*req).type_ } != UV_WORK { return UV_EINVAL; } - // SAFETY: `type` says `uv_queue_work` initialised this memory as a - // `UvWork`; only its atomic is touched, as the pool thread may be racing. + // SAFETY: `type` says this is a `UvWork`; only its atomic is touched, as the + // pool thread may be in `run`. let state = unsafe { &(*req.cast::()).state }; match state.compare_exchange( WorkState::Queued as u32, From dfd8ab17f8c0f6e82decf5d9135389e7acc29e98 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:54:53 +0000 Subject: [PATCH 4/4] uv posix: check uv_async_init in the test addon, document of_vm's two null states of_vm returns null before init_runtime_state has run and after destroy has cleared the field (which it does before freeing the state), so its doc says that instead of "never freed". The test addon now throws when uv_async_init fails instead of going on to use the handle, and the callback-scope todo comment names the pre-existing test.js exclusion too. --- src/runtime/napi/uv_posix.rs | 13 +++---- .../node-api/test_callback_scope/do.test.ts | 2 ++ test/napi/uv-stub-stuff/uv_impl.c | 34 ++++++++++++++----- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/runtime/napi/uv_posix.rs b/src/runtime/napi/uv_posix.rs index 8deef490d620..d827ad17fd7f 100644 --- a/src/runtime/napi/uv_posix.rs +++ b/src/runtime/napi/uv_posix.rs @@ -81,8 +81,8 @@ enum DispatchState { Running = 2, } -/// What a `uv_loop_t*` points at. Lives in the VM's `RuntimeState`, so as long -/// as the VM: forever for the main thread's, like libuv's default loop. +/// What a `uv_loop_t*` points at. Lives in the VM's `RuntimeState`, so exactly +/// as long as the VM does ([`Self::of_vm`]). #[repr(C)] pub(crate) struct UvLoop { /// `uv_loop_t.data`: the addon's, read and written by it (offset 0 as in uv.h). @@ -113,12 +113,13 @@ impl UvLoop { } } - /// Null once a Worker's VM has torn its `RuntimeState` down. + /// Null before `init_runtime_state` and after `VirtualMachine::destroy`, which + /// clears the field before it frees the state. /// /// # Safety - /// `vm` is live or never freed. Any thread: `runtime_state` is written before - /// the VM runs script and again only by a Worker's teardown, after which, as - /// with libuv, the addon must not use that loop. + /// `vm`'s allocation is still there (the main thread's always is). Any thread: + /// between those two writes `runtime_state` is constant, and an addon's + /// cleanup hooks, where it stops its threads, run before the second one. pub(crate) unsafe fn of_vm(vm: *const VirtualMachine) -> *mut UvLoop { // SAFETY: fn contract. let state = unsafe { (*vm).runtime_state }.cast::(); diff --git a/test/napi/node-napi-tests/test/node-api/test_callback_scope/do.test.ts b/test/napi/node-napi-tests/test/node-api/test_callback_scope/do.test.ts index 5e4faf615a48..8bb60c24a197 100644 --- a/test/napi/node-napi-tests/test/node-api/test_callback_scope/do.test.ts +++ b/test/napi/node-napi-tests/test/node-api/test_callback_scope/do.test.ts @@ -11,6 +11,8 @@ for (const file of Array.from(new Bun.Glob("*.js").scanSync(import.meta.dir))) { // test-resolve-async.js on Windows: the process exits before the // after_work_cb of a uv_queue_work on napi_get_uv_event_loop's loop runs // ("Mismatched noop function calls. Expected exactly 1, actual 0"). + // test.js on Windows: todo since the suite was imported, for a reason this + // file does not record; it is unrelated to the line above. test.todoIf( file === "test-async-hooks.js" || (["test.js", "test-resolve-async.js"].includes(file) && isWindows), )(file, () => { diff --git a/test/napi/uv-stub-stuff/uv_impl.c b/test/napi/uv-stub-stuff/uv_impl.c index 47dc550f8f4e..58f2a2b9a3c0 100644 --- a/test/napi/uv-stub-stuff/uv_impl.c +++ b/test/napi/uv-stub-stuff/uv_impl.c @@ -420,6 +420,20 @@ static void *send_after_a_while(void *arg) { return NULL; } +// uv_async_init, as a thrown error when it fails so that a test fails there +// and not on the events the uninitialised handle would then produce. +static bool init_async(napi_env env, uv_loop_t *loop, uv_async_t *handle, + uv_async_cb cb) { + int rc = uv_async_init(loop, handle, cb); + if (rc != 0) { + char message[64]; + snprintf(message, sizeof(message), "uv_async_init returned %d", rc); + napi_throw_error(env, NULL, message); + return false; + } + return true; +} + // testAsync(useDefaultLoop, sendFromThread, callback): events // async 1 1 first callback; the three sends coalesced into it // async 2 1 the callback's own send @@ -437,11 +451,11 @@ static napi_value test_async(napi_env env, napi_callback_info info) { struct async_test *test = calloc(1, sizeof(*test)); reporter_init(&test->reporter, env, args[2]); test->handle.data = test; // set before init, as addons commonly do - int rc = uv_async_init(get_loop(env, use_default_loop), &test->handle, - async_test_cb); - if (rc != 0 || test->handle.data != test || - uv_handle_get_loop((uv_handle_t *)&test->handle) != - get_loop(env, use_default_loop) || + uv_loop_t *loop = get_loop(env, use_default_loop); + if (!init_async(env, loop, &test->handle, async_test_cb)) + return NULL; + if (test->handle.data != test || + uv_handle_get_loop((uv_handle_t *)&test->handle) != loop || uv_handle_get_type((uv_handle_t *)&test->handle) != UV_ASYNC) { napi_throw_error(env, NULL, "uv_async_init did not set the handle up"); return NULL; @@ -479,7 +493,8 @@ static napi_value test_async_close_with_send_pending(napi_env env, get_args(env, info, args, 1); struct async_test *test = calloc(1, sizeof(*test)); reporter_init(&test->reporter, env, args[0]); - uv_async_init(get_loop(env, false), &test->handle, async_test_cb); + if (!init_async(env, get_loop(env, false), &test->handle, async_test_cb)) + return NULL; uv_handle_set_data((uv_handle_t *)&test->handle, test); uv_async_send(&test->handle); uv_close((uv_handle_t *)&test->handle, async_test_close_cb); @@ -546,7 +561,8 @@ static napi_value test_async_stress(napi_env env, napi_callback_info info) { struct stress_test *test = calloc(1, sizeof(*test)); reporter_init(&test->reporter, env, args[0]); test->handle.data = test; - uv_async_init(get_loop(env, false), &test->handle, stress_async_cb); + if (!init_async(env, get_loop(env, false), &test->handle, stress_async_cb)) + return NULL; if (pthread_create(&test->sender, NULL, stress_sender, test) != 0) { napi_throw_error(env, NULL, "pthread_create failed"); return NULL; @@ -561,7 +577,9 @@ static uv_async_t unref_test_handle; // process must exit although the handle is never closed. static napi_value test_async_ref(napi_env env, napi_callback_info info) { uv_handle_t *handle = (uv_handle_t *)&unref_test_handle; - uv_async_init(get_loop(env, false), &unref_test_handle, unused_async_cb); + if (!init_async(env, get_loop(env, false), &unref_test_handle, + unused_async_cb)) + return NULL; int32_t observed[6]; observed[0] = uv_has_ref(handle); uv_unref(handle);