Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
31 changes: 30 additions & 1 deletion bench/ffi/bun.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CString, dlopen, ptr } from "bun:ffi";
import { cc, CString, dlopen, ptr } from "bun:ffi";
import { bench, group, run } from "../runner.mjs";

const { napiNoop, napiHash, napiString } = require(import.meta.dir + "/src/ffi_napi_bench.node");
Expand Down Expand Up @@ -35,6 +35,35 @@ group("bun:ffi", () => {
bench("string arg: TypedArray", () => ffi_strlen(strBuf));
});

const {
symbols: { cc_noop, cc_sink, cc_identity, cc_add, cc_sum10, cc_sum_of_squares, cc_is_null, cc_same },
} = cc({
source: import.meta.dir + "/cc.c",
symbols: {
cc_noop: { args: [], returns: "void" },
cc_sink: { args: ["i32"], returns: "void" },
cc_identity: { args: ["i32"], returns: "i32" },
cc_add: { args: ["i32", "i32"], returns: "i32" },
cc_sum10: { args: Array(10).fill("i32"), returns: "i32" },
cc_sum_of_squares: { args: ["f64", "f64"], returns: "f64" },
cc_is_null: { args: ["ptr"], returns: "bool" },
cc_same: { args: ["ptr", "ptr"], returns: "bool" },
},
});

