Implement uv_mutex_* fns and others (#18555)

This commit is contained in:
Zack Radisic
2025-04-01 19:08:32 -07:00
committed by GitHub
parent 575d2c40a8
commit 38a776a404
16 changed files with 535 additions and 84 deletions

View File

@@ -142,6 +142,14 @@ if(UNIX)
-fno-unwind-tables
-fno-asynchronous-unwind-tables
)
# needed for libuv stubs because they use
# C23 feature which lets you define parameter without
# name
register_compiler_flags(
DESCRIPTION "Allow C23 extensions"
-Wno-c23-extensions
)
endif()
register_compiler_flags(

View File

@@ -635,6 +635,8 @@ file(GLOB BUN_C_SOURCES ${CONFIGURE_DEPENDS}
${BUN_USOCKETS_SOURCE}/src/eventing/*.c
${BUN_USOCKETS_SOURCE}/src/internal/*.c
${BUN_USOCKETS_SOURCE}/src/crypto/*.c
${CWD}/src/bun.js/bindings/uv-posix-polyfills.c
${CWD}/src/bun.js/bindings/uv-posix-stubs.c
)
if(WIN32)

View File

@@ -274,9 +274,9 @@ ${parts.map(([stub, _]) => stub).join("\n\n")}
`;
await Bun.write(join(import.meta.dir, "../", "uv-posix-stubs.cpp"), final_contents);
await Bun.write(join(import.meta.dir, "../", "uv-posix-stubs.c"), final_contents);
if (Bun.which("clang-format")) {
await Bun.$`clang-format -i ${join(import.meta.dir, "../", "uv-posix-stubs.cpp")}`;
await Bun.$`clang-format -i ${join(import.meta.dir, "../", "uv-posix-stubs.c")}`;
}
const test_plugin_contents = ` // GENERATED CODE ... NO TOUCHY!!

View File

@@ -123,7 +123,8 @@ export const symbols = [
"uv_handle_size",
"uv_handle_type_name",
"uv_has_ref",
"uv_hrtime",
// Defined in uv-posix-polyfills.cpp
// "uv_hrtime",
"uv_idle_init",
"uv_idle_start",
"uv_idle_stop",
@@ -161,14 +162,16 @@ export const symbols = [
"uv_loop_size",
"uv_metrics_idle_time",
"uv_metrics_info",
"uv_mutex_destroy",
"uv_mutex_init",
"uv_mutex_init_recursive",
"uv_mutex_lock",
"uv_mutex_trylock",
"uv_mutex_unlock",
// Defined in uv-posix-polyfills.cpp
// "uv_mutex_destroy",
// "uv_mutex_init",
// "uv_mutex_init_recursive",
// "uv_mutex_lock",
// "uv_mutex_trylock",
// "uv_mutex_unlock",
"uv_now",
"uv_once",
// Defined in uv-posix-polyfills.cpp
// "uv_once",
"uv_open_osfhandle",
"uv_os_environ",
"uv_os_free_environ",

View File

@@ -71,6 +71,9 @@
#define WEBCORE_EXPORT JS_EXPORT_PRIVATE
#endif
#include <wtf/Platform.h>
#ifdef __cplusplus
#include <wtf/PlatformCallingConventions.h>
#include <JavaScriptCore/JSCJSValue.h>
#include <wtf/text/MakeString.h>
@@ -78,6 +81,7 @@
#include <JavaScriptCore/HandleSet.h>
#include <wtf/Ref.h>
#include <wtf/ThreadSafeRefCounted.h>
#endif
#define ENABLE_WEB_CRYPTO 1
#define USE_OPENSSL 1

View File

@@ -0,0 +1,20 @@
#include "uv-posix-polyfills.h"
#include <mach/mach.h>
#include <mach/mach_time.h>
#include <stdlib.h>
static uv_once_t once = UV_ONCE_INIT;
static mach_timebase_info_data_t timebase;
static void uv__hrtime_init_once(void)
{
if (KERN_SUCCESS != mach_timebase_info(&timebase))
abort();
}
uint64_t uv__hrtime(uv_clocktype_t type)
{
uv_once(&once, uv__hrtime_init_once);
return mach_continuous_time() * timebase.numer / timebase.denom;
}

View File

@@ -0,0 +1,42 @@
#include "uv-posix-polyfills.h"
#include <stdatomic.h>
#include <time.h>
#include <unistd.h>
uint64_t uv__hrtime(uv_clocktype_t type)
{
static _Atomic clock_t fast_clock_id = -1;
struct timespec t;
clock_t clock_id;
/* Prefer CLOCK_MONOTONIC_COARSE if available but only when it has
* millisecond granularity or better. CLOCK_MONOTONIC_COARSE is
* serviced entirely from the vDSO, whereas CLOCK_MONOTONIC may
* decide to make a costly system call.
*/
/* TODO(bnoordhuis) Use CLOCK_MONOTONIC_COARSE for UV_CLOCK_PRECISE
* when it has microsecond granularity or better (unlikely).
*/
clock_id = CLOCK_MONOTONIC;
if (type != UV_CLOCK_FAST)
goto done;
clock_id = atomic_load_explicit(&fast_clock_id, memory_order_relaxed);
if (clock_id != -1)
goto done;
clock_id = CLOCK_MONOTONIC;
if (0 == clock_getres(CLOCK_MONOTONIC_COARSE, &t))
if (t.tv_nsec <= 1 * 1000 * 1000)
clock_id = CLOCK_MONOTONIC_COARSE;
atomic_store_explicit(&fast_clock_id, clock_id, memory_order_relaxed);
done:
if (clock_gettime(clock_id, &t))
return 0; /* Not really possible. */
return t.tv_sec * (uint64_t)1e9 + t.tv_nsec;
}

View File

@@ -0,0 +1,15 @@
#include "uv-posix-polyfills.h"
#include <stdint.h>
#include <stdlib.h>
#include <time.h>
uint64_t uv__hrtime(uv_clocktype_t type)
{
struct timespec t;
if (clock_gettime(CLOCK_MONOTONIC, &t))
abort();
return t.tv_sec * (uint64_t)1e9 + t.tv_nsec;
}

View File

@@ -0,0 +1,141 @@
#include "uv-posix-polyfills.h"
#if OS(LINUX) || OS(DARWIN)
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
// libuv does the annoying thing of #undef'ing these
#include <errno.h>
#if EDOM > 0
#define UV__ERR(x) (-(x))
#else
#define UV__ERR(x) (x)
#endif
void __bun_throw_not_implemented(const char* symbol_name)
{
CrashHandler__unsupportedUVFunction(symbol_name);
}
// Internals
uint64_t uv__hrtime(uv_clocktype_t type);
#if defined(__linux__)
#include "uv-posix-polyfills-linux.c"
// #elif defined(__MVS__)
// #include "uv/os390.h"
// #elif defined(__PASE__) /* __PASE__ and _AIX are both defined on IBM i */
// #include "uv/posix.h" /* IBM i needs uv/posix.h, not uv/aix.h */
// #elif defined(_AIX)
// #include "uv/aix.h"
// #elif defined(__sun)
// #include "uv/sunos.h"
#elif defined(__APPLE__)
#include "uv-posix-polyfills-darwin.c"
// #elif defined(__DragonFly__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
// #include "uv/bsd.h"
#elif defined(__CYGWIN__) || defined(__MSYS__) || defined(__HAIKU__) || defined(__QNX__) || defined(__GNU__)
#include "uv-posix-polyfills-posix.c"
#endif
uv_pid_t uv_os_getpid()
{
return getpid();
}
uv_pid_t uv_os_getppid()
{
return getppid();
}
UV_EXTERN void uv_once(uv_once_t* guard, void (*callback)(void))
{
if (pthread_once(guard, callback))
abort();
}
UV_EXTERN uint64_t uv_hrtime(void)
{
return uv__hrtime(UV_CLOCK_PRECISE);
}
// Copy-pasted from libuv
UV_EXTERN void uv_mutex_destroy(uv_mutex_t* mutex)
{
if (pthread_mutex_destroy(mutex))
abort();
}
// Copy-pasted from libuv
UV_EXTERN int uv_mutex_init(uv_mutex_t* mutex)
{
pthread_mutexattr_t attr;
int err;
if (pthread_mutexattr_init(&attr))
abort();
if (pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK))
abort();
err = pthread_mutex_init(mutex, &attr);
if (pthread_mutexattr_destroy(&attr))
abort();
return UV__ERR(err);
}
// Copy-pasted from libuv
UV_EXTERN int uv_mutex_init_recursive(uv_mutex_t* mutex)
{
pthread_mutexattr_t attr;
int err;
if (pthread_mutexattr_init(&attr))
abort();
if (pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE))
abort();
err = pthread_mutex_init(mutex, &attr);
if (pthread_mutexattr_destroy(&attr))
abort();
return UV__ERR(err);
}
// Copy-pasted from libuv
UV_EXTERN void uv_mutex_lock(uv_mutex_t* mutex)
{
if (pthread_mutex_lock(mutex))
abort();
}
// Copy-pasted from libuv
UV_EXTERN int uv_mutex_trylock(uv_mutex_t* mutex)
{
int err;
err = pthread_mutex_trylock(mutex);
if (err) {
if (err != EBUSY && err != EAGAIN)
abort();
return UV_EBUSY;
}
return 0;
}
// Copy-pasted from libuv
UV_EXTERN void uv_mutex_unlock(uv_mutex_t* mutex)
{
if (pthread_mutex_unlock(mutex))
abort();
}
#endif

