Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/js/bun/ffi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ delete ffi.closeCallback;

class JSCallback {
constructor(cb, options) {
const { ctx, ptr } = nativeCallback(options, cb);
this.#ctx = ctx;
this.ptr = ptr;
const result = nativeCallback(options, cb);
if (Error.isError(result)) throw result;
this.#ctx = result.ctx;
this.ptr = result.ptr;
this.#threadsafe = !!options?.threadsafe;
}

Expand Down
61 changes: 55 additions & 6 deletions src/jsc/bindings/JSFFIFunction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "root.h"
#include "JSFFIFunction.h"

#include <JavaScriptCore/JSBigInt.h>
#include <JavaScriptCore/JSCJSValueInlines.h>
#include <JavaScriptCore/VM.h>
#include "ZigGlobalObject.h"
Expand Down Expand Up @@ -205,24 +206,72 @@ FFI_Callback_call(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::Enc
return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments);
}

// Must match ABIType in src/runtime/ffi/abi_type.rs.
enum class FFIABIType : uint8_t {
int64_t_ = 7,
uint64_t_ = 8,
i64_fast = 15,
u64_fast = 16,
};

// For threadsafe callbacks, 64-bit integer arguments are passed through as
// raw bits because the TCC-generated trampoline may run on an arbitrary OS
// thread and must not heap-allocate a JSBigInt there. Convert them here,
// which always runs on the JS thread.
static inline JSC::JSValue decodeThreadsafeCallbackArgument(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue raw, uint8_t abiType)
{
switch (static_cast<FFIABIType>(abiType)) {
case FFIABIType::int64_t_:
return JSC::JSBigInt::createFrom(globalObject, static_cast<int64_t>(raw));
case FFIABIType::uint64_t_:
return JSC::JSBigInt::createFrom(globalObject, static_cast<uint64_t>(raw));
case FFIABIType::i64_fast: {
int64_t val = static_cast<int64_t>(raw);
if (val >= JSC::minSafeInteger() && val <= JSC::maxSafeInteger())
return JSC::jsNumber(val);
return JSC::JSBigInt::createFrom(globalObject, val);
Comment thread
robobun marked this conversation as resolved.
}
case FFIABIType::u64_fast: {
uint64_t val = static_cast<uint64_t>(raw);
if (val <= static_cast<uint64_t>(JSC::maxSafeInteger()))
return JSC::jsNumber(static_cast<double>(val));
return JSC::JSBigInt::createFrom(globalObject, val);
}
Comment thread
robobun marked this conversation as resolved.
default:
return JSC::JSValue::decode(raw);
}
}

extern "C" void
FFI_Callback_threadsafe_call(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args)
FFI_Callback_threadsafe_call(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args, const uint8_t* argTypes)
{
// Runs on a foreign thread: do not touch the wrapper's JSC::Strong members here.
WTF::Vector<JSC::EncodedJSValue, 8> argsVec;
for (size_t i = 0; i < argCount; ++i)
WTF::Vector<uint8_t, 8> argTypesVec;
for (size_t i = 0; i < argCount; ++i) {
argsVec.append(args[i]);
argTypesVec.append(argTypes[i]);
}

// Ref only once the context is found live (inside the map lock) and release via
// adoptRef in the task, so the last deref — destroying two JSC::Strong members —
// can only happen on the JS thread. On a dead/terminating context nothing is destroyed here.
WebCore::ScriptExecutionContext::postTaskTo(wrapper.m_contextId, [&wrapper] { wrapper.ref(); }, [argsVec = WTF::move(argsVec), wrapper = &wrapper](WebCore::ScriptExecutionContext& ctx) mutable {
WebCore::ScriptExecutionContext::postTaskTo(wrapper.m_contextId, [&wrapper] { wrapper.ref(); }, [argsVec = WTF::move(argsVec), argTypesVec = WTF::move(argTypesVec), wrapper = &wrapper](WebCore::ScriptExecutionContext& ctx) mutable {
auto protectedWrapper = adoptRef(*wrapper);
auto* globalObject = uncheckedDowncast<Zig::GlobalObject>(ctx.jsGlobalObject());
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSC::MarkedArgumentBuffer arguments;
for (size_t i = 0; i < argsVec.size(); ++i)
arguments.appendWithCrashOnOverflow(JSC::JSValue::decode(argsVec[i]));
invokeFFICallback(globalObject, protectedWrapper->m_function.get(), arguments); });
for (size_t i = 0; i < argsVec.size(); ++i) {
// JSBigInt::createFrom can throw (e.g. OOM). Leave the exception
// pending on the VM, matching invokeFFICallback.
JSC::JSValue arg = decodeThreadsafeCallbackArgument(globalObject, argsVec[i], argTypesVec[i]);
RETURN_IF_EXCEPTION(scope, void());
arguments.appendWithCrashOnOverflow(arg);
}
scope.release();
invokeFFICallback(globalObject, protectedWrapper->m_function.get(), arguments);
});
}

extern "C" JSC::EncodedJSValue
Expand Down
14 changes: 12 additions & 2 deletions src/runtime/ffi/FFI.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,15 @@ typedef void* JSContext;