group("bun:ffi cc()", () => {
bench("noop", () => cc_noop());
bench("sink(i32)", () => cc_sink(1));
bench("identity(i32)", () => cc_identity(1));
bench("add(i32, i32)", () => cc_add(1, 2));
bench("sum10(i32 x 10)", () => cc_sum10(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
bench("sum_of_squares(f64, f64)", () => cc_sum_of_squares(1.5, 2.5));
bench("is_null(ptr): raw pointer", () => cc_is_null(strPtr));
bench("is_null(ptr): TypedArray", () => cc_is_null(strBuf));
bench("same(ptr, ptr)", () => cc_same(strPtr, strBuf));
bench("add(i32, i32), one argument missing", () => cc_add(1));
});

if (process.env.SHOW_NAPI)
group("bun:napi", () => {
bench("noop", () => napiNoop());
Expand Down
10 changes: 10 additions & 0 deletions bench/ffi/cc.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
void cc_noop(void) {}
void cc_sink(int a) {}
int cc_identity(int a) { return a; }
int cc_add(int a, int b) { return a + b; }
int cc_sum10(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j) {
return a + b + c + d + e + f + g + h + i + j;
}
double cc_sum_of_squares(double a, double b) { return a * a + b * b; }
_Bool cc_is_null(const void* a) { return a == 0; }
_Bool cc_same(const void* a, const void* b) { return a == b; }
7 changes: 3 additions & 4 deletions src/jsc/bindings/JSFFIFunction.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,15 @@ using CFFIFunction = FFIFunction;
* Call a C function with low overhead, modeled after JSC::JSNativeStdFunction
*
* The C function is expected to know how to get the arguments out of the JSC::CallFrame and
* return a JSC::EncodedJSValue. To do that, the argumentOffset is inlined at compile-time
* into Bun's binary and again inlined into the C function.
* return a JSC::EncodedJSValue. The slot indices it needs are defined into it when TinyCC compiles it.
*
* This is used by functions compiled with TinyCC
*
* It was about 20% faster than using the JavaScriptCore C API for functions with 1 argument
*
* There is no wrapper function. It does zero bounds checking on the arguments.
* There is no wrapper function. This class does not check the arguments: the C function does.
* It does not check for exceptions. It does not check for return value.
* It is the caller's responsibility to not buffer overflow the arguments
* A function cc() compiles pads a call that passed too few arguments (LOAD_ARGUMENTS_FROM_CALL_FRAME in src/runtime/ffi/FFI.h).
* For all those reasons, this shouldn't be used directly.
*/
class JSFFIFunction final : public JSC::JSFunction {
Expand Down
7 changes: 7 additions & 0 deletions src/jsc/bindings/ffi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@ typedef struct FFIFields {
uint32_t JSArrayBufferView__offsetOfByteOffset;
uint32_t JSArrayBufferView__offsetOfVector;
uint32_t JSCell__offsetOfType;
// Register (8-byte slot) indices into a CallFrame, not byte offsets.
uint32_t CallFrame__argumentCountIncludingThisSlot;
uint32_t CallFrame__firstArgumentSlot;
} FFIFields;
extern "C" FFIFields Bun__FFI__offsets = { 0 };

static_assert(sizeof(JSC::Register) == sizeof(void*), "the cc() wrapper indexes the CallFrame as an array of pointer-sized slots");

extern "C" void Bun__FFI__ensureOffsetsAreLoaded()
{
Bun__FFI__offsets.JSArrayBufferView__offsetOfLength = JSC::JSArrayBufferView::offsetOfLength();
Bun__FFI__offsets.JSArrayBufferView__offsetOfByteOffset = JSC::JSArrayBufferView::offsetOfByteOffset();
Bun__FFI__offsets.JSArrayBufferView__offsetOfVector = JSC::JSArrayBufferView::offsetOfVector();
Bun__FFI__offsets.JSCell__offsetOfType = JSC::JSCell::typeInfoTypeOffset();
Bun__FFI__offsets.CallFrame__argumentCountIncludingThisSlot = static_cast<uint32_t>(JSC::CallFrameSlot::argumentCountIncludingThis);
Bun__FFI__offsets.CallFrame__firstArgumentSlot = static_cast<uint32_t>(JSC::CallFrameSlot::firstArgument);
}
2 changes: 0 additions & 2 deletions src/jsc/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,6 @@ pub mod marked_argument_buffer;
pub mod regular_expression;
#[path = "ScriptExecutionStatus.rs"]
pub mod script_execution_status;
#[path = "sizes.rs"]
pub mod sizes;
#[path = "SourceProvider.rs"]
pub mod source_provider;
#[path = "URLSearchParams.rs"]
Expand Down
8 changes: 0 additions & 8 deletions src/jsc/sizes.rs

This file was deleted.

27 changes: 19 additions & 8 deletions src/runtime/ffi/FFI.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,23 @@ EncodedJSValue ValueTrue = { TagValueTrue };

typedef void* JSContext;

// Bun_FFI_PointerOffsetToArgumentsList is injected into the build
// The value is BUN_FFI_POINTER_OFFSET_TO_ARGUMENTS_LIST in src/jsc/sizes.rs
// The value is 6.
// On ARM64_32, the value is something else but it really doesn't matter for our case
// However, I don't want this to subtly break amidst future upgrades to JavaScriptCore
#define LOAD_ARGUMENTS_FROM_CALL_FRAME \
int64_t *argsPtr = (int64_t*)((size_t*)callFrame + Bun_FFI_PointerOffsetToArgumentsList)
// A call frame holds only the arguments the call passed. A call with fewer than `count` continues on a copy of its argument slots, padded with undefined.
static inline void* Bun_FFI_padArguments(void* callFrame, EncodedJSValue* paddedFrame, int32_t count) {
EncodedJSValue *passed = (EncodedJSValue*)callFrame + Bun_FFI_PointerOffsetToArgumentsList;
EncodedJSValue *padded = paddedFrame + Bun_FFI_PointerOffsetToArgumentsList;
int32_t passedCount = ((EncodedJSValue*)callFrame)[Bun_FFI_PointerOffsetToArgumentCountIncludingThis].asBits.payload - 1;
int32_t i = 0;
for (; i < passedCount; i++) padded[i].asInt64 = passed[i].asInt64;
for (; i < count; i++) padded[i].asInt64 = TagValueUndefined;
Comment thread
robobun marked this conversation as resolved.
return paddedFrame;
}

#define LOAD_ARGUMENTS_FROM_CALL_FRAME(count) \
EncodedJSValue Bun_FFI_paddedFrame[Bun_FFI_PointerOffsetToArgumentsList + count]; \
Comment thread
robobun marked this conversation as resolved.
if (((EncodedJSValue*)callFrame)[Bun_FFI_PointerOffsetToArgumentCountIncludingThis].asBits.payload <= count) \
callFrame = Bun_FFI_padArguments(callFrame, Bun_FFI_paddedFrame, count)

#define ARGUMENT_FROM_CALL_FRAME(i) (((EncodedJSValue*)callFrame)[Bun_FFI_PointerOffsetToArgumentsList + (i)])



Expand Down Expand Up @@ -208,7 +218,8 @@ static uint64_t JSVALUE_TO_TYPED_ARRAY_LENGTH(EncodedJSValue val) {
// This behavior change enables the JIT to handle it better
// It also is better readability when console.log(myPtr)
static void* JSVALUE_TO_PTR(EncodedJSValue val) {
if (val.asInt64 == TagValueNull)
// null or undefined, which is what an argument the call did not pass is
if ((val.asInt64 & ~UndefinedTag) == TagValueNull)
return 0;

if (JSCELL_IS_TYPED_ARRAY(val)) {
Expand Down
71 changes: 26 additions & 45 deletions src/runtime/ffi/ffi_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,15 @@ fn dangerously_run_without_jit_protections<R>(func: impl FnOnce() -> R) -> R {
func()
}

/// Mirrors `FFIFields` in `src/jsc/bindings/ffi.cpp`.
#[repr(C)]
struct Offsets {
js_array_buffer_view_offset_of_length: u32,
js_array_buffer_view_offset_of_byte_offset: u32,
js_array_buffer_view_offset_of_vector: u32,
js_cell_offset_of_type: u32,
call_frame_argument_count_including_this_slot: u32,
call_frame_first_argument_slot: u32,
}

unsafe extern "C" {
Expand Down Expand Up @@ -2101,68 +2104,42 @@ impl Function {
)?;
}

if !self.arg_types.is_empty() {
writer.write_all(b" LOAD_ARGUMENTS_FROM_CALL_FRAME;\n")?;
for (i, arg) in self.arg_types.iter().enumerate() {
if *arg == ABIType::NapiEnv {
write!(
writer,
" napi_env arg{} = (napi_env)&Bun__thisFFIModuleNapiEnv;\n argsPtr++;\n",
i
)?;
} else if *arg == ABIType::NapiValue {
writeln!(
writer,
" EncodedJSValue arg{} = {{ .asInt64 = *argsPtr++ }};",
i
)?;
} else if arg.needs_a_cast_in_c() {
if i < self.arg_types.len() - 1 {
writeln!(
writer,
" EncodedJSValue arg{} = {{ .asInt64 = *argsPtr++ }};",
i
)?;
} else {
write!(
writer,
" EncodedJSValue arg{};\n arg{}.asInt64 = *argsPtr;\n",
i, i
)?;
}
} else {
if i < self.arg_types.len() - 1 {
writeln!(writer, " int64_t arg{} = *argsPtr++;", i)?;
} else {
writeln!(writer, " int64_t arg{} = *argsPtr;", i)?;
}
}
}
// The wrapper fills in a napi_env parameter itself: its slot in the frame is a placeholder that is never read.
let slots_read = self
.arg_types
.iter()
.rposition(|arg| *arg != ABIType::NapiEnv)
.map_or(0, |last| last + 1);
if slots_read > 0 {
writeln!(writer, " LOAD_ARGUMENTS_FROM_CALL_FRAME({slots_read});")?;
}

let mut arg_buf = [0u8; 512];

writer.write_all(b" ")?;
if self.return_type != ABIType::Void {
self.return_type.typename(writer)?;
writer.write_all(b" return_value = ")?;
}
write!(writer, "{}(", BStr::new(self.base_name.as_bytes()))?;
first = true;
arg_buf[0..3].copy_from_slice(b"arg");
// Each argument is read from the frame where the call uses it: a local per argument costs a store and a load.
const SLOT: &[u8] = b"ARGUMENT_FROM_CALL_FRAME(";
let mut slot_buf = [0u8; 64];
slot_buf[..SLOT.len()].copy_from_slice(SLOT);
for (i, arg) in self.arg_types.iter().enumerate() {
if !first {
writer.write_all(b", ")?;
}
first = false;
writer.write_all(b" ")?;

let length_buf = bun_core::fmt::print_int(&mut arg_buf[3..], i);
let arg_name = &arg_buf[0..3 + length_buf];
let digits = bun_core::fmt::print_int(&mut slot_buf[SLOT.len()..], i);
slot_buf[SLOT.len() + digits] = b')';
let slot = &slot_buf[..SLOT.len() + digits + 1];
if arg.needs_a_cast_in_c() {
write!(writer, "{}", arg.to_c(arg_name))?;
write!(writer, "{}", arg.to_c(slot))?;
} else {
writer.write_all(arg_name)?;
writer.write_all(slot)?;
writer.write_all(b".asInt64")?;
Comment thread
robobun marked this conversation as resolved.
}
}
writer.write_all(b");\n")?;
Expand Down Expand Up @@ -2488,7 +2465,11 @@ impl CompilerRT {
state.define_symbols(&[
(
"Bun_FFI_PointerOffsetToArgumentsList",
bun_jsc::sizes::BUN_FFI_POINTER_OFFSET_TO_ARGUMENTS_LIST as i64,
offsets.call_frame_first_argument_slot as i64,
),
(
"Bun_FFI_PointerOffsetToArgumentCountIncludingThis",
offsets.call_frame_argument_count_including_this_slot as i64,
),
(
"JSArrayBufferView__offsetOfLength",
Expand Down
Loading
Loading