Skip to content
155 changes: 22 additions & 133 deletions src/jsc/bindings/JSFFIFunction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,24 +184,25 @@ JSFFIFunction* JSFFIFunction::createForFFI(VM& vm, Zig::GlobalObject* globalObje

} // namespace JSC

// Shared tail for the FFI_Callback_* entry points: call back into JS and leave any exception
// pending on the VM, like any other host function. Never clear and re-throw here: re-installing
// the TerminationException once the termination request is retired trips VM::setException.
static JSC::EncodedJSValue invokeFFICallback(Zig::GlobalObject* globalObject, JSC::JSFunction* function, JSC::MarkedArgumentBuffer& arguments)
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
auto result = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, function, JSC::getCallData(function), JSC::jsUndefined(), arguments);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsNull()));
return JSC::JSValue::encode(result);
}

extern "C" JSC::EncodedJSValue
FFI_Callback_call(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args)
{
auto* function = wrapper.m_function.get();
auto* globalObject = wrapper.globalObject.get();
auto& vm = JSC::getVM(globalObject);
JSC::MarkedArgumentBuffer arguments;
for (size_t i = 0; i < argCount; ++i)
arguments.appendWithCrashOnOverflow(JSC::JSValue::decode(args[i]));
WTF::NakedPtr<JSC::Exception> exception;
auto result = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, function, JSC::getCallData(function), JSC::jsUndefined(), arguments, exception);
if (exception) [[unlikely]] {
auto scope = DECLARE_THROW_SCOPE(vm);
scope.throwException(globalObject, exception);
return JSC::JSValue::encode(JSC::jsNull());
}

return JSC::JSValue::encode(result);
return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments);
}

extern "C" void
Expand All @@ -214,186 +215,83 @@ FFI_Callback_threadsafe_call(FFICallbackFunctionWrapper& wrapper, size_t argCoun

WebCore::ScriptExecutionContext::postTaskTo(wrapper.m_contextId, [argsVec = WTF::move(argsVec), protectedWrapper = Ref { wrapper }](WebCore::ScriptExecutionContext& ctx) mutable {
Comment thread
robobun marked this conversation as resolved.
auto* globalObject = uncheckedDowncast<Zig::GlobalObject>(ctx.jsGlobalObject());
auto& vm = JSC::getVM(globalObject);
JSC::MarkedArgumentBuffer arguments;
auto* function = protectedWrapper->m_function.get();
for (size_t i = 0; i < argsVec.size(); ++i)
arguments.appendWithCrashOnOverflow(JSC::JSValue::decode(argsVec[i]));
WTF::NakedPtr<JSC::Exception> exception;
JSC::profiledCall(globalObject, JSC::ProfilingReason::API, function, JSC::getCallData(function), JSC::jsUndefined(), arguments, exception);
if (exception) [[unlikely]] {
auto scope = DECLARE_THROW_SCOPE(vm);
scope.throwException(globalObject, exception);
return;
}
invokeFFICallback(globalObject, protectedWrapper->m_function.get(), arguments);
});
Comment thread
robobun marked this conversation as resolved.
}

extern "C" JSC::EncodedJSValue
FFI_Callback_call_0(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args)
{
auto* function = wrapper.m_function.get();
auto* globalObject = wrapper.globalObject.get();
auto& vm = JSC::getVM(globalObject);

JSC::MarkedArgumentBuffer arguments;

WTF::NakedPtr<JSC::Exception> exception;
auto result = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, function, JSC::getCallData(function), JSC::jsUndefined(), arguments, exception);
if (exception) [[unlikely]] {
auto scope = DECLARE_THROW_SCOPE(vm);
scope.throwException(globalObject, exception);
return JSC::JSValue::encode(JSC::jsNull());
}

return JSC::JSValue::encode(result);
return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments);
}

extern "C" JSC::EncodedJSValue
FFI_Callback_call_1(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args)
{
auto* function = wrapper.m_function.get();
auto* globalObject = wrapper.globalObject.get();
auto& vm = JSC::getVM(globalObject);

JSC::MarkedArgumentBuffer arguments;
arguments.append(JSC::JSValue::decode(args[0]));

WTF::NakedPtr<JSC::Exception> exception;
auto result = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, function, JSC::getCallData(function), JSC::jsUndefined(), arguments, exception);
if (exception) [[unlikely]] {
auto scope = DECLARE_THROW_SCOPE(vm);
scope.throwException(globalObject, exception);
return JSC::JSValue::encode(JSC::jsNull());
}

return JSC::JSValue::encode(result);
return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments);
}