#ifdef IS_CALLBACK
void* callback_ctx;
#ifdef IS_THREADSAFE
// Threadsafe callbacks may be invoked from an arbitrary OS thread. Converting
// 64-bit integer arguments to JSValue may need to heap-allocate a JSBigInt,
// which must only happen on the JS thread. The trampoline therefore passes
// the raw 64-bit bits for such arguments along with a per-argument ABIType
// tag so FFI_Callback_threadsafe_call can perform the conversion inside the
// task it posts to the JS thread.
BUN_FFI_IMPORT void FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args, const uint8_t* argTypes);
#else
Comment thread
claude[bot] marked this conversation as resolved.
BUN_FFI_IMPORT ZIG_REPR_TYPE FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args);
// We wrap
static EncodedJSValue _FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args) __attribute__((__always_inline__));
Expand All @@ -155,6 +164,7 @@ static EncodedJSValue _FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TY
return return_value;
}
#endif
#endif

static bool JSVALUE_IS_CELL(EncodedJSValue val) __attribute__((__always_inline__));
static bool JSVALUE_IS_INT32(EncodedJSValue val) __attribute__((__always_inline__));
Expand Down Expand Up @@ -353,15 +363,15 @@ static EncodedJSValue UINT64_TO_JSVALUE(void* jsGlobalObject, uint64_t val) {
return INT32_TO_JSVALUE((int32_t)val);
}

if (val < MAX_INT52) {
if (val <= MAX_INT52) {
return DOUBLE_TO_JSVALUE((double)val);
}

return UINT64_TO_JSVALUE_SLOW(jsGlobalObject, val);
}

