diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index e60d65ad940f..d161d788d333 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -798,7 +798,9 @@ impl VirtualMachine { VM.get() } - pub(crate) fn get_main_thread_vm() -> Option<*mut VirtualMachine> { + /// 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 f5aead014a1c..eb1cc945ee22 100644 --- a/src/jsc/bindings/BunProcess.cpp +++ b/src/jsc/bindings/BunProcess.cpp @@ -80,6 +80,7 @@ #include #include #include +extern "C" const char* uv_version_string(void); // uv-posix-polyfills.c #else #include #include @@ -260,18 +261,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..85d347e06af5 100644 --- a/src/jsc/bindings/uv-posix-polyfills.c +++ b/src/jsc/bindings/uv-posix-polyfills.c @@ -130,4 +130,146 @@ UV_EXTERN void uv_mutex_unlock(uv_mutex_t* mutex) abort(); } +// 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 + +#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 headers' version, which is also what process.versions.uv reports. +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); its first field is `data` too. +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..e5c78e846872 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -102,6 +102,10 @@ 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` 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, } #[derive(Clone, Copy, PartialEq, Eq, Hash)] @@ -399,6 +403,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..4bb40f882d18 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,11 @@ mod posix_platform_specific_v8_apis { // uv_* symbol references (posix DCE suppression) // ────────────────────────────────────────────────────────────────────────── +/// The uv_* symbols defined in C and C++; `uv_posix` keeps its own. #[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 +4075,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 +4089,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 +4171,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 +4185,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 +4258,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 +4361,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 +4540,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 +4551,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 +4565,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 +4647,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 +4661,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 +4734,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 +4837,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..d827ad17fd7f --- /dev/null +++ b/src/runtime/napi/uv_posix.rs @@ -0,0 +1,725 @@ +//! 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: +//! +//! - 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`] (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}; +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" { + /// The stubs' crash; keeps `name` by pointer, never returns. + 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; + +/// 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; + +/// 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` 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; + +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. 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). + data: Cell<*mut c_void>, + vm: NonNull, + /// 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>>, + /// The handles the running dispatch pass has not visited yet, last first. + /// `uv_close` removes from both lists. 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()), + } + } + + /// Null before `init_runtime_state` and after `VirtualMachine::destroy`, which + /// clears the field before it frees the state. + /// + /// # Safety + /// `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::(); + 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. 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 + .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: 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 + .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 (threadsafe functions agree). + if global.bun_vm().script_allowed() { + this.run_pass(global); + } + // 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( + DispatchState::Running as u8, + DispatchState::Idle as u8, + Ordering::SeqCst, + Ordering::SeqCst, + ) + .is_ok() + { + return Ok(()); + } + } + } + + /// 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| { + 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; + } + } + // Left over only by a termination. + 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. `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 + .with_mut(|todo| todo.retain(|h| *h != async_)); + } +} + +/// `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 { + 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_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, + 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 { + /// 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. + 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 prefix, then Bun's use of `UV_ASYNC_PRIVATE_FIELDS`. +#[repr(C)] +pub(crate) struct UvAsync { + handle: UvHandle, + async_cb: Option, + /// `uv_ref` / `uv_unref`. JS thread. + keep_alive: KeepAlive, + /// libuv's busy counter: senders past their first check; `uv_close` waits it out. + busy: AtomicI32, + /// Set by a send, cleared by the dispatch pass, set for good by `uv_close`. + 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); + +// 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 (libuv's `uv__async_io`): true if the handle was sent. + /// + /// # 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`): 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`]. + unsafe fn stop_sends(this: NonNull) { + // SAFETY: fn contract. + let (pending, busy) = unsafe { (&(*this.as_ptr()).pending, &(*this.as_ptr()).busy) }; + pending.store(1, Ordering::SeqCst); + 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(); + } + } + } + + /// 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. + 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. +/// +/// # Safety +/// `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, + 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; 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 { + // 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 { + // Already scheduled, or closed. + return 0; + } + let _ = busy.fetch_add(1, Ordering::SeqCst); + if pending.swap(1, Ordering::SeqCst) == 0 { + // SAFETY: the loop outlives the handle; `schedule_dispatch` is any-thread. + unsafe { &*loop_ }.schedule_dispatch(); + } + let _ = busy.fetch_sub(1, Ordering::SeqCst); + 0 +} + +/// 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. +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 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. +#[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`); the loop thread alone uses these fields. + 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); + // 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 (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. + unsafe { ConcurrentTask::release_refused(task) }; + } +} + +/// `void uv_ref(uv_handle_t*)`. Loop thread. Idempotent; `uv_close`'s 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. libuv starts an async +/// handle in its init, so: not closed. +/// +/// # 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. +/// +/// # 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): every request's prefix. Addons read `data` and `type`. +#[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 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, + 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: +/// 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 = (); + + // 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: 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, + 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 documented. + 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 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 { + UV_ECANCELED + } else { + 0 + }; + bun_output::scoped_log!(uv, "uv_work_t {:?}: after_work_cb({})", req, status); + // 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) }; + } + 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, as an active one does in libuv. +/// +/// # Safety +/// `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, + 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, + // 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]); + (&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*)`. 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 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() { + 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 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, + 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..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 @@ -7,12 +7,15 @@ 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.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, () => { + 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..58f2a2b9a3c0 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,535 @@ 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; +} + +// 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 +// 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 + 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; + } + 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, and +// so does one that is sent again after uv_close (allowed until close_cb). +// Returns [is_active, is_closing, send after close] 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]); + 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); + uv_close((uv_handle_t *)&test->handle, async_test_close_cb); // ignored + 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, 3); +} + +// --- 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; + 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; + } + 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; + 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); + 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 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..055883f09c3d 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: 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,0]\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); + }); });