View File

@@ -1,20 +0,0 @@
#include "uv-posix-polyfills.h"
#if OS(LINUX) || OS(DARWIN)
void __bun_throw_not_implemented(const char* symbol_name)
{
CrashHandler__unsupportedUVFunction(symbol_name);
}
uv_pid_t uv_os_getpid()
{
return getpid();
}
uv_pid_t uv_os_getppid()
{
return getppid();
}
#endif

View File

@@ -8,12 +8,17 @@
// These functions are called by the stubs to crash with a nice error message
// when accessing a libuv functin which we do not support on posix
extern "C" void CrashHandler__unsupportedUVFunction(const char* function_name);
void CrashHandler__unsupportedUVFunction(const char* function_name);
void __bun_throw_not_implemented(const char* symbol_name);
// libuv headers will use UV_EXTERN
#define UV_EXTERN extern "C" __attribute__((visibility("default"))) __attribute__((used))
#define UV_EXTERN __attribute__((visibility("default"))) __attribute__((used))
#include <uv.h>
typedef enum {
UV_CLOCK_PRECISE = 0, /* Use the highest resolution clock available. */
UV_CLOCK_FAST = 1 /* Use the fastest clock with <= 1ms granularity. */
} uv_clocktype_t;
#endif

View File

@@ -830,12 +830,6 @@ UV_EXTERN int uv_has_ref(const uv_handle_t*)
__builtin_unreachable();
}
UV_EXTERN uint64_t uv_hrtime(void)
{
__bun_throw_not_implemented("uv_hrtime");
__builtin_unreachable();
}
UV_EXTERN int uv_idle_init(uv_loop_t*, uv_idle_t* idle)
{
__bun_throw_not_implemented("uv_idle_init");
@@ -1063,54 +1057,12 @@ UV_EXTERN int uv_metrics_info(uv_loop_t* loop, uv_metrics_t* metrics)
__builtin_unreachable();
}
UV_EXTERN void uv_mutex_destroy(uv_mutex_t* handle)
{
__bun_throw_not_implemented("uv_mutex_destroy");
__builtin_unreachable();
}
UV_EXTERN int uv_mutex_init(uv_mutex_t* handle)
{
__bun_throw_not_implemented("uv_mutex_init");
__builtin_unreachable();
}
UV_EXTERN int uv_mutex_init_recursive(uv_mutex_t* handle)
{
__bun_throw_not_implemented("uv_mutex_init_recursive");
__builtin_unreachable();
}
UV_EXTERN void uv_mutex_lock(uv_mutex_t* handle)
{
__bun_throw_not_implemented("uv_mutex_lock");
__builtin_unreachable();
}
UV_EXTERN int uv_mutex_trylock(uv_mutex_t* handle)
{
__bun_throw_not_implemented("uv_mutex_trylock");
__builtin_unreachable();
}
UV_EXTERN void uv_mutex_unlock(uv_mutex_t* handle)
{
__bun_throw_not_implemented("uv_mutex_unlock");
__builtin_unreachable();
}
UV_EXTERN uint64_t uv_now(const uv_loop_t*)
{
__bun_throw_not_implemented("uv_now");
__builtin_unreachable();
}
UV_EXTERN void uv_once(uv_once_t* guard, void (*callback)(void))
{
__bun_throw_not_implemented("uv_once");
__builtin_unreachable();
}
UV_EXTERN int uv_open_osfhandle(uv_os_fd_t os_fd)
{
__bun_throw_not_implemented("uv_open_osfhandle");

View File

@@ -449,6 +449,7 @@ pub const Run = struct {
}
JSC.napi.fixDeadCodeElimination();
bun.crash_handler.fixDeadCodeElimination();
vm.globalExit();
}

