diff --git a/AGENTS.md b/AGENTS.md deleted file mode 120000 index 681311eb9cf4..000000000000 --- a/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -CLAUDE.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000000..ceb2b988dc8a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md diff --git a/scripts/build/rust.ts b/scripts/build/rust.ts index 7fb47e357ab5..188666804eab 100644 --- a/scripts/build/rust.ts +++ b/scripts/build/rust.ts @@ -486,7 +486,9 @@ export function emitRust(n: Ninja, cfg: Config, inputs: RustBuildInputs): string // `-fuse-ld=`. RUSTFLAGS only reach *target* crates when `--target` is given, // and the `bun_bin` staticlib has no link step, so it's normally dead — but // if a target cdylib ever appears it'd fail with "could not open '-fuse-ld=lld'". - if (!cfg.windows) rustflags.push(`-Clink-arg=-fuse-ld=lld`); + if (!cfg.windows && !cfg.darwin) { + rustflags.push(`-Clink-arg=-fuse-ld=lld`); + } if (cfg.crossLangLto) { // Cross-language LTO: emit LLVM bitcode (not machine code) into the .a // so the final lld `-flto=full` link sees through Rust↔C++ call edges. diff --git a/src/jsc/bindings/InternalModuleRegistry.cpp b/src/jsc/bindings/InternalModuleRegistry.cpp index 37329fdde143..13d2bb838446 100644 --- a/src/jsc/bindings/InternalModuleRegistry.cpp +++ b/src/jsc/bindings/InternalModuleRegistry.cpp @@ -100,7 +100,7 @@ ALWAYS_INLINE JSC::JSValue generateNativeModule( #ifdef BUN_DYNAMIC_JS_LOAD_PATH JSValue initializeInternalModuleFromDisk(JSGlobalObject* globalObject, VM& vm, const WTF::String& moduleName, WTF::String fileBase, const WTF::String& urlString) { - WTF::String file = makeString(ASCIILiteral::fromLiteralUnsafe(BUN_DYNAMIC_JS_LOAD_PATH), "/"_s, WTF::move(fileBase)); + WTF::String file = makeString(WTF::String::fromUTF8(BUN_DYNAMIC_JS_LOAD_PATH), "/"_s, WTF::move(fileBase)); if (auto contents = WTF::FileSystemImpl::readEntireFile(file)) { auto string = WTF::String::fromUTF8(contents.value()); return generateModule(globalObject, vm, string, moduleName, urlString); diff --git a/src/jsc/bindings/JSFFIFunction.cpp b/src/jsc/bindings/JSFFIFunction.cpp index bbf982c37c78..0060b527ba6c 100644 --- a/src/jsc/bindings/JSFFIFunction.cpp +++ b/src/jsc/bindings/JSFFIFunction.cpp @@ -36,25 +36,40 @@ #include "DOMJITIDLType.h" #include "DOMJITIDLTypeFilter.h" #include "DOMJITHelpers.h" +#include "ScriptExecutionContext.h" -class FFICallbackFunctionWrapper { +class FFICallbackFunctionWrapper final : public WTF::ThreadSafeRefCounted { WTF_DEPRECATED_MAKE_FAST_ALLOCATED(FFICallbackFunctionWrapper); public: + static Ref create(JSC::JSFunction* function, Zig::GlobalObject* globalObject) + { + return adoptRef(*new FFICallbackFunctionWrapper(function, globalObject)); + } + JSC::Strong m_function; JSC::Strong globalObject; - ~FFICallbackFunctionWrapper() = default; + + WebCore::ScriptExecutionContextIdentifier contextIdentifier() const { return m_contextIdentifier; } FFICallbackFunctionWrapper(JSC::JSFunction* function, Zig::GlobalObject* globalObject) : m_function(globalObject->vm(), function) , globalObject(globalObject->vm(), globalObject) + , m_contextIdentifier(globalObject->scriptExecutionContext()->identifier()) { } + +private: + friend class WTF::ThreadSafeRefCounted; + + ~FFICallbackFunctionWrapper() = default; + + const WebCore::ScriptExecutionContextIdentifier m_contextIdentifier; }; extern "C" void FFICallbackFunctionWrapper_destroy(FFICallbackFunctionWrapper* wrapper) { - delete wrapper; + wrapper->deref(); } extern "C" FFICallbackFunctionWrapper* Bun__createFFICallbackFunction( @@ -66,9 +81,7 @@ extern "C" FFICallbackFunctionWrapper* Bun__createFFICallbackFunction( auto* callbackFunction = uncheckedDowncast(JSC::JSValue::decode(callbackFn)); - auto* wrapper = new FFICallbackFunctionWrapper(callbackFunction, globalObject); - - return wrapper; + return &FFICallbackFunctionWrapper::create(callbackFunction, globalObject).leakRef(); } extern "C" Zig::JSFFIFunction* Bun__CreateFFIFunctionWithData(Zig::GlobalObject* globalObject, const ZigString* symbolName, unsigned argCount, Zig::FFIFunction functionPointer, void* data) @@ -206,17 +219,19 @@ FFI_Callback_call(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::Enc extern "C" void FFI_Callback_threadsafe_call(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args) { - - auto* globalObject = wrapper.globalObject.get(); + // This can run on an arbitrary native thread. Keep JSC::Strong access on the + // posted JS task; touching it here can race with GC's handle visitation. + Ref protectedWrapper { wrapper }; + auto contextIdentifier = protectedWrapper->contextIdentifier(); WTF::Vector argsVec; for (size_t i = 0; i < argCount; ++i) argsVec.append(args[i]); - WebCore::ScriptExecutionContext::postTaskTo(globalObject->scriptExecutionContext()->identifier(), [argsVec = WTF::move(argsVec), wrapper](WebCore::ScriptExecutionContext& ctx) mutable { + WebCore::ScriptExecutionContext::postTaskTo(contextIdentifier, [argsVec = WTF::move(argsVec), protectedWrapper = WTF::move(protectedWrapper)](WebCore::ScriptExecutionContext& ctx) mutable { auto* globalObject = uncheckedDowncast(ctx.jsGlobalObject()); auto& vm = JSC::getVM(globalObject); JSC::MarkedArgumentBuffer arguments; - auto* function = wrapper.m_function.get(); + auto* function = protectedWrapper->m_function.get(); for (size_t i = 0; i < argsVec.size(); ++i) arguments.appendWithCrashOnOverflow(JSC::JSValue::decode(argsVec[i])); WTF::NakedPtr exception; diff --git a/test/js/bun/ffi/cc.test.ts b/test/js/bun/ffi/cc.test.ts index 7df4e4554373..36939242e390 100644 --- a/test/js/bun/ffi/cc.test.ts +++ b/test/js/bun/ffi/cc.test.ts @@ -1,4 +1,4 @@ -import { cc, CString, ptr, type FFIFunction, type Library } from "bun:ffi"; +import { cc, CString, JSCallback, ptr, type FFIFunction, type Library } from "bun:ffi"; import { afterAll, beforeAll, describe, expect, it } from "bun:test"; import { promises as fs } from "fs"; import { bunEnv, bunExe, isArm64, isASAN, isWindows, tempDirWithFiles } from "harness"; @@ -96,6 +96,116 @@ describe.skipIf(isASAN || isFFIUnavailable)("given an add(a, b) function", () => }); }); // +describe.skipIf(isWindows || isASAN || isFFIUnavailable)("threadsafe JSCallback", () => { + const source = /* c */ ` + typedef void (*callback_t)(int); + + #ifdef __APPLE__ + typedef struct _opaque_pthread_t* pthread_t; + #else + typedef unsigned long pthread_t; + #endif + + extern int pthread_create(pthread_t*, const void*, void* (*)(void*), void*); + extern int pthread_detach(pthread_t); + + static callback_t active_callback; + static int active_count; + static _Atomic int callbacks_finished; + + static void* run_callbacks(void* unused) { + (void)unused; + for (int i = 0; i < active_count; i++) { + active_callback(i); + } + callbacks_finished = 1; + return (void*)0; + } + + int start_threadsafe_callbacks(callback_t callback, int count) { + pthread_t thread; + active_callback = callback; + active_count = count; + callbacks_finished = 0; + if (pthread_create(&thread, (void*)0, run_callbacks, (void*)0) != 0) { + return -1; + } + pthread_detach(thread); + return count; + } + + int threadsafe_callbacks_finished(void) { + return callbacks_finished; + } + `; + + let dir: string; + let library: Library<{ + start_threadsafe_callbacks: { args: ["ptr", "int"]; returns: "int" }; + threadsafe_callbacks_finished: { args: []; returns: "int" }; + }>; + + beforeAll(() => { + dir = tempDirWithFiles("bun-ffi-threadsafe-callback-test", { + "callback.c": source, + }); + library = cc({ + source: path.join(dir, "callback.c"), + library: "pthread", + symbols: { + start_threadsafe_callbacks: { + returns: "int", + args: ["ptr", "int"], + }, + threadsafe_callbacks_finished: { + returns: "int", + args: [], + }, + }, + }); + }); + + afterAll(async () => { + library?.close(); + if (dir) { + await fs.rm(dir, { recursive: true, force: true }); + } + }); + + it("can be called repeatedly from a native thread while the JS thread runs GC", async () => { + const count = 4096; + const values: number[] = []; + const callback = new JSCallback( + value => { + values.push(value); + }, + { + args: ["int"], + returns: "void", + threadsafe: true, + }, + ); + + try { + expect(library.symbols.start_threadsafe_callbacks(callback.ptr, count)).toBe(count); + + for (let i = 0; i < 4096 && values.length < count; i++) { + Bun.gc(true); + await Bun.sleep(0); + if (library.symbols.threadsafe_callbacks_finished() && values.length === count) { + break; + } + } + + expect(library.symbols.threadsafe_callbacks_finished()).toBe(1); + expect(values).toHaveLength(count); + expect(values).toEqual(Array.from({ length: count }, (_, i) => i)); + } finally { + callback.close(); + } + }); +}); + describe("given a source file with syntax errors", () => { const source = /* c */ ` int add(int a, int b) {