static EncodedJSValue INT64_TO_JSVALUE(void* jsGlobalObject, int64_t val) {
if (val >= -MAX_INT32 && val <= MAX_INT32) {
if (val >= -MAX_INT32 && val < MAX_INT32) {
return INT32_TO_JSVALUE((int32_t)val);
}

Expand Down
11 changes: 11 additions & 0 deletions src/runtime/ffi/abi_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,17 @@ impl ABIType {
matches!(self, ABIType::Double | ABIType::Float)
}

/// Returns true if converting this type to a JSValue may heap-allocate
/// a JS object (a JSBigInt). For threadsafe callbacks the trampoline
/// may run on an arbitrary OS thread, so the allocation must be
/// deferred to the JS thread inside `FFI_Callback_threadsafe_call`.
pub fn may_allocate_bigint_when_converted_to_js(self) -> bool {
matches!(
self,
ABIType::Int64T | ABIType::Uint64T | ABIType::I64Fast | ABIType::U64Fast
)
}

pub fn to_c(self, symbol: &[u8]) -> ToCFormatter<'_> {
ToCFormatter {
tag: self,
Expand Down
65 changes: 58 additions & 7 deletions src/runtime/ffi/ffi_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1814,7 +1814,7 @@ pub(super) fn generate_symbol_for_function(
));
}

if function.threadsafe && return_type != ABIType::Void {
if threadsafe && return_type != ABIType::Void {
return Ok(Some(
ZigString::static_(b"Threadsafe functions must return void").to_error_instance(global),
));
Expand Down Expand Up @@ -2388,6 +2388,9 @@ impl Function {
}

writer.write_all(b"#define IS_CALLBACK 1\n")?;
if self.threadsafe {
writer.write_all(b"#define IS_THREADSAFE 1\n")?;
}

'brk: {
if self.return_type.is_floating_point() {
Expand Down Expand Up @@ -2444,12 +2447,43 @@ impl Function {
for (i, arg) in self.arg_types.iter().enumerate() {
let printed = bun_core::fmt::print_int(&mut arg_buf[3..], i);
let arg_name = &arg_buf[0..3 + printed];
writeln!(
if self.threadsafe && arg.may_allocate_bigint_when_converted_to_js() {
// The trampoline for a threadsafe callback may run on an
// arbitrary OS thread. Converting a 64-bit integer here
// would call {U,}INT64_TO_JSVALUE_SLOW, which allocates a
// JSBigInt on the calling thread without the JS lock and
// corrupts the GC heap. Pass the raw bits through instead
// and let FFI_Callback_threadsafe_call convert them on the
// JS thread using the argTypes table emitted below.
writeln!(
writer,
"arguments[{}] = (ZIG_REPR_TYPE)(int64_t){};",
i,
BStr::new(arg_name)
)?;
} else {
writeln!(
writer,
"arguments[{}] = {}.asZigRepr;",
i,
arg.to_js(arg_name)
)?;
}
}

if self.threadsafe {
write!(
writer,
"arguments[{}] = {}.asZigRepr;",
i,
arg.to_js(arg_name)
"static const uint8_t argTypes[{}] = {{",
self.arg_types.len()
)?;
for (i, arg) in self.arg_types.iter().enumerate() {
if i > 0 {
writer.write_all(b", ")?;
}
write!(writer, "{}", *arg as i32)?;
}
writer.write_all(b"};\n")?;
}
}

Expand All @@ -2461,7 +2495,24 @@ impl Function {
let ptr = context_ptr.map(|p| p as usize).unwrap_or(0);
let fmt = bun_fmt::hex_int_upper::<16>(ptr as u64);

let written = if !self.arg_types.is_empty() {
let written = if self.threadsafe {
let mut cursor = std::io::Cursor::new(&mut inner_buf_[1..]);
if !self.arg_types.is_empty() {
write!(
&mut cursor,
"FFI_Callback_call((void*)0x{}ULL, {}, arguments, argTypes)",
fmt,
self.arg_types.len()
)?;
} else {
write!(
&mut cursor,
"FFI_Callback_call((void*)0x{}ULL, 0, (ZIG_REPR_TYPE*)0, (const uint8_t*)0)",
fmt
)?;
}
cursor.position() as usize
} else if !self.arg_types.is_empty() {
let mut cursor = std::io::Cursor::new(&mut inner_buf_[1..]);
write!(
&mut cursor,
Expand Down Expand Up @@ -2513,7 +2564,7 @@ unsafe extern "C" {
fn FFI_Callback_call_3(_: *mut c_void, _: usize, _: *mut JSValue) -> JSValue;
fn FFI_Callback_call_4(_: *mut c_void, _: usize, _: *mut JSValue) -> JSValue;
fn FFI_Callback_call_5(_: *mut c_void, _: usize, _: *mut JSValue) -> JSValue;
fn FFI_Callback_threadsafe_call(_: *mut c_void, _: usize, _: *mut JSValue) -> JSValue;
fn FFI_Callback_threadsafe_call(_: *mut c_void, _: usize, _: *mut JSValue, _: *const u8);
Comment thread
robobun marked this conversation as resolved.
fn FFI_Callback_call_6(_: *mut c_void, _: usize, _: *mut JSValue) -> JSValue;
fn FFI_Callback_call_7(_: *mut c_void, _: usize, _: *mut JSValue) -> JSValue;
fn Bun__createFFICallbackFunction(_: &JSGlobalObject, _: JSValue) -> *mut c_void;
Expand Down
61 changes: 55 additions & 6 deletions src/runtime/ffi/host_fns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ pub fn generate_symbol_for_function(
))));
}

if function.threadsafe && return_type != ABIType::Void {
if threadsafe && return_type != ABIType::Void {
return Ok(Some(global.create_error_instance(format_args!(
"Threadsafe functions must return void"
))));
Expand Down Expand Up @@ -369,6 +369,9 @@ impl Function {
}

writer.write_all(b"#define IS_CALLBACK 1\n")?;
if self.threadsafe {
writer.write_all(b"#define IS_THREADSAFE 1\n")?;
}

'brk: {
if self.return_type.is_floating_point() {
Expand Down Expand Up @@ -423,12 +426,43 @@ impl Function {
for (i, arg) in self.arg_types.iter().enumerate() {
let printed = bun_core::fmt::print_int(&mut arg_buf[3..], i);
let arg_name = &arg_buf[0..3 + printed];
writeln!(
if self.threadsafe && arg.may_allocate_bigint_when_converted_to_js() {
// The trampoline for a threadsafe callback may run on an
// arbitrary OS thread. Converting a 64-bit integer here
// would call {U,}INT64_TO_JSVALUE_SLOW, which allocates a
// JSBigInt on the calling thread without the JS lock and
// corrupts the GC heap. Pass the raw bits through instead
// and let FFI_Callback_threadsafe_call convert them on the
// JS thread using the argTypes table emitted below.
writeln!(
writer,
"arguments[{}] = (ZIG_REPR_TYPE)(int64_t){};",
i,
BStr::new(arg_name)
)?;
} else {
writeln!(
writer,
"arguments[{}] = {}.asZigRepr;",
i,
arg.to_js(arg_name)
)?;
}
}

if self.threadsafe {
write!(
writer,
"arguments[{}] = {}.asZigRepr;",
i,
arg.to_js(arg_name)
"static const uint8_t argTypes[{}] = {{",
self.arg_types.len()
)?;
for (i, arg) in self.arg_types.iter().enumerate() {
if i > 0 {
writer.write_all(b", ")?;
}
write!(writer, "{}", *arg as i32)?;
}
writer.write_all(b"};\n")?;
}
}

Expand All @@ -438,7 +472,22 @@ impl Function {
let written = {
let ptr = context_ptr.map(|p| p as usize).unwrap_or(0);
let mut cursor = std::io::Cursor::new(&mut inner_buf_[1..]);
if !self.arg_types.is_empty() {
if self.threadsafe {
if !self.arg_types.is_empty() {
write!(
&mut cursor,
"FFI_Callback_call((void*)0x{:X}ULL, {}, arguments, argTypes)",
ptr,
self.arg_types.len()
)?;
} else {
write!(
&mut cursor,
"FFI_Callback_call((void*)0x{:X}ULL, 0, (ZIG_REPR_TYPE*)0, (const uint8_t*)0)",
ptr
)?;
}
} else if !self.arg_types.is_empty() {
write!(
&mut cursor,
"FFI_Callback_call((void*)0x{:X}ULL, {}, arguments)",
Expand Down
Loading
Loading