extern "C" JSC::EncodedJSValue
FFI_Callback_call_2(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args)
{
auto* function = wrapper.m_function.get();
auto* globalObject = wrapper.globalObject.get();
auto& vm = JSC::getVM(globalObject);

JSC::MarkedArgumentBuffer arguments;
arguments.append(JSC::JSValue::decode(args[0]));
arguments.append(JSC::JSValue::decode(args[1]));

WTF::NakedPtr<JSC::Exception> exception;
auto result = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, function, JSC::getCallData(function), JSC::jsUndefined(), arguments, exception);
if (exception) [[unlikely]] {
auto scope = DECLARE_THROW_SCOPE(vm);
scope.throwException(globalObject, exception);
return JSC::JSValue::encode(JSC::jsNull());
}

return JSC::JSValue::encode(result);
return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments);
}

extern "C" JSC::EncodedJSValue FFI_Callback_call_3(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args)
{
auto* function = wrapper.m_function.get();
auto* globalObject = wrapper.globalObject.get();
auto& vm = JSC::getVM(globalObject);

JSC::MarkedArgumentBuffer arguments;
arguments.append(JSC::JSValue::decode(args[0]));
arguments.append(JSC::JSValue::decode(args[1]));
arguments.append(JSC::JSValue::decode(args[2]));

WTF::NakedPtr<JSC::Exception> exception;
auto result = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, function, JSC::getCallData(function), JSC::jsUndefined(), arguments, exception);
if (exception) [[unlikely]] {
auto scope = DECLARE_THROW_SCOPE(vm);
scope.throwException(globalObject, exception);
return JSC::JSValue::encode(JSC::jsNull());
}

return JSC::JSValue::encode(result);
return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments);
}

extern "C" JSC::EncodedJSValue FFI_Callback_call_4(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args)
{
auto* function = wrapper.m_function.get();
auto* globalObject = wrapper.globalObject.get();
auto& vm = JSC::getVM(globalObject);

JSC::MarkedArgumentBuffer arguments;
arguments.append(JSC::JSValue::decode(args[0]));
arguments.append(JSC::JSValue::decode(args[1]));
arguments.append(JSC::JSValue::decode(args[2]));
arguments.append(JSC::JSValue::decode(args[3]));

WTF::NakedPtr<JSC::Exception> exception;
auto result = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, function, JSC::getCallData(function), JSC::jsUndefined(), arguments, exception);
if (exception) [[unlikely]] {
auto scope = DECLARE_THROW_SCOPE(vm);
scope.throwException(globalObject, exception);
return JSC::JSValue::encode(JSC::jsNull());
}

return JSC::JSValue::encode(result);
return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments);
}

extern "C" JSC::EncodedJSValue FFI_Callback_call_5(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args)
{
auto* function = wrapper.m_function.get();
auto* globalObject = wrapper.globalObject.get();
auto& vm = JSC::getVM(globalObject);

JSC::MarkedArgumentBuffer arguments;
arguments.append(JSC::JSValue::decode(args[0]));
arguments.append(JSC::JSValue::decode(args[1]));
arguments.append(JSC::JSValue::decode(args[2]));
arguments.append(JSC::JSValue::decode(args[3]));
arguments.append(JSC::JSValue::decode(args[4]));

WTF::NakedPtr<JSC::Exception> exception;
auto result = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, function, JSC::getCallData(function), JSC::jsUndefined(), arguments, exception);
if (exception) [[unlikely]] {
auto scope = DECLARE_THROW_SCOPE(vm);
scope.throwException(globalObject, exception);
return JSC::JSValue::encode(JSC::jsNull());
}

return JSC::JSValue::encode(result);
return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments);
}

extern "C" JSC::EncodedJSValue
FFI_Callback_call_6(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args)
{
auto* function = wrapper.m_function.get();
auto* globalObject = wrapper.globalObject.get();
auto& vm = JSC::getVM(globalObject);

JSC::MarkedArgumentBuffer arguments;
arguments.append(JSC::JSValue::decode(args[0]));
arguments.append(JSC::JSValue::decode(args[1]));
arguments.append(JSC::JSValue::decode(args[2]));
arguments.append(JSC::JSValue::decode(args[3]));
arguments.append(JSC::JSValue::decode(args[4]));
arguments.append(JSC::JSValue::decode(args[5]));

WTF::NakedPtr<JSC::Exception> exception;
auto result = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, function, JSC::getCallData(function), JSC::jsUndefined(), arguments, exception);
if (exception) [[unlikely]] {
auto scope = DECLARE_THROW_SCOPE(vm);
scope.throwException(globalObject, exception);
return JSC::JSValue::encode(JSC::jsNull());
}

return JSC::JSValue::encode(result);
return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments);
}

