From f263436911be96d8831c03150bac6e336d92f7dc Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 19 Sep 2024 21:26:50 -0700 Subject: [PATCH] Experiment: Add `buffer` type and inline pointer (#14036) --- docs/api/ffi.md | 3 ++ packages/bun-types/ffi.d.ts | 4 ++ src/bun.js/api/FFI.h | 43 +++++++++++++++--- src/bun.js/api/ffi.zig | 77 ++++++++++++++++++++++++++++---- src/bun.js/bindings/bindings.zig | 3 ++ src/bun.js/bindings/ffi.cpp | 17 +++++++ src/js/bun/ffi.ts | 24 +++++++++- test/js/bun/ffi/cc-fixture.c | 4 ++ test/js/bun/ffi/cc-fixture.js | 13 +++++- 9 files changed, 171 insertions(+), 17 deletions(-) create mode 100644 src/bun.js/bindings/ffi.cpp diff --git a/docs/api/ffi.md b/docs/api/ffi.md index d3a7fc3e58b80..6284689cb271b 100644 --- a/docs/api/ffi.md +++ b/docs/api/ffi.md @@ -110,6 +110,7 @@ The following `FFIType` values are supported. | `FFIType` | C Type | Aliases | | ---------- | -------------- | --------------------------- | +| buffer | `char*` | | | cstring | `char*` | | | function | `(void*)(*)()` | `fn`, `callback` | | ptr | `void*` | `pointer`, `void*`, `char*` | @@ -130,6 +131,8 @@ The following `FFIType` values are supported. | napi_env | `napi_env` | | | napi_value | `napi_value` | | +Note: `buffer` arguments must be a `TypedArray` or `DataView`. + ## Strings JavaScript strings and C-like strings are different, and that complicates using strings with native libraries. diff --git a/packages/bun-types/ffi.d.ts b/packages/bun-types/ffi.d.ts index 343548edbe12a..2bf3bb6882bff 100644 --- a/packages/bun-types/ffi.d.ts +++ b/packages/bun-types/ffi.d.ts @@ -340,6 +340,7 @@ declare module "bun:ffi" { napi_env = 18, napi_value = 19, + buffer = 20, } type Pointer = number & { __pointer__: null }; @@ -377,6 +378,7 @@ declare module "bun:ffi" { [FFIType.function]: Pointer | JSCallback; // cannot be null [FFIType.napi_env]: unknown; [FFIType.napi_value]: unknown; + [FFIType.buffer]: NodeJS.TypedArray | DataView; } interface FFITypeToReturnsType { [FFIType.char]: number; @@ -411,6 +413,7 @@ declare module "bun:ffi" { [FFIType.function]: Pointer | null; [FFIType.napi_env]: unknown; [FFIType.napi_value]: unknown; + [FFIType.buffer]: NodeJS.TypedArray | DataView; } interface FFITypeStringToType { ["char"]: FFIType.char; @@ -445,6 +448,7 @@ declare module "bun:ffi" { ["callback"]: FFIType.pointer; // for now ["napi_env"]: never; ["napi_value"]: unknown; + ["buffer"]: FFIType.buffer; } type FFITypeOrString = FFIType | keyof FFITypeStringToType; diff --git a/src/bun.js/api/FFI.h b/src/bun.js/api/FFI.h index 6feeaa68bdca5..6616e5de4727e 100644 --- a/src/bun.js/api/FFI.h +++ b/src/bun.js/api/FFI.h @@ -35,7 +35,7 @@ typedef _Bool bool; #ifndef SRC_JS_NATIVE_API_TYPES_H_ typedef struct napi_env__ *napi_env; -typedef struct napi_value__ *napi_value; +typedef int64_t napi_value; typedef enum { napi_ok, napi_invalid_arg, @@ -74,14 +74,14 @@ void NapiHandleScope__pop(void* jsGlobalObject, void* handleScope); // begin with a 15-bit pattern within the range 0x0002..0xFFFC. #define DoubleEncodeOffsetBit 49 #define DoubleEncodeOffset (1ll << DoubleEncodeOffsetBit) -#define OtherTag 0x2 -#define BoolTag 0x4 -#define UndefinedTag 0x8 +#define OtherTag 0x2ll +#define BoolTag 0x4ll +#define UndefinedTag 0x8ll #define TagValueFalse (OtherTag | BoolTag | false) #define TagValueTrue (OtherTag | BoolTag | true) #define TagValueUndefined (OtherTag | UndefinedTag) #define TagValueNull (OtherTag) -#define NotCellMask NumberTag | OtherTag +#define NotCellMask (int64_t)(NumberTag | OtherTag) #define MAX_INT32 2147483648 #define MAX_INT52 9007199254740991 @@ -171,6 +171,11 @@ static int32_t JSVALUE_TO_INT32(EncodedJSValue val) __attribute__((__always_inli static float JSVALUE_TO_FLOAT(EncodedJSValue val) __attribute__((__always_inline__)); static double JSVALUE_TO_DOUBLE(EncodedJSValue val) __attribute__((__always_inline__)); static bool JSVALUE_TO_BOOL(EncodedJSValue val) __attribute__((__always_inline__)); +static uint8_t GET_JSTYPE(EncodedJSValue val) __attribute__((__always_inline__)); +static bool JSTYPE_IS_TYPED_ARRAY(uint8_t type) __attribute__((__always_inline__)); +static bool JSCELL_IS_TYPED_ARRAY(EncodedJSValue val) __attribute__((__always_inline__)); +static void* JSVALUE_TO_TYPED_ARRAY_VECTOR(EncodedJSValue val) __attribute__((__always_inline__)); +static uint64_t JSVALUE_TO_TYPED_ARRAY_LENGTH(EncodedJSValue val) __attribute__((__always_inline__)); static bool JSVALUE_IS_CELL(EncodedJSValue val) { return !(val.asInt64 & NotCellMask); @@ -184,6 +189,25 @@ static bool JSVALUE_IS_NUMBER(EncodedJSValue val) { return val.asInt64 & NumberTag; } +static uint8_t GET_JSTYPE(EncodedJSValue val) { + return *(uint8_t*)((uint8_t*)val.asPtr + JSCell__offsetOfType); +} + +static bool JSTYPE_IS_TYPED_ARRAY(uint8_t type) { + return type >= JSTypeArrayBufferViewMin && type <= JSTypeArrayBufferViewMax; +} + +static bool JSCELL_IS_TYPED_ARRAY(EncodedJSValue val) { + return JSVALUE_IS_CELL(val) && JSTYPE_IS_TYPED_ARRAY(GET_JSTYPE(val)); +} + +static void* JSVALUE_TO_TYPED_ARRAY_VECTOR(EncodedJSValue val) { + return *(void**)((char*)val.asPtr + JSArrayBufferView__offsetOfVector); +} + +static uint64_t JSVALUE_TO_TYPED_ARRAY_LENGTH(EncodedJSValue val) { + return *(uint64_t*)((char*)val.asPtr + JSArrayBufferView__offsetOfLength); +} // JSValue numbers-as-pointers are represented as a 52-bit integer // Previously, the pointer was stored at the end of the 64-bit value @@ -193,6 +217,11 @@ static bool JSVALUE_IS_NUMBER(EncodedJSValue val) { static void* JSVALUE_TO_PTR(EncodedJSValue val) { if (val.asInt64 == TagValueNull) return 0; + + if (JSCELL_IS_TYPED_ARRAY(val)) { + return JSVALUE_TO_TYPED_ARRAY_VECTOR(val); + } + val.asInt64 -= DoubleEncodeOffset; size_t ptr = (size_t)val.asDouble; return (void*)ptr; @@ -275,6 +304,10 @@ static uint64_t JSVALUE_TO_UINT64(EncodedJSValue value) { return (uint64_t)JSVALUE_TO_DOUBLE(value); } + if (JSCELL_IS_TYPED_ARRAY(value)) { + return (uint64_t)JSVALUE_TO_TYPED_ARRAY_LENGTH(value); + } + return JSVALUE_TO_UINT64_SLOW(value); } static int64_t JSVALUE_TO_INT64(EncodedJSValue value) { diff --git a/src/bun.js/api/ffi.zig b/src/bun.js/api/ffi.zig index 6524f10526c42..25e14d2778ddb 100644 --- a/src/bun.js/api/ffi.zig +++ b/src/bun.js/api/ffi.zig @@ -76,6 +76,24 @@ const IOTask = JSC.IOTask; const TCC = @import("../../tcc.zig"); extern fn pthread_jit_write_protect_np(enable: bool) callconv(.C) void; +const Offsets = extern struct { + JSArrayBufferView__offsetOfLength: u32, + JSArrayBufferView__offsetOfByteOffset: u32, + JSArrayBufferView__offsetOfVector: u32, + JSCell__offsetOfType: u32, + + extern "C" var Bun__FFI__offsets: Offsets; + extern "C" fn Bun__FFI__ensureOffsetsAreLoaded() void; + fn loadOnce() void { + Bun__FFI__ensureOffsetsAreLoaded(); + } + var once = std.once(loadOnce); + pub fn get() *const Offsets { + once.call(); + return &Bun__FFI__offsets; + } +}; + pub const FFI = struct { dylib: ?std.DynLib = null, relocated_bytes_to_free: ?[]u8 = null, @@ -1361,6 +1379,11 @@ pub const FFI = struct { return ZigString.static("Cannot return napi_env to JavaScript").toErrorInstance(global); } + if (return_type == .buffer) { + abi_types.clearAndFree(allocator); + return ZigString.static("Cannot return a buffer to JavaScript (since byteLength and byteOffset are unknown)").toErrorInstance(global); + } + if (function.threadsafe and return_type != ABIType.void) { abi_types.clearAndFree(allocator); return ZigString.static("Threadsafe functions must return void").toErrorInstance(global); @@ -1542,14 +1565,7 @@ pub const FFI = struct { } _ = TCC.tcc_set_output_type(state, TCC.TCC_OUTPUT_MEMORY); - const Sizes = @import("../bindings/sizes.zig"); - var symbol_buf: [256]u8 = undefined; - TCC.tcc_define_symbol( - state, - "Bun_FFI_PointerOffsetToArgumentsList", - std.fmt.bufPrintZ(&symbol_buf, "{d}", .{Sizes.Bun_FFI_PointerOffsetToArgumentsList}) catch unreachable, - ); CompilerRT.define(state); // TCC.tcc_define_symbol( @@ -2064,7 +2080,7 @@ pub const FFI = struct { function = 17, napi_env = 18, napi_value = 19, - + buffer = 20, pub const max = @intFromEnum(ABIType.napi_value); /// Types that we can directly pass through as an `int64_t` @@ -2104,6 +2120,8 @@ pub const FFI = struct { .{ "uint64_t", ABIType.uint64_t }, .{ "uint8_t", ABIType.uint8_t }, .{ "usize", ABIType.uint64_t }, + .{ "size_t", ABIType.uint64_t }, + .{ "buffer", ABIType.buffer }, .{ "void*", ABIType.ptr }, .{ "ptr", ABIType.ptr }, .{ "pointer", ABIType.ptr }, @@ -2219,6 +2237,9 @@ pub const FFI = struct { try writer.writeAll(".asNapiValue"); return; }, + .buffer => { + try writer.writeAll("JSVALUE_TO_TYPED_ARRAY_VECTOR("); + }, } // if (self.fromi64) { // try writer.writeAll("EncodedJSValue{ "); @@ -2274,6 +2295,9 @@ pub const FFI = struct { .napi_value => { try writer.print("((EncodedJSValue) {{.asNapiValue = {s} }} )", .{self.symbol}); }, + .buffer => { + try writer.writeAll("0"); + }, } } }; @@ -2302,7 +2326,7 @@ pub const FFI = struct { pub fn typenameLabel(this: ABIType) []const u8 { return switch (this) { - .function, .cstring, .ptr => "void*", + .buffer, .function, .cstring, .ptr => "void*", .bool => "bool", .int8_t => "int8_t", .uint8_t => "uint8_t", @@ -2345,6 +2369,7 @@ pub const FFI = struct { .void => "void", .napi_env => "napi_env", .napi_value => "napi_value", + .buffer => "buffer", }; } }; @@ -2423,6 +2448,40 @@ const CompilerRT = struct { // there _ = TCC.tcc_compile_string(state, @embedFile(("libtcc1.c"))); } + + const Sizes = @import("../bindings/sizes.zig"); + var symbol_buf: [256]u8 = undefined; + TCC.tcc_define_symbol( + state, + "Bun_FFI_PointerOffsetToArgumentsList", + std.fmt.bufPrintZ(&symbol_buf, "{d}", .{Sizes.Bun_FFI_PointerOffsetToArgumentsList}) catch unreachable, + ); + const offsets = Offsets.get(); + TCC.tcc_define_symbol( + state, + "JSArrayBufferView__offsetOfLength", + std.fmt.bufPrintZ(&symbol_buf, "{d}", .{offsets.JSArrayBufferView__offsetOfLength}) catch unreachable, + ); + TCC.tcc_define_symbol( + state, + "JSArrayBufferView__offsetOfVector", + std.fmt.bufPrintZ(&symbol_buf, "{d}", .{offsets.JSArrayBufferView__offsetOfVector}) catch unreachable, + ); + TCC.tcc_define_symbol( + state, + "JSCell__offsetOfType", + std.fmt.bufPrintZ(&symbol_buf, "{d}", .{offsets.JSCell__offsetOfType}) catch unreachable, + ); + TCC.tcc_define_symbol( + state, + "JSTypeArrayBufferViewMin", + std.fmt.bufPrintZ(&symbol_buf, "{d}", .{@intFromEnum(JSC.JSValue.JSType.min_typed_array)}) catch unreachable, + ); + TCC.tcc_define_symbol( + state, + "JSTypeArrayBufferViewMax", + std.fmt.bufPrintZ(&symbol_buf, "{d}", .{@intFromEnum(JSC.JSValue.JSType.max_typed_array)}) catch unreachable, + ); } pub fn inject(state: *TCC.TCCState) void { diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 9a01304a0ffdb..e18a6fa776ffa 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -3679,6 +3679,9 @@ pub const JSValue = enum(JSValueReprInt) { JSAsJSONType = 0b11110000 | 1, _, + pub const min_typed_array: JSType = .Int8Array; + pub const max_typed_array: JSType = .DataView; + pub fn canGet(this: JSType) bool { return switch (this) { .Array, diff --git a/src/bun.js/bindings/ffi.cpp b/src/bun.js/bindings/ffi.cpp new file mode 100644 index 0000000000000..2f4b6f561ceeb --- /dev/null +++ b/src/bun.js/bindings/ffi.cpp @@ -0,0 +1,17 @@ +#include "root.h" + +typedef struct FFIFields { + uint32_t JSArrayBufferView__offsetOfLength; + uint32_t JSArrayBufferView__offsetOfByteOffset; + uint32_t JSArrayBufferView__offsetOfVector; + uint32_t JSCell__offsetOfType; +} FFIFields; +extern "C" FFIFields Bun__FFI__offsets = { 0 }; + +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(); +} diff --git a/src/js/bun/ffi.ts b/src/js/bun/ffi.ts index f6978c0966f9b..d5e4d74da212e 100644 --- a/src/js/bun/ffi.ts +++ b/src/js/bun/ffi.ts @@ -57,6 +57,7 @@ const FFIType = { fn: 17, napi_env: 18, napi_value: 19, + buffer: 20, }; const suffix = process.platform === "win32" ? "dll" : process.platform === "darwin" ? "dylib" : "so"; @@ -155,7 +156,7 @@ Object.defineProperty(globalThis, "__GlobalBunCString", { configurable: false, }); -const ffiWrappers = new Array(20); +const ffiWrappers = new Array(21); var char = "val|0"; ffiWrappers.fill(char); @@ -281,6 +282,13 @@ Object.defineProperty(globalThis, "__GlobalBunFFIPtrFunctionForWrapper", { enumerable: false, configurable: true, }); +Object.defineProperty(globalThis, "__GlobalBunFFIPtrArrayBufferViewFn", { + value: function isTypedArrayView(val) { + return $isTypedArrayView(val); + }, + enumerable: false, + configurable: true, +}); ffiWrappers[FFIType.cstring] = ffiWrappers[FFIType.pointer] = `{ if (typeof val === "number") return val; @@ -288,7 +296,11 @@ ffiWrappers[FFIType.cstring] = ffiWrappers[FFIType.pointer] = `{ return null; } - if (ArrayBuffer.isView(val) || val instanceof ArrayBuffer) { + if (__GlobalBunFFIPtrArrayBufferViewFn(val)) { + return val; + } + + if (val instanceof ArrayBuffer) { return __GlobalBunFFIPtrFunctionForWrapper(val); } @@ -299,6 +311,14 @@ ffiWrappers[FFIType.cstring] = ffiWrappers[FFIType.pointer] = `{ throw new TypeError(\`Unable to convert \${ val } to a pointer\`); }`; +ffiWrappers[FFIType.buffer] = `{ + if (!__GlobalBunFFIPtrArrayBufferViewFn(val)) { + throw new TypeError("Expected a TypedArray"); + } + + return val; +}`; + ffiWrappers[FFIType.function] = `{ if (typeof val === "number") { return val; diff --git a/test/js/bun/ffi/cc-fixture.c b/test/js/bun/ffi/cc-fixture.c index e39a15c784796..43541431b423b 100644 --- a/test/js/bun/ffi/cc-fixture.c +++ b/test/js/bun/ffi/cc-fixture.c @@ -8,6 +8,8 @@ #include #endif +#include + #if __has_include() #include @@ -20,6 +22,8 @@ napi_value napi_main(napi_env env) { #endif +uint8_t lastByte(uint8_t *arr, size_t len) { return arr[len - 1]; } + int main() { #if __has_include() diff --git a/test/js/bun/ffi/cc-fixture.js b/test/js/bun/ffi/cc-fixture.js index b3d4d642c4f72..1a71eabe94410 100644 --- a/test/js/bun/ffi/cc-fixture.js +++ b/test/js/bun/ffi/cc-fixture.js @@ -1,7 +1,10 @@ import { cc } from "bun:ffi"; import fixture from "./cc-fixture.c" with { type: "file" }; +let bytes = new Uint8Array(64); +bytes[bytes.length - 1] = 42; + const { - symbols: { napi_main, main }, + symbols: { napi_main, main, lastByte }, } = cc({ source: fixture, define: { @@ -9,6 +12,10 @@ const { }, symbols: { + "lastByte": { + args: ["ptr", "uint64_t"], + returns: "uint8_t", + }, "napi_main": { args: ["napi_env"], returns: "napi_value", @@ -27,3 +34,7 @@ if (main() !== 42) { if (napi_main(null) !== "Hello, Napi!") { throw new Error("napi_main() !== Hello, Napi!"); } + +if (lastByte(bytes, bytes.byteLength) !== 42) { + throw new Error("lastByte(bytes, bytes.length) !== 42"); +}