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..3d6117fdd3c6 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 uv-posix-polyfills.c + // "uv_async_init", + // "uv_async_send", "uv_available_parallelism", "uv_backend_fd", "uv_backend_timeout", @@ -29,7 +30,8 @@ export const symbols = [ "uv_check_start", "uv_check_stop", "uv_clock_gettime", - "uv_close", + // Defined in uv-posix-polyfills.c + // "uv_close", "uv_cond_broadcast", "uv_cond_destroy", "uv_cond_init", @@ -39,7 +41,8 @@ export const symbols = [ "uv_cpu_info", "uv_cpumask_size", "uv_cwd", - "uv_default_loop", + // Defined in uv-posix-polyfills.c + // "uv_default_loop", "uv_disable_stdio_inheritance", "uv_dlclose", "uv_dlerror", @@ -116,13 +119,15 @@ 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", + // 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", - "uv_has_ref", + // Defined in uv-posix-polyfills.c + // "uv_has_ref", // Defined in uv-posix-polyfills.cpp // "uv_hrtime", "uv_idle_init", @@ -138,8 +143,9 @@ export const symbols = [ "uv_ip6_addr", "uv_ip6_name", "uv_ip_name", - "uv_is_active", - "uv_is_closing", + // Defined in uv-posix-polyfills.c + // "uv_is_active", + // "uv_is_closing", "uv_is_readable", "uv_is_writable", "uv_key_create", @@ -222,7 +228,8 @@ export const symbols = [ "uv_read_start", "uv_read_stop", "uv_recv_buffer_size", - "uv_ref", + // Defined in uv-posix-polyfills.c + // "uv_ref", "uv_replace_allocator", "uv_req_get_data", "uv_req_get_type", @@ -323,7 +330,8 @@ export const symbols = [ "uv_udp_try_send", "uv_udp_try_send2", "uv_udp_using_recvmmsg", - "uv_unref", + // Defined in uv-posix-polyfills.c + // "uv_unref", "uv_update_time", "uv_uptime", "uv_utf16_length_as_wtf8", diff --git a/src/jsc/bindings/uv-posix-polyfills.c b/src/jsc/bindings/uv-posix-polyfills.c index 3eea2489c1bf..9b1a9004b2ef 100644 --- a/src/jsc/bindings/uv-posix-polyfills.c +++ b/src/jsc/bindings/uv-posix-polyfills.c @@ -2,7 +2,9 @@ #if OS(LINUX) || OS(DARWIN) || OS(FREEBSD) +#include #include +#include #include #include @@ -138,4 +140,208 @@ UV_EXTERN void uv_mutex_unlock(uv_mutex_t* mutex) abort(); } +// --------------------------------------------------------------------------- +// uv_handle_t / uv_async_t +// --------------------------------------------------------------------------- +// +// On POSIX Bun does not run a libuv event loop. The `uv_loop_t*` that +// `napi_get_uv_event_loop` / `uv_default_loop` hand out is really Bun's +// `*mut EventLoop` (see `napi_get_uv_event_loop` in +// src/runtime/napi/napi_body.rs). The shims below schedule work onto that +// loop and adjust its keep-alive refcount. +extern void Bun__uv_handle_schedule(uv_loop_t* loop, uv_handle_t* handle); +extern void Bun__uv_handle_ref(uv_loop_t* loop, int delta); +extern uv_loop_t* Bun__uv_default_loop(void); + +// Match the bits libuv uses so addons that peek at handle->flags see what +// they expect (libuv's src/uv-common.h). +#define BUN_UV_HANDLE_CLOSING 0x00000001 +#define BUN_UV_HANDLE_CLOSED 0x00000002 +#define BUN_UV_HANDLE_ACTIVE 0x00000004 +#define BUN_UV_HANDLE_REF 0x00000008 + +// uv_async_t->pending is our "task is queued" bit: 0 idle, 1 queued. uv_close +// sets it to 2 so subsequent uv_async_send calls observe non-zero and skip +// scheduling. +#define BUN_UV_ASYNC_CLOSING 2 + +static int bun__is_supported_handle(const uv_handle_t* handle) +{ + return handle->type == UV_ASYNC; +} + +UV_EXTERN uv_loop_t* uv_default_loop(void) +{ + return Bun__uv_default_loop(); +} + +UV_EXTERN void* uv_handle_get_data(const uv_handle_t* handle) +{ + return handle->data; +} + +UV_EXTERN void uv_handle_set_data(uv_handle_t* handle, void* data) +{ + handle->data = data; +} + +UV_EXTERN uv_loop_t* uv_handle_get_loop(const uv_handle_t* handle) +{ + return handle->loop; +} + +UV_EXTERN uv_handle_type uv_handle_get_type(const uv_handle_t* handle) +{ + return handle->type; +} + +UV_EXTERN int uv_has_ref(const uv_handle_t* handle) +{ + return (handle->flags & BUN_UV_HANDLE_REF) != 0; +} + +UV_EXTERN int uv_is_active(const uv_handle_t* handle) +{ + return (handle->flags & BUN_UV_HANDLE_ACTIVE) != 0; +} + +UV_EXTERN int uv_is_closing(const uv_handle_t* handle) +{ + return (handle->flags & (BUN_UV_HANDLE_CLOSING | BUN_UV_HANDLE_CLOSED)) != 0; +} + +UV_EXTERN void uv_ref(uv_handle_t* handle) +{ + if (!bun__is_supported_handle(handle)) { + __bun_throw_not_implemented("uv_ref"); + } + if (handle->flags & BUN_UV_HANDLE_REF) + return; + handle->flags |= BUN_UV_HANDLE_REF; + if ((handle->flags & BUN_UV_HANDLE_ACTIVE) && !(handle->flags & BUN_UV_HANDLE_CLOSING)) + Bun__uv_handle_ref(handle->loop, 1); +} + +UV_EXTERN void uv_unref(uv_handle_t* handle) +{ + if (!bun__is_supported_handle(handle)) { + __bun_throw_not_implemented("uv_unref"); + } + if (!(handle->flags & BUN_UV_HANDLE_REF)) + return; + handle->flags &= ~BUN_UV_HANDLE_REF; + if ((handle->flags & BUN_UV_HANDLE_ACTIVE) && !(handle->flags & BUN_UV_HANDLE_CLOSING)) + Bun__uv_handle_ref(handle->loop, -1); +} + +UV_EXTERN int uv_async_init(uv_loop_t* loop, uv_async_t* handle, uv_async_cb async_cb) +{ + if (loop == NULL) + return UV_EINVAL; + handle->loop = loop; + handle->type = UV_ASYNC; + handle->close_cb = NULL; + handle->next_closing = NULL; + // libuv: uv__handle_init sets REF, then uv__handle_start sets ACTIVE and + // bumps loop->active_handles. Mirror that so a freshly initialized async + // handle keeps the loop alive. + handle->flags = BUN_UV_HANDLE_REF | BUN_UV_HANDLE_ACTIVE; + handle->async_cb = async_cb; + handle->u.fd = 0; + __atomic_store_n(&handle->pending, 0, __ATOMIC_SEQ_CST); + Bun__uv_handle_ref(loop, 1); + return 0; +} + +UV_EXTERN int uv_async_send(uv_async_t* handle) +{ + // libuv coalesces: only the 0->1 transition schedules. handle->u.fd is the + // "busy" counter libuv uses so uv_close can spin until no thread is between + // the exchange and the schedule below. + if (__atomic_load_n(&handle->pending, __ATOMIC_RELAXED) != 0) + return 0; + __atomic_fetch_add(&handle->u.fd, 1, __ATOMIC_SEQ_CST); + if (__atomic_exchange_n(&handle->pending, 1, __ATOMIC_SEQ_CST) == 0) + Bun__uv_handle_schedule(handle->loop, (uv_handle_t*)handle); + __atomic_fetch_sub(&handle->u.fd, 1, __ATOMIC_SEQ_CST); + return 0; +} + +// Called from the event loop's task dispatcher on the loop thread. +UV_EXTERN void Bun__uv_handle_dispatch(uv_handle_t* handle) +{ + if (handle->type != UV_ASYNC) + return; + uv_async_t* async = (uv_async_t*)handle; + if (handle->flags & BUN_UV_HANDLE_CLOSING) { + // uv_close ran; this is the deferred close. Leave pending non-zero so + // a racing uv_async_send cannot schedule a second task behind the + // close callback. Release the loop ref uv_close held (or took) so the + // process can exit once the close callback returns. + handle->flags |= BUN_UV_HANDLE_CLOSED; + Bun__uv_handle_ref(handle->loop, -1); + if (handle->close_cb != NULL) + handle->close_cb(handle); + return; + } + // Reset before the callback so a send inside the callback schedules again, + // matching libuv's uv__async_io. + __atomic_store_n(&async->pending, 0, __ATOMIC_SEQ_CST); + if (async->async_cb != NULL) + async->async_cb(async); +} + +static void bun__uv_async_spin(uv_async_t* handle) +{ + int i; + for (;;) { + for (i = 0; i < 997; i++) { + if (__atomic_load_n(&handle->u.fd, __ATOMIC_SEQ_CST) == 0) + return; +#if defined(__i386__) || defined(__x86_64__) + __asm__ __volatile__("pause" ::: "memory"); +#elif defined(__aarch64__) || defined(__arm__) + __asm__ __volatile__("yield" ::: "memory"); +#endif + } + sched_yield(); + } +} + +UV_EXTERN void uv_close(uv_handle_t* handle, uv_close_cb close_cb) +{ + if (!bun__is_supported_handle(handle)) { + __bun_throw_not_implemented("uv_close"); + } + // libuv: assert(!uv__is_closing(handle)) before any field write. + if (handle->flags & (BUN_UV_HANDLE_CLOSING | BUN_UV_HANDLE_CLOSED)) { + assert(0); + return; + } + handle->close_cb = close_cb; + // A closing handle keeps the loop alive until close_cb runs (libuv's + // closing_handles list). If the handle was unref'd, take a ref back for + // the duration of the deferred close; Bun__uv_handle_dispatch drops it. + int had_active_ref = (handle->flags & BUN_UV_HANDLE_REF) && (handle->flags & BUN_UV_HANDLE_ACTIVE); + if (!had_active_ref) + Bun__uv_handle_ref(handle->loop, 1); + handle->flags |= BUN_UV_HANDLE_CLOSING; + handle->flags &= ~BUN_UV_HANDLE_ACTIVE; + + uv_async_t* async = (uv_async_t*)handle; + // Force pending non-zero so no uv_async_send after this point schedules a + // new task, then wait for any send that is mid-flight between its exchange + // and its schedule call. + int prev = __atomic_exchange_n(&async->pending, BUN_UV_ASYNC_CLOSING, __ATOMIC_SEQ_CST); + bun__uv_async_spin(async); + if (prev == 0) { + // No send task is or will be queued; schedule the close ourselves. + Bun__uv_handle_schedule(handle->loop, handle); + } else { + // A send already queued a task (or is about to, within the spin window + // we just waited out). That task will observe CLOSING above and run the + // close path instead of async_cb. + } +} + #endif diff --git a/src/jsc/bindings/uv-posix-stubs.c b/src/jsc/bindings/uv-posix-stubs.c index 04868a91cf9e..d51850cf0b32 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"); @@ -100,12 +86,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 +142,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"); @@ -788,30 +762,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"); @@ -824,12 +774,6 @@ UV_EXTERN const char* uv_handle_type_name(uv_handle_type type) __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 +857,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"); @@ -1366,12 +1298,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, @@ -2031,12 +1957,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"); diff --git a/src/runtime/napi/napi_body.rs b/src/runtime/napi/napi_body.rs index f4eea2cc8539..f7ecc2c96fa1 100644 --- a/src/runtime/napi/napi_body.rs +++ b/src/runtime/napi/napi_body.rs @@ -3570,6 +3570,103 @@ mod posix_platform_specific_v8_apis { } } +// ────────────────────────────────────────────────────────────────────────── +// uv_async_t bridge (POSIX only) +// ────────────────────────────────────────────────────────────────────────── +// +// On POSIX the `uv_loop_t*` that `napi_get_uv_event_loop`/`uv_default_loop` +// hand out is really a `*mut EventLoop` (see `napi_get_uv_event_loop` above). +// The C side (`src/jsc/bindings/uv-posix-polyfills.c`) owns the `uv_async_t` +// struct layout and the `flags`/`pending` bookkeeping that addons can observe; +// these shims provide the two things it cannot do from C: schedule a callback +// onto Bun's event loop, and adjust the loop's keep-alive refcount. On Windows +// Bun links real libuv and none of this is used. +// +// Worker teardown: a `uv_async_t` initialised in a Worker stores that Worker's +// `*mut EventLoop` in `handle->loop`. Unlike `ThreadSafeFunction` there is no +// env-teardown hook yet to null it when the Worker terminates (#18546), so an +// addon thread that keeps calling `uv_async_send` past `Worker::terminate()` +// without the usual `uv_close` + join in a cleanup hook can reach a freed loop. +// Handles on the main thread's loop are unaffected. + +#[cfg(unix)] +mod uv_async_posix { + use core::ffi::c_void; + + use bun_jsc::event_loop::{ConcurrentTaskItem as ConcurrentTask, EventLoop}; + use bun_jsc::virtual_machine::VirtualMachine; + + unsafe extern "C" { + fn Bun__uv_handle_dispatch(handle: *mut c_void); + } + + fn dispatch(handle: *mut c_void) -> bun_event_loop::JsResult<()> { + // SAFETY: `handle` is the live `uv_handle_t*` enqueued below; libuv's + // contract is that a handle may only be freed in or after its close + // callback, and the C side guarantees at most one task per handle is + // in the queue, so this never observes a freed handle. + unsafe { Bun__uv_handle_dispatch(handle) }; + Ok(()) + } + + /// Schedule `handle`'s callback on its loop thread. Called from + /// `uv_async_send` (any thread) and the `uv_close` path (loop thread). + #[unsafe(no_mangle)] + pub(super) extern "C" fn Bun__uv_handle_schedule( + event_loop: *const EventLoop, + handle: *mut c_void, + ) { + if event_loop.is_null() { + return; + } + // SAFETY: `event_loop` is the `*mut EventLoop` stored in `handle->loop` + // by `uv_async_init`, which came from `napi_get_uv_event_loop` or + // `uv_default_loop`; it is live for the VM lifetime and + // `enqueue_task_concurrent` is thread-safe. + let event_loop = unsafe { &*event_loop }; + event_loop.enqueue_task_concurrent(ConcurrentTask::from_callback(handle, dispatch)); + } + + /// Adjust the event loop's concurrent keep-alive refcount. `uv_async_init` + /// / `uv_ref` pass `delta > 0`; `uv_unref` / the close-callback path pass + /// `delta < 0`. Every call site is either on the loop thread or is + /// `uv_async_init` itself, so the atomic counter in `EventLoop` is + /// sufficient. + #[unsafe(no_mangle)] + pub(super) extern "C" fn Bun__uv_handle_ref( + event_loop: *const EventLoop, + delta: core::ffi::c_int, + ) { + if event_loop.is_null() { + return; + } + // SAFETY: see `Bun__uv_handle_schedule`. + let event_loop = unsafe { &*event_loop }; + if delta > 0 { + event_loop.ref_concurrently(); + } else { + event_loop.unref_concurrently(); + } + } + + /// POSIX `uv_default_loop`: the main-thread VM's event loop, which is the + /// same pointer `napi_get_uv_event_loop` returns on the main thread. + /// Addons that call `uv_default_loop()` are assuming Node's model where + /// that loop is the one JS runs on. Fall back to this thread's VM when + /// there is no main-thread VM yet (the `bun build` macro VM is created + /// with `is_main_thread: false`). + #[unsafe(no_mangle)] + pub(super) extern "C" fn Bun__uv_default_loop() -> *mut EventLoop { + let vm = VirtualMachine::get_main_thread_vm().or_else(VirtualMachine::get_or_null); + match vm { + // SAFETY: both accessors return a live per-thread or process + // singleton; `event_loop()` is a raw self-pointer into it. + Some(vm) => unsafe { &*vm }.event_loop(), + None => core::ptr::null_mut(), + } + } +} + // ────────────────────────────────────────────────────────────────────────── // uv_* symbol references (posix DCE suppression) // ────────────────────────────────────────────────────────────────────────── @@ -4056,6 +4153,13 @@ pub fn fix_dead_code_elimination() { node_api_is_sharedarraybuffer, ); + #[cfg(unix)] + keep_symbols!( + uv_async_posix::Bun__uv_handle_schedule, + uv_async_posix::Bun__uv_handle_ref, + uv_async_posix::Bun__uv_default_loop, + ); + // uv_functions_to_export // This list is hand-maintained — keep it in sync with the // `uv_functions_to_export` module above. diff --git a/test/napi/uv-stub-stuff/plugin.c b/test/napi/uv-stub-stuff/plugin.c index 932e776ec352..5eb8e37c5ed1 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(); @@ -156,14 +140,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 +207,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(); @@ -932,35 +902,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}; @@ -975,13 +916,6 @@ napi_value call_uv_func(napi_env env, napi_callback_info info) { 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 +1029,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}; @@ -1653,13 +1573,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}; @@ -2406,13 +2319,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}; diff --git a/test/napi/uv-stub-stuff/uv_impl.c b/test/napi/uv-stub-stuff/uv_impl.c index ca3b4358c576..2cdc033e27b9 100644 --- a/test/napi/uv-stub-stuff/uv_impl.c +++ b/test/napi/uv-stub-stuff/uv_impl.c @@ -1,7 +1,9 @@ #include +#include #include #include +#include #include #include #include @@ -131,6 +133,196 @@ static napi_value test_hrtime(napi_env env, napi_callback_info info) { return obj; } +// ----------------------------------------------------------------------------- +// uv_async_t tests +// ----------------------------------------------------------------------------- + +struct async_test_state { + uv_async_t async; + napi_env env; + napi_ref cb_ref; + napi_ref holder_ref; + int async_fired; + int closed; +}; + +static void async_test_on_close(uv_handle_t *handle) { + struct async_test_state *state = (struct async_test_state *)handle->data; + state->closed = 1; + + napi_handle_scope scope; + napi_open_handle_scope(state->env, &scope); + + napi_value result; + napi_create_object(state->env, &result); + napi_value v; + napi_create_int32(state->env, state->async_fired, &v); + napi_set_named_property(state->env, result, "asyncFired", v); + napi_create_int32(state->env, state->closed, &v); + napi_set_named_property(state->env, result, "closed", v); + napi_create_int32(state->env, uv_is_closing(handle), &v); + napi_set_named_property(state->env, result, "isClosingInCloseCb", v); + + napi_value cb; + napi_get_reference_value(state->env, state->cb_ref, &cb); + napi_value global; + napi_get_global(state->env, &global); + napi_call_function(state->env, global, cb, 1, &result, NULL); + + napi_delete_reference(state->env, state->cb_ref); + napi_delete_reference(state->env, state->holder_ref); + napi_close_handle_scope(state->env, scope); + free(state); +} + +static void async_test_on_async(uv_async_t *async) { + struct async_test_state *state = (struct async_test_state *)async->data; + state->async_fired += 1; + uv_close((uv_handle_t *)async, async_test_on_close); +} + +static void *async_test_sender_thread(void *arg) { + uv_async_t *async = (uv_async_t *)arg; + // Multiple sends must coalesce into a single callback invocation. + uv_async_send(async); + uv_async_send(async); + uv_async_send(async); + return NULL; +} + +// testUvAsync(useDefaultLoop: bool, sendFromThread: bool, cb: (result) => void) +static napi_value test_uv_async(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value argv[3]; + napi_get_cb_info(env, info, &argc, argv, NULL, NULL); + + bool use_default_loop = false; + bool send_from_thread = false; + napi_get_value_bool(env, argv[0], &use_default_loop); + napi_get_value_bool(env, argv[1], &send_from_thread); + + struct async_test_state *state = calloc(1, sizeof(*state)); + state->env = env; + napi_create_reference(env, argv[2], 1, &state->cb_ref); + + uv_loop_t *loop; + if (use_default_loop) { + loop = uv_default_loop(); + } else { + napi_get_uv_event_loop(env, &loop); + } + + int rc = uv_async_init(loop, &state->async, async_test_on_async); + state->async.data = state; + + napi_value ret; + napi_create_object(env, &ret); + // Keep the return object alive until the close callback runs so the test can + // read the synchronous assertions below even if GC runs first. + napi_create_reference(env, ret, 1, &state->holder_ref); + + napi_value v; + napi_create_int32(env, rc, &v); + napi_set_named_property(env, ret, "initRc", v); + napi_get_boolean(env, uv_default_loop() == loop, &v); + napi_set_named_property(env, ret, "defaultLoopMatchesNapiLoop", v); + napi_create_int32(env, (int)uv_handle_get_type((uv_handle_t *)&state->async), + &v); + napi_set_named_property(env, ret, "handleType", v); + napi_create_int32(env, (int)UV_ASYNC, &v); + napi_set_named_property(env, ret, "expectedHandleType", v); + napi_get_boolean( + env, uv_handle_get_loop((uv_handle_t *)&state->async) == loop, &v); + napi_set_named_property(env, ret, "handleLoopMatches", v); + napi_get_boolean( + env, uv_handle_get_data((uv_handle_t *)&state->async) == state, &v); + napi_set_named_property(env, ret, "handleDataMatches", v); + napi_create_int32(env, uv_has_ref((uv_handle_t *)&state->async), &v); + napi_set_named_property(env, ret, "hasRefAfterInit", v); + napi_create_int32(env, uv_is_active((uv_handle_t *)&state->async), &v); + napi_set_named_property(env, ret, "isActiveAfterInit", v); + napi_create_int32(env, uv_is_closing((uv_handle_t *)&state->async), &v); + napi_set_named_property(env, ret, "isClosingAfterInit", v); + + uv_unref((uv_handle_t *)&state->async); + napi_create_int32(env, uv_has_ref((uv_handle_t *)&state->async), &v); + napi_set_named_property(env, ret, "hasRefAfterUnref", v); + uv_ref((uv_handle_t *)&state->async); + napi_create_int32(env, uv_has_ref((uv_handle_t *)&state->async), &v); + napi_set_named_property(env, ret, "hasRefAfterReref", v); + + if (send_from_thread) { + pthread_t tid; + pthread_create(&tid, NULL, async_test_sender_thread, &state->async); + pthread_join(tid, NULL); + } else { + uv_async_send(&state->async); + uv_async_send(&state->async); + } + + // Must not fire synchronously. + napi_create_int32(env, state->async_fired, &v); + napi_set_named_property(env, ret, "firedSynchronously", v); + + return ret; +} + +static void async_test_on_async_must_not_run(uv_async_t *async) { + struct async_test_state *state = (struct async_test_state *)async->data; + state->async_fired += 1; +} + +// uv_async_send then uv_close on the same tick, before dispatch runs: close +// must take the "send already queued" path and async_cb must not fire. +static napi_value test_uv_async_close_pending(napi_env env, + napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + napi_get_cb_info(env, info, &argc, argv, NULL, NULL); + + struct async_test_state *state = calloc(1, sizeof(*state)); + state->env = env; + napi_create_reference(env, argv[0], 1, &state->cb_ref); + + uv_loop_t *loop; + napi_get_uv_event_loop(env, &loop); + uv_async_init(loop, &state->async, async_test_on_async_must_not_run); + state->async.data = state; + + napi_value ret; + napi_create_object(env, &ret); + napi_create_reference(env, ret, 1, &state->holder_ref); + + uv_async_send(&state->async); + uv_close((uv_handle_t *)&state->async, async_test_on_close); + + napi_value v; + napi_create_int32(env, state->async_fired, &v); + napi_set_named_property(env, ret, "firedSynchronously", v); + napi_create_int32(env, uv_is_closing((uv_handle_t *)&state->async), &v); + napi_set_named_property(env, ret, "isClosingAfterClose", v); + return ret; +} + +// Handle kept ref'd with no send: process must stay alive until unref. +static uv_async_t keepalive_async; +static napi_value test_uv_async_keepalive_init(napi_env env, + napi_callback_info info) { + uv_loop_t *loop; + napi_get_uv_event_loop(env, &loop); + uv_async_init(loop, &keepalive_async, NULL); + napi_value v; + napi_get_undefined(env, &v); + return v; +} +static napi_value test_uv_async_keepalive_unref(napi_env env, + napi_callback_info info) { + uv_unref((uv_handle_t *)&keepalive_async); + napi_value v; + napi_get_undefined(env, &v); + return v; +} + napi_value Init(napi_env env, napi_value exports) { // Register all test functions napi_value fn; @@ -153,6 +345,18 @@ napi_value Init(napi_env env, napi_value exports) { napi_create_function(env, NULL, 0, test_hrtime, NULL, &fn); napi_set_named_property(env, exports, "testHrtime", fn); + napi_create_function(env, NULL, 0, test_uv_async, NULL, &fn); + napi_set_named_property(env, exports, "testUvAsync", fn); + + napi_create_function(env, NULL, 0, test_uv_async_close_pending, NULL, &fn); + napi_set_named_property(env, exports, "testUvAsyncClosePending", fn); + + napi_create_function(env, NULL, 0, test_uv_async_keepalive_init, NULL, &fn); + napi_set_named_property(env, exports, "testUvAsyncKeepaliveInit", fn); + + napi_create_function(env, NULL, 0, test_uv_async_keepalive_unref, NULL, &fn); + napi_set_named_property(env, exports, "testUvAsyncKeepaliveUnref", fn); + return exports; } diff --git a/test/napi/uv.test.ts b/test/napi/uv.test.ts index e6b523b0b6f2..a09d3232f26f 100644 --- a/test/napi/uv.test.ts +++ b/test/napi/uv.test.ts @@ -1,10 +1,7 @@ import { afterEach, beforeAll, describe, expect, test } from "bun:test"; import { bunEnv, bunExe, isWindows, tempDirWithFiles } from "harness"; import path from "node:path"; -import { symbols, test_skipped } from "../../src/jsc/bindings/libuv/generate_uv_posix_stubs_constants"; -import source from "./uv-stub-stuff/uv_impl.c"; - -const symbols_to_test = symbols.filter(s => !test_skipped.includes(s)); +import source from "./uv-stub-stuff/uv_impl.c" with { type: "file" }; // We use libuv on Windows describe.if(!isWindows)("uv stubs", () => { @@ -112,4 +109,116 @@ describe.if(!isWindows)("uv stubs", () => { // Let's say not more than 100ms (100,000,000 ns) expect(diff <= 100_000_000n).toBe(true); }); + + // Run the uv_async_t tests in a subprocess: the async callback is deferred to + // the next event-loop tick, and on older builds these functions abort the + // process. + async function runUvAsync(useDefaultLoop: boolean, sendFromThread: boolean) { + const addon = path.join(tempdir, "./build/Release/uv_test.node"); + const script = ` + const addon = require(${JSON.stringify(addon)}); + const sync = addon.testUvAsync(${useDefaultLoop}, ${sendFromThread}, result => { + console.log(JSON.stringify({ sync, result })); + }); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", script], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout.trim().length).toBeGreaterThan(0); + const out = JSON.parse(stdout.trim()); + expect(out).toEqual({ + sync: { + initRc: 0, + defaultLoopMatchesNapiLoop: true, + handleType: out.sync.expectedHandleType, + expectedHandleType: out.sync.expectedHandleType, + handleLoopMatches: true, + handleDataMatches: true, + hasRefAfterInit: 1, + isActiveAfterInit: 1, + isClosingAfterInit: 0, + hasRefAfterUnref: 0, + hasRefAfterReref: 1, + firedSynchronously: 0, + }, + result: { + asyncFired: 1, + closed: 1, + isClosingInCloseCb: 1, + }, + }); + expect(exitCode).toBe(0); + } + + test.concurrent("uv_async: napi_get_uv_event_loop, send from loop thread", async () => { + await runUvAsync(false, false); + }); + + test.concurrent("uv_async: napi_get_uv_event_loop, send from another thread", async () => { + await runUvAsync(false, true); + }); + + test.concurrent("uv_async: uv_default_loop, send from loop thread", async () => { + await runUvAsync(true, false); + }); + + test.concurrent("uv_async: uv_default_loop, send from another thread", async () => { + await runUvAsync(true, true); + }); + + test.concurrent("uv_async: close with a send already queued skips async_cb", async () => { + const addon = path.join(tempdir, "./build/Release/uv_test.node"); + const script = ` + const addon = require(${JSON.stringify(addon)}); + const sync = addon.testUvAsyncClosePending(result => { + console.log(JSON.stringify({ sync, result })); + }); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", script], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + const out = JSON.parse(stdout.trim()); + expect(out).toEqual({ + sync: { firedSynchronously: 0, isClosingAfterClose: 1 }, + result: { asyncFired: 0, closed: 1, isClosingInCloseCb: 1 }, + }); + expect(exitCode).toBe(0); + }); + + test.concurrent("uv_async: ref'd handle keeps the process alive until unref", async () => { + const addon = path.join(tempdir, "./build/Release/uv_test.node"); + // The timer is unref'd so the uv_async handle is the only thing keeping + // the loop alive; if its loop ref is a no-op the process exits before the + // timer fires and stdout is empty. + const script = ` + const addon = require(${JSON.stringify(addon)}); + addon.testUvAsyncKeepaliveInit(); + setTimeout(() => { + console.log("alive"); + addon.testUvAsyncKeepaliveUnref(); + // After unref there is nothing keeping the loop alive; the process + // must exit on its own without an explicit process.exit(). + }, 20).unref(); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", script], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe("alive\n"); + expect(exitCode).toBe(0); + }); });