extern "C" JSC::EncodedJSValue
FFI_Callback_call_7(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::EncodedJSValue* args)
{
auto* function = wrapper.m_function.get();
auto* globalObject = wrapper.globalObject.get();
auto& vm = JSC::getVM(globalObject);

JSC::MarkedArgumentBuffer arguments;
arguments.append(JSC::JSValue::decode(args[0]));
arguments.append(JSC::JSValue::decode(args[1]));
Expand All @@ -402,14 +300,5 @@ FFI_Callback_call_7(FFICallbackFunctionWrapper& wrapper, size_t argCount, JSC::E
arguments.append(JSC::JSValue::decode(args[4]));
arguments.append(JSC::JSValue::decode(args[5]));
arguments.append(JSC::JSValue::decode(args[6]));

WTF::NakedPtr<JSC::Exception> exception;
auto result = JSC::profiledCall(globalObject, JSC::ProfilingReason::API, function, JSC::getCallData(function), JSC::jsUndefined(), arguments, exception);
if (exception) [[unlikely]] {
auto scope = DECLARE_THROW_SCOPE(vm);
scope.throwException(globalObject, exception);
return JSC::JSValue::encode(JSC::jsNull());
}

return JSC::JSValue::encode(result);
return invokeFFICallback(wrapper.globalObject.get(), wrapper.m_function.get(), arguments);
}
117 changes: 116 additions & 1 deletion test/js/bun/ffi/ffi.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { afterAll, describe, expect, it } from "bun:test";
import { existsSync } from "fs";
import { isGlibcVersionAtLeast } from "harness";
import { bunEnv, bunExe, isArm64, isGlibcVersionAtLeast, isWindows, tempDir } from "harness";
import { platform } from "os";

import {
Expand Down Expand Up @@ -677,6 +677,121 @@ it(".ptr is not leaked", () => {
}
});

// TinyCC, which implements JSCallback and CFunction, is unavailable on Windows ARM64.
const isFFIUnavailable = isWindows && isArm64;

// Runs in a subprocess: `bun test`'s exit path does not finalize the CFunction's native handle,
// which the ASan lane's leak checker then reports against this file.
it.skipIf(isFFIUnavailable)("JSCallback exceptions propagate out of the native call", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`import { CFunction, JSCallback } from "bun:ffi";
const callback = new JSCallback(
() => {
throw new Error("boom");
},
{ returns: "int32_t", args: [] },
);
const call = new CFunction({ ptr: callback.ptr, returns: "int32_t", args: [] });
try {
call();
console.log("did not throw");
} catch (e) {
console.log("caught", e.message);
}
call.close();
callback.close();`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout, stderr, exitCode }).toEqual({
stdout: "caught boom\n",
stderr: "",
exitCode: 0,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});

// worker.terminate() delivered inside a threadsafe JSCallback used to trip
// "ASSERTION FAILED: !isTerminationException(exception) || hasTerminationRequest()"
// in JSC::VM::setException on the worker thread and re-enter the terminated VM.
it.skipIf(isFFIUnavailable)("JSCallback tolerates worker.terminate() arriving inside the callback", async () => {
using dir = tempDir("ffi-jscallback-terminate", {
"main.js": `
import { join } from "node:path";
import { Worker } from "node:worker_threads";

const sab = new SharedArrayBuffer(4);
const flag = new Int32Array(sab);

const worker = new Worker(join(import.meta.dir, "worker.js"), { workerData: sab });
let terminating = false;
worker.on("error", err => {
console.error("worker error:", err);
process.exit(1);
});
worker.on("exit", code => {
if (!terminating) {
console.error("worker exited early:", code);
process.exit(1);
}
});

// Wait until the worker thread is inside the native -> JS callback frame.
await Atomics.waitAsync(flag, 0, 0).value;

terminating = true;
await worker.terminate();
console.log("done");
`,
"worker.js": `
import { CFunction, JSCallback } from "bun:ffi";
import { workerData } from "node:worker_threads";

const flag = new Int32Array(workerData);

const callback = new JSCallback(
() => {
// Tell the parent we are inside the native -> JS callback frame, then
// spin until worker.terminate() delivers the TerminationException.
Atomics.store(flag, 0, 1);
Atomics.notify(flag, 0);
while (true) {}
},
{ returns: "void", args: [], threadsafe: true },
);

// CFunction makes the callback's native function pointer callable from JS. A threadsafe
// JSCallback enqueues a task instead of running synchronously, so the callback runs at
// the top of the worker's event loop once this module finishes evaluating.
const fire = new CFunction({ ptr: callback.ptr, returns: "void", args: [] });
fire();

// Keep the worker alive until the queued callback task runs.
setInterval(() => {}, 1000);
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "main.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout, stderr, exitCode, signalCode: proc.signalCode }).toEqual({
stdout: "done\n",
stderr: "",
exitCode: 0,
signalCode: null,
});
});

const libPath =
platform() === "darwin"
? "/usr/lib/libSystem.B.dylib"
Expand Down
Loading