View File

@@ -255,11 +255,12 @@ pub fn crashHandler(
\\Bun is actively working on supporting all libuv functions for POSIX
\\systems, please see this issue to track our progress:
\\
\\<cyan>https://github.com/oven-sh/bun/issues/4290<r>
\\<cyan>https://github.com/oven-sh/bun/issues/18546<r>
\\
\\
;
writer.print(Output.prettyFmt(fmt, true), .{name}) catch std.posix.abort();
has_printed_message = true;
}
} else {
if (Output.enable_ansi_colors) {
@@ -368,7 +369,7 @@ pub fn crashHandler(
\\Bun is actively working on supporting all libuv functions for POSIX
\\systems, please see this issue to track our progress:
\\
\\<cyan>https://github.com/oven-sh/bun/issues/4290<r>
\\<cyan>https://github.com/oven-sh/bun/issues/18546<r>
\\
\\
;
@@ -1862,7 +1863,7 @@ export fn CrashHandler__setInsideNativePlugin(name: ?[*:0]const u8) callconv(.C)
inside_native_plugin = name;
}
fn unsupportUVFunction(name: ?[*:0]const u8) callconv(.C) void {
export fn CrashHandler__unsupportedUVFunction(name: ?[*:0]const u8) callconv(.C) void {
bun.analytics.Features.unsupported_uv_function += 1;
unsupported_uv_function = name;
std.debug.panic("unsupported uv function: {s}", .{name.?});
@@ -1882,9 +1883,13 @@ export fn CrashHandler__setDlOpenAction(action: ?[*:0]const u8) void {
}
}
pub fn fixDeadCodeElimination() void {
std.mem.doNotOptimizeAway(&CrashHandler__unsupportedUVFunction);
}
comptime {
_ = &Bun__crashHandler;
if (!bun.Environment.isWindows) {
@export(&unsupportUVFunction, .{ .name = "CrashHandler__unsupportedUVFunction" });
std.mem.doNotOptimizeAway(&CrashHandler__unsupportedUVFunction);
// @export(&unsupportUVFunction, .{ .name = "CrashHandler__unsupportedUVFunction", .linkage = .strong });
}
}

View File

@@ -0,0 +1,159 @@
#include <node_api.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <uv.h>
// Test mutex initialization and destruction
static napi_value test_mutex_init_destroy(napi_env env,
napi_callback_info info) {
uv_mutex_t mutex;
int result = uv_mutex_init(&mutex);
if (result != 0) {
napi_throw_error(env, NULL, "Failed to initialize mutex");
return NULL;
}
uv_mutex_destroy(&mutex);
napi_value ret;
napi_get_boolean(env, true, &ret);
return ret;
}
// Test recursive mutex
static napi_value test_mutex_recursive(napi_env env, napi_callback_info info) {
uv_mutex_t mutex;
int result = uv_mutex_init_recursive(&mutex);
if (result != 0) {
napi_throw_error(env, NULL, "Failed to initialize recursive mutex");
return NULL;
}
// Try locking multiple times
uv_mutex_lock(&mutex);
uv_mutex_lock(&mutex);
// Unlock the same number of times
uv_mutex_unlock(&mutex);
uv_mutex_unlock(&mutex);
uv_mutex_destroy(&mutex);
napi_value ret;
napi_get_boolean(env, true, &ret);
return ret;
}
// Test mutex trylock
static napi_value test_mutex_trylock(napi_env env, napi_callback_info info) {
uv_mutex_t mutex;
uv_mutex_init(&mutex);
int result = uv_mutex_trylock(&mutex);
if (result != 0) {
uv_mutex_destroy(&mutex);
napi_throw_error(env, NULL, "Failed to trylock mutex");
return NULL;
}
uv_mutex_unlock(&mutex);
uv_mutex_destroy(&mutex);
napi_value ret;
napi_get_boolean(env, true, &ret);
return ret;
}
// Test getpid and getppid
static napi_value test_process_ids(napi_env env, napi_callback_info info) {
uv_pid_t pid = uv_os_getpid();
uv_pid_t ppid = uv_os_getppid();
// Create return object with pid and ppid
napi_value obj;
napi_create_object(env, &obj);
napi_value pid_value, ppid_value;
napi_create_int32(env, pid, &pid_value);
napi_create_int32(env, ppid, &ppid_value);
napi_set_named_property(env, obj, "pid", pid_value);
napi_set_named_property(env, obj, "ppid", ppid_value);
return obj;
}
int count = 0;
// Test uv_once
static void once_callback(void) {
// Just a dummy callback
count++;
}
uv_once_t guard = UV_ONCE_INIT;
static napi_value test_uv_once(napi_env env, napi_callback_info info) {
uv_once(&guard, once_callback);
napi_value ret;
napi_create_int32(env, count, &ret);
return ret;
}
// Test uv_hrtime
static napi_value test_hrtime(napi_env env, napi_callback_info info) {
uint64_t time1 = uv_hrtime();
// Sleep for a tiny bit to ensure time passes
usleep(1000); // Sleep for 1ms
uint64_t time2 = uv_hrtime();
// Create return object with both timestamps
napi_value obj;
napi_create_object(env, &obj);
// Convert uint64_t to two int32 values (high and low bits)
// because JavaScript numbers can't safely handle 64-bit integers
napi_value time1_low, time1_high, time2_low, time2_high;
napi_create_int32(env, (int32_t)(time1 & 0xFFFFFFFF), &time1_low);
napi_create_int32(env, (int32_t)(time1 >> 32), &time1_high);
napi_create_int32(env, (int32_t)(time2 & 0xFFFFFFFF), &time2_low);
napi_create_int32(env, (int32_t)(time2 >> 32), &time2_high);
napi_set_named_property(env, obj, "time1Low", time1_low);
napi_set_named_property(env, obj, "time1High", time1_high);
napi_set_named_property(env, obj, "time2Low", time2_low);
napi_set_named_property(env, obj, "time2High", time2_high);
return obj;
}
napi_value Init(napi_env env, napi_value exports) {
// Register all test functions
napi_value fn;
napi_create_function(env, NULL, 0, test_mutex_init_destroy, NULL, &fn);
napi_set_named_property(env, exports, "testMutexInitDestroy", fn);
napi_create_function(env, NULL, 0, test_mutex_recursive, NULL, &fn);
napi_set_named_property(env, exports, "testMutexRecursive", fn);
napi_create_function(env, NULL, 0, test_mutex_trylock, NULL, &fn);
napi_set_named_property(env, exports, "testMutexTrylock", fn);
napi_create_function(env, NULL, 0, test_process_ids, NULL, &fn);
napi_set_named_property(env, exports, "testProcessIds", fn);
napi_create_function(env, NULL, 0, test_uv_once, NULL, &fn);
napi_set_named_property(env, exports, "testUvOnce", fn);
napi_create_function(env, NULL, 0, test_hrtime, NULL, &fn);
napi_set_named_property(env, exports, "testHrtime", fn);
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

114
test/napi/uv.test.ts Normal file
View File

@@ -0,0 +1,114 @@
import { beforeAll, describe, expect, afterEach, test } from "bun:test";
import path from "node:path";
import { bunEnv, bunExe, makeTree, tempDirWithFiles, isWindows } from "harness";
import source from "./uv-stub-stuff/uv_impl.c";
import { symbols, test_skipped } from "../../src/bun.js/bindings/libuv/generate_uv_posix_stubs_constants";
const symbols_to_test = symbols.filter(s => !test_skipped.includes(s));
// We use libuv on Windows
describe.if(!isWindows)("uv stubs", () => {
const cwd = process.cwd();
let tempdir: string = "";
let outdir: string = "";
let nativeModule: any;
beforeAll(async () => {
const files = {
"uv_impl.c": await Bun.file(source).text(),
"package.json": JSON.stringify({
"name": "fake-plugin",
"module": "index.ts",
"type": "module",
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5.0.0",
},
"scripts": {
"build:napi": "node-gyp configure && node-gyp build",
},
"dependencies": {
"node-gyp": "10.2.0",
},
}),
"binding.gyp": `{
"targets": [
{
"target_name": "uv_test",
"sources": [ "uv_impl.c" ],
"include_dirs": [ ".", "./libuv" ],
"cflags": ["-fPIC"],
"ldflags": ["-Wl,--export-dynamic"]
},
]
}`,
};
tempdir = tempDirWithFiles("uv-tests", files);
await makeTree(tempdir, files);
outdir = path.join(tempdir, "dist");
process.chdir(tempdir);
const libuvDir = path.join(__dirname, "../../src/bun.js/bindings/libuv");
await Bun.$`cp -R ${libuvDir} ${path.join(tempdir, "libuv")}`;
await Bun.$`${bunExe()} i && ${bunExe()} build:napi`.env(bunEnv).cwd(tempdir);
nativeModule = require(path.join(tempdir, "./build/Release/uv_test.node"));
});
afterEach(() => {
process.chdir(cwd);
});
test("mutex init and destroy", () => {
expect(() => nativeModule.testMutexInitDestroy()).not.toThrow();
});
test("recursive mutex", () => {
expect(() => nativeModule.testMutexRecursive()).not.toThrow();
});
test("mutex trylock", () => {
expect(() => nativeModule.testMutexTrylock()).not.toThrow();
});
test("process IDs", () => {
const result = nativeModule.testProcessIds();
expect(result).toHaveProperty("pid");
expect(result).toHaveProperty("ppid");
expect(result.pid).toBeGreaterThan(0);
expect(result.ppid).toBeGreaterThan(0);
// The process ID should match Node's process.pid
expect(result.pid).toBe(process.pid);
});
test("uv_once", () => {
expect(nativeModule.testUvOnce()).toBe(1);
expect(nativeModule.testUvOnce()).toBe(1);
expect(nativeModule.testUvOnce()).toBe(1);
});
test("hrtime", () => {
const result = nativeModule.testHrtime();
// Reconstruct the 64-bit values
const time1 = (BigInt(result.time1High) << 32n) | BigInt(result.time1Low >>> 0);
const time2 = (BigInt(result.time2High) << 32n) | BigInt(result.time2Low >>> 0);
// Verify that:
// 1. time2 is greater than time1 (time passed)
expect(time2 > time1).toBe(true);
// 2. The difference should be at least 1ms (we slept for 1ms)
// hrtime is in nanoseconds, so 1ms = 1,000,000 ns
const diff = time2 - time1;
expect(diff >= 1_000_000n).toBe(true);
// 3. The difference shouldn't be unreasonably large
// Let's say not more than 100ms (100,000,000 ns)
expect(diff <= 100_000_000n).toBe(true);
});
});