aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile9
-rw-r--r--README.md335
-rw-r--r--bench/ffi/ffi-overhead.js106
-rw-r--r--bench/ffi/noop.c6
-rwxr-xr-xbench/ffi/noop.dylibbin16759 -> 16759 bytes
-rw-r--r--bench/ffi/noop.js5
-rw-r--r--bench/ffi/plus100/.gitignore1
-rw-r--r--bench/ffi/plus100/README.md27
-rw-r--r--bench/ffi/plus100/download-napi-plus100.sh7
-rwxr-xr-xbench/ffi/plus100/libadd.dylibbin0 -> 396768 bytes
-rw-r--r--bench/ffi/plus100/package.json12
-rw-r--r--bench/ffi/plus100/plus100.bun.js19
-rw-r--r--bench/ffi/plus100/plus100.c6
-rw-r--r--bench/ffi/plus100/plus100.deno.js18
-rwxr-xr-xbench/ffi/plus100/plus100.dylibbin0 -> 16778 bytes
-rw-r--r--bench/ffi/plus100/plus100.napi.mjs10
-rw-r--r--examples/add.rs7
-rw-r--r--examples/add.ts12
-rw-r--r--examples/add.zig6
-rw-r--r--integration/bunjs-only-snippets/ffi.test.js132
-rw-r--r--src/javascript/jsc/ffi.exports.js4
-rw-r--r--src/javascript/jsc/javascript.zig11
-rw-r--r--types/bun/ffi.d.ts39
23 files changed, 629 insertions, 143 deletions
diff --git a/Makefile b/Makefile
index 0e19242cc..02353a5b0 100644
--- a/Makefile
+++ b/Makefile
@@ -12,7 +12,7 @@ endif
MIN_MACOS_VERSION = 10.14
-MARCH_NATIVE =
+MARCH_NATIVE = -mtune=native
ARCH_NAME :=
DOCKER_BUILDARCH =
@@ -21,11 +21,12 @@ ifeq ($(ARCH_NAME_RAW),arm64)
DOCKER_BUILDARCH = arm64
BREW_PREFIX_PATH = /opt/homebrew
MIN_MACOS_VERSION = 11.0
+ MARCH_NATIVE = -mtune=native
else
ARCH_NAME = x64
DOCKER_BUILDARCH = amd64
BREW_PREFIX_PATH = /usr/local
- MARCH_NATIVE = -march=native
+ MARCH_NATIVE = -march=native -mtune=native
endif
AR=
@@ -102,7 +103,7 @@ LIBICONV_PATH =
AR=llvm-ar-13
endif
-OPTIMIZATION_LEVEL=-O3
+OPTIMIZATION_LEVEL=-O3 $(MARCH_NATIVE)
CFLAGS = $(MACOS_MIN_FLAG) $(MARCH_NATIVE) $(BITCODE_OR_SECTIONS) -g $(OPTIMIZATION_LEVEL) -fno-exceptions -fvisibility=hidden -fvisibility-inlines-hidden
BUN_TMP_DIR := /tmp/make-bun
BUN_DEPLOY_DIR = /tmp/bun-v$(PACKAGE_JSON_VERSION)/$(PACKAGE_NAME)
@@ -416,7 +417,7 @@ boringssl: boringssl-build boringssl-copy
boringssl-debug: boringssl-build-debug boringssl-copy
compile-ffi-test:
- clang -O3 -shared -undefined dynamic_lookup -o /tmp/bun-ffi-test$(SHARED_LIB_EXTENSION) ./integration/bunjs-only-snippets/ffi-test.c
+ clang $(OPTIMIZATION_LEVEL) -shared -undefined dynamic_lookup -o /tmp/bun-ffi-test$(SHARED_LIB_EXTENSION) ./integration/bunjs-only-snippets/ffi-test.c
libbacktrace:
cd $(BUN_DEPS_DIR)/libbacktrace && \
diff --git a/README.md b/README.md
index 5733a2aa8..f03b99a56 100644
--- a/README.md
+++ b/README.md
@@ -1549,6 +1549,341 @@ await Bun.write(Bun.file("index.html"), await fetch("http://example.com"));
await Bun.write("output.txt", Bun.file("input.txt"));
```
+### `bun:ffi` (Foreign Functions Interface)
+
+`bun:ffi` lets you efficiently call native libraries from JavaScript. It works with languages that support the C ABI (Zig, Rust, C/C++, C#, Nim, Kotlin, etc).
+
+Note: this is available in the next version of Bun (v0.0.79), which is not released yet.
+
+This snippet prints sqlite3's version number:
+
+```ts
+import { dlopen, FFIType, suffix } from "bun:ffi";
+
+// `suffix` is either "dylib", "so", or "dll" depending on the platform
+// you don't have to use "suffix", it's just there for convenience
+const path = `libsqlite3.${suffix}`;
+
+const {
+ symbols: {
+ // sqlite3_libversion is the function we will call
+ sqlite3_libversion,
+ },
+} =
+ // dlopen() expects:
+ // 1. a library name or file path
+ // 2. a map of symbols
+ dlopen(path, {
+ // `sqlite3_libversion` is a function that returns a string
+ sqlite3_libversion: {
+ // sqlite3_libversion takes no arguments
+ args: [],
+ // sqlite3_libversion returns a pointer to a string
+ returns: FFIType.cstring,
+ },
+ });
+
+console.log(`SQLite 3 version: ${sqlite3_libversion()}`);
+```
+
+#### Low-overhead FFI
+
+7ns to go from JavaScript <> native code with `bun:ffi` (on my machine, an M1X)
+
+- 2x faster than napi (Node v17.7.1)
+- 75x faster than Deno v1.21.1
+
+As measured in [this simple benchmark](./bench/ffi/plus100)
+
+<img width="699" alt="image" src="https://user-images.githubusercontent.com/709451/166412310-df3df42c-68af-40f0-aa7f-fb72895df72d.png">
+
+<details>
+
+<summary>Why is bun:ffi fast?</summary>
+
+Bun generates & just-in-time compiles C bindings that efficiently convert values between JavaScript types and native types.
+
+To compile C, Bun embeds [TinyCC](https://github.com/TinyCC/tinycc) a small and fast C compiler.
+
+</details>
+
+#### Usage
+
+With Zig:
+
+```zig
+// add.zig
+pub export fn add(a: i32, b: i32) i32 {
+ return a + b;
+}
+```
+
+To compile:
+
+```bash
+zig build-lib add.zig -dynamic -OReleaseFast
+```
+
+Pass `dlopen` the path to the shared library and the list of symbols you want to import.
+
+```ts
+import { dlopen, FFIType, suffix } from "bun:ffi";
+
+const path = `libadd.${suffix}`;
+
+const lib = dlopen(path, {
+ add: {
+ args: [FFIType.i32, FFIType.i32],
+ returns: FFIType.i32,
+ },
+});
+
+lib.symbols.add(1, 2);
+```
+
+With Rust:
+
+```rust
+// add.rs
+#[no_mangle]
+pub extern "C" fn add(a: isize, b: isize) -> isize {
+ a + b
+}
+```
+
+To compile:
+
+```bash
+rustc --crate-type cdylib add.rs
+```
+
+#### Supported FFI types (`FFIType`)
+
+| `FFIType` | C Type | Aliases |
+| --------- | ---------- | --------------------------- |
+| cstring | `char*` | |
+| ptr | `void*` | `pointer`, `void*`, `char*` |
+| i8 | `int8_t` | `int8_t` |
+| i16 | `int16_t` | `int16_t` |
+| i32 | `int32_t` | `int32_t`, `int` |
+| i64 | `int64_t` | `int32_t` |
+| u8 | `uint8_t` | `uint8_t` |
+| u16 | `uint16_t` | `uint16_t` |
+| u32 | `uint32_t` | `uint32_t` |
+| u64 | `uint64_t` | `uint32_t` |
+| f32 | `float` | `float` |
+| f64 | `double` | `double` |
+| bool | `bool` | |
+| char | `char` | |
+
+#### Strings (`CString`)
+
+JavaScript strings and C-like strings are different, and that complicates using strings with native libraries.
+
+<details>
+<summary>How are JavaScript strings and C strings different?</summary>
+
+JavaScript strings:
+
+- UTF16 (2 bytes per letter) or potentially latin1, depending on the JavaScript engine &amp; what characters are used
+- `length` stored separately
+- Immutable
+
+C strings:
+
+- UTF8 (1 byte per letter), usually
+- The length is not stored. Instead, the string is null-terminated which means the length is the index of the first `\0` it finds
+- Mutable
+
+</details>
+
+To help with that, `bun:ffi` exports `CString` which extends JavaScript's builtin `String` with a few extras:
+
+```ts
+class CString extends String {
+ /**
+ * Given a `ptr`, this will automatically search for the closing `\0` character and transcode from UTF-8 to UTF-16 if necessary.
+ */
+ constructor(ptr: number, byteOffset?: number, byteLength?: number): string;
+
+ /**
+ * The ptr to the C string
+ *
+ * This `CString` instance is a clone of the string, so it
+ * is safe to continue using this instance after the `ptr` has been
+ * freed.
+ */
+ ptr: number;
+ byteOffset?: number;
+ byteLength?: number;
+}
+```
+
+To convert from a 0-terminated pointer to a JavaScript string:
+
+```ts
+const myString = new CString(ptr);
+```
+
+To convert from a pointer with a known length to a JavaScript string:
+
+```ts
+const myString = new CString(ptr, 0, byteLength);
+```
+
+`new CString` clones the C string, so it is safe to continue using `myString` after `ptr` has been freed.
+
+```ts
+my_library_free(myString.ptr);
+
+// this is safe because myString is a clone
+console.log(myString);
+```
+
+##### Returning a string
+
+When used in `returns`, `FFIType.cstring` coerces the pointer to a JavaScript `string`. When used in `args`, `cstring` is identical to `ptr`.
+
+#### Pointers
+
+Bun represents [pointers](<https://en.wikipedia.org/wiki/Pointer_(computer_programming)>) as a `number` in JavaScript.
+
+<details>
+
+<summary>How does a 64 bit pointer fit in a JavaScript number?</summary>
+
+64-bit processors support up to [52 bits of addressible space](https://en.wikipedia.org/wiki/64-bit_computing#Limits_of_processors).
+
+[JavaScript numbers](https://en.wikipedia.org/wiki/Double-precision_floating-point_format#IEEE_754_double-precision_binary_floating-point_format:_binary64) support 63 bits of usable space, so that leaves us with about 11 bits of extra space.
+
+Why not `BigInt`?
+
+`BigInt` is slower. JavaScript engines allocate a separate `BigInt` which means they can't just fit in a regular javascript value.
+
+If you pass a `BigInt` to a function, it will be converted to a `number`
+
+</details>
+
+**To convert from a TypedArray to a pointer**:
+
+```ts
+import { ptr } from "bun:ffi";
+var myTypedArray = new Uint8Array(32);
+const myPtr = ptr(myTypedArray);
+```
+
+**To convert from a pointer to an ArrayBuffer**:
+
+```ts
+import { ptr, toArrayBuffer } from "bun:ffi";
+var myTypedArray = new Uint8Array(32);
+const myPtr = ptr(myTypedArray);
+
+// toTypedArray accepts a `byteOffset` and `byteLength`
+// if `byteLength` is not provided, it is assumed to be a null-terminated pointer
+myTypedArray = new Uint8Array(toArrayBuffer(myPtr, 0, 32), 0, 32);
+```
+
+**Pointers & memory safety**
+
+Using raw pointers outside of FFI is extremely not recommended.
+
+A future version of bun may add a CLI flag to disable `bun:ffi` (or potentially a separate build of bun).
+
+**Pointer alignment**
+
+If an API expects a pointer sized to something other than `char` or `u8`, make sure the typed array is also that size.
+
+A `u64*` is not exactly the same as `[8]u8*` due to alignment
+
+##### Passing a pointer
+
+Where FFI functions expect a pointer, pass a TypedArray of equivalent size
+
+Easymode:
+
+```ts
+import { dlopen, FFIType } from "bun:ffi";
+
+const {
+ symbols: { encode_png },
+} = dlopen(myLibraryPath, {
+ encode_png: {
+ // FFIType's can be specified as strings too
+ args: ["ptr", "uint32_t"],
+ returns: FFIType.ptr,
+ },
+});
+
+const pixels = new Uint8ClampedArray(128 * 128 * 4);
+pixels.fill(254);
+pixels.subarray(0, 32 * 32 * 2).fill(0);
+
+const out = encode_png(
+ // pixels will be passed as a pointer
+ pixels,
+
+ pixels.byteLength
+);
+```
+
+The [generated wrapper](https://github.com/Jarred-Sumner/bun/blob/c6d732eee2721cd6191672cbe2c57fb17c3fffe4/src/javascript/jsc/ffi.exports.js#L146-L148) will automatically convert the pointer to a TypedArray.
+
+<details>
+
+<summary>Hardmode</summary>
+
+If you don't want the automatic conversion or you want a pointer to a specific byte offset within the TypedArray, you can also directly get the pointer to the TypedArray:
+
+```ts
+import { dlopen, FFIType, ptr } from "bun:ffi";
+
+const {
+ symbols: { encode_png },
+} = dlopen(myLibraryPath, {
+ encode_png: {
+ // FFIType's can be specified as strings too
+ args: ["ptr", "u32", "u32"],
+ returns: FFIType.ptr,
+ },
+});
+
+const pixels = new Uint8ClampedArray(128 * 128 * 4);
+pixels.fill(254);
+
+// this returns a number! not a BigInt!
+const myPtr = ptr(pixels);
+
+const out = encode_png(
+ myPtr,
+
+ // dimensions:
+ 128,
+ 128
+);
+```
+
+</details>
+
+##### Reading pointers
+
+```ts
+const out = encode_png(
+ // pixels will be passed as a pointer
+ pixels,
+
+ // dimensions:
+ 128,
+ 128
+);
+
+// assuming it is 0-terminated, it can be read like this:
+var png = new Uint8Array(toArrayBuffer(out));
+
+// save it to disk:
+await Bun.write("out.png", png);
+```
+
### `Bun.Transpiler`
`Bun.Transpiler` lets you use Bun's transpiler from JavaScript (available in Bun.js)
diff --git a/bench/ffi/ffi-overhead.js b/bench/ffi/ffi-overhead.js
index 841c59992..4f63cebe5 100644
--- a/bench/ffi/ffi-overhead.js
+++ b/bench/ffi/ffi-overhead.js
@@ -12,221 +12,221 @@ import { bench, group, run } from "mitata";
const types = {
returns_true: {
- return_type: "bool",
+ returns: "bool",
args: [],
},
returns_false: {
- return_type: "bool",
+ returns: "bool",
args: [],
},
returns_42_char: {
- return_type: "char",
+ returns: "char",
args: [],
},
// returns_42_float: {
- // return_type: "float",
+ // returns: "float",
// args: [],
// },
// returns_42_double: {
- // return_type: "double",
+ // returns: "double",
// args: [],
// },
returns_42_uint8_t: {
- return_type: "uint8_t",
+ returns: "uint8_t",
args: [],
},
returns_neg_42_int8_t: {
- return_type: "int8_t",
+ returns: "int8_t",
args: [],
},
returns_42_uint16_t: {
- return_type: "uint16_t",
+ returns: "uint16_t",
args: [],
},
returns_42_uint32_t: {
- return_type: "uint32_t",
+ returns: "uint32_t",
args: [],
},
// // returns_42_uint64_t: {
- // // return_type: "uint64_t",
+ // // returns: "uint64_t",
// // args: [],
// // },
returns_neg_42_int16_t: {
- return_type: "int16_t",
+ returns: "int16_t",
args: [],
},
returns_neg_42_int32_t: {
- return_type: "int32_t",
+ returns: "int32_t",
args: [],
},
// returns_neg_42_int64_t: {
- // return_type: "int64_t",
+ // returns: "int64_t",
// args: [],
// },
identity_char: {
- return_type: "char",
+ returns: "char",
args: ["char"],
},
// identity_float: {
- // return_type: "float",
+ // returns: "float",
// args: ["float"],
// },
identity_bool: {
- return_type: "bool",
+ returns: "bool",
args: ["bool"],
},
// identity_double: {
- // return_type: "double",
+ // returns: "double",
// args: ["double"],
// },
identity_int8_t: {
- return_type: "int8_t",
+ returns: "int8_t",
args: ["int8_t"],
},
identity_int16_t: {
- return_type: "int16_t",
+ returns: "int16_t",
args: ["int16_t"],
},
identity_int32_t: {
- return_type: "int32_t",
+ returns: "int32_t",
args: ["int32_t"],
},
// identity_int64_t: {
- // return_type: "int64_t",
+ // returns: "int64_t",
// args: ["int64_t"],
// },
identity_uint8_t: {
- return_type: "uint8_t",
+ returns: "uint8_t",
args: ["uint8_t"],
},
identity_uint16_t: {
- return_type: "uint16_t",
+ returns: "uint16_t",
args: ["uint16_t"],
},
identity_uint32_t: {
- return_type: "uint32_t",
+ returns: "uint32_t",
args: ["uint32_t"],
},
// identity_uint64_t: {
- // return_type: "uint64_t",
+ // returns: "uint64_t",
// args: ["uint64_t"],
// },
add_char: {
- return_type: "char",
+ returns: "char",
args: ["char", "char"],
},
add_float: {
- return_type: "float",
+ returns: "float",
args: ["float", "float"],
},
add_double: {
- return_type: "double",
+ returns: "double",
args: ["double", "double"],
},
add_int8_t: {
- return_type: "int8_t",
+ returns: "int8_t",
args: ["int8_t", "int8_t"],
},
add_int16_t: {
- return_type: "int16_t",
+ returns: "int16_t",
args: ["int16_t", "int16_t"],
},
add_int32_t: {
- return_type: "int32_t",
+ returns: "int32_t",
args: ["int32_t", "int32_t"],
},
// add_int64_t: {
- // return_type: "int64_t",
+ // returns: "int64_t",
// args: ["int64_t", "int64_t"],
// },
add_uint8_t: {
- return_type: "uint8_t",
+ returns: "uint8_t",
args: ["uint8_t", "uint8_t"],
},
add_uint16_t: {
- return_type: "uint16_t",
+ returns: "uint16_t",
args: ["uint16_t", "uint16_t"],
},
add_uint32_t: {
- return_type: "uint32_t",
+ returns: "uint32_t",
args: ["uint32_t", "uint32_t"],
},
does_pointer_equal_42_as_int32_t: {
- return_type: "bool",
+ returns: "bool",
args: ["ptr"],
},
ptr_should_point_to_42_as_int32_t: {
- return_type: "ptr",
+ returns: "ptr",
args: [],
},
identity_ptr: {
- return_type: "ptr",
+ returns: "ptr",
args: ["ptr"],
},
// add_uint64_t: {
- // return_type: "uint64_t",
+ // returns: "uint64_t",
// args: ["uint64_t", "uint64_t"],
// },
cb_identity_true: {
- return_type: "bool",
+ returns: "bool",
args: ["ptr"],
},
cb_identity_false: {
- return_type: "bool",
+ returns: "bool",
args: ["ptr"],
},
cb_identity_42_char: {
- return_type: "char",
+ returns: "char",
args: ["ptr"],
},
// cb_identity_42_float: {
- // return_type: "float",
+ // returns: "float",
// args: ["ptr"],
// },
// cb_identity_42_double: {
- // return_type: "double",
+ // returns: "double",
// args: ["ptr"],
// },
cb_identity_42_uint8_t: {
- return_type: "uint8_t",
+ returns: "uint8_t",
args: ["ptr"],
},
cb_identity_neg_42_int8_t: {
- return_type: "int8_t",
+ returns: "int8_t",
args: ["ptr"],
},
cb_identity_42_uint16_t: {
- return_type: "uint16_t",
+ returns: "uint16_t",
args: ["ptr"],
},
cb_identity_42_uint32_t: {
- return_type: "uint32_t",
+ returns: "uint32_t",
args: ["ptr"],
},
// cb_identity_42_uint64_t: {
- // return_type: "uint64_t",
+ // returns: "uint64_t",
// args: ["ptr"],
// },
cb_identity_neg_42_int16_t: {
- return_type: "int16_t",
+ returns: "int16_t",
args: ["ptr"],
},
cb_identity_neg_42_int32_t: {
- return_type: "int32_t",
+ returns: "int32_t",
args: ["ptr"],
},
// cb_identity_neg_42_int64_t: {
- // return_type: "int64_t",
+ // returns: "int64_t",
// args: ["ptr"],
// },
return_a_function_ptr_to_function_that_returns_true: {
- return_type: "ptr",
+ returns: "ptr",
args: [],
},
};
diff --git a/bench/ffi/noop.c b/bench/ffi/noop.c
index 6b93beeaf..de15eb5e0 100644
--- a/bench/ffi/noop.c
+++ b/bench/ffi/noop.c
@@ -1,5 +1,5 @@
-// clang -O3 -shared -undefined dynamic_lookup ./noop.c -o noop.dylib
+// clang -O3 -shared -mtune=native ./noop.c -o noop.dylib
-int noop();
+void noop();
-int noop() { return 1; } \ No newline at end of file
+void noop() {} \ No newline at end of file
diff --git a/bench/ffi/noop.dylib b/bench/ffi/noop.dylib
index 74a1d3155..66c00c17c 100755
--- a/bench/ffi/noop.dylib
+++ b/bench/ffi/noop.dylib
Binary files differ
diff --git a/bench/ffi/noop.js b/bench/ffi/noop.js
index e28ea0629..13c8aef28 100644
--- a/bench/ffi/noop.js
+++ b/bench/ffi/noop.js
@@ -6,11 +6,10 @@ const {
} = dlopen("./noop.dylib", {
noop: {
args: [],
- return_type: "i32",
+ returns: "void",
},
});
-var raw = Object.keys(noop);
bench("noop", () => {
- raw();
+ noop();
});
run({ collect: false, percentiles: true });
diff --git a/bench/ffi/plus100/.gitignore b/bench/ffi/plus100/.gitignore
new file mode 100644
index 000000000..8911651ed
--- /dev/null
+++ b/bench/ffi/plus100/.gitignore
@@ -0,0 +1 @@
+./napi-plus100
diff --git a/bench/ffi/plus100/README.md b/bench/ffi/plus100/README.md
new file mode 100644
index 000000000..418e7bd34
--- /dev/null
+++ b/bench/ffi/plus100/README.md
@@ -0,0 +1,27 @@
+## FFI overhead comparison
+
+This compares the cost of a simple function call going from JavaScript to native code and back in:
+
+- Bun v0.0.79
+- napi.rs (Node v17.7.1)
+- Deno v1.21.1
+
+To set up:
+
+```bash
+bun setup
+```
+
+To run the benchmark:
+
+```bash
+bun bench
+```
+
+| Overhead | Using | Version | Platform |
+| -------- | ------- | ------- | --------------- |
+| 7ns | bun:ffi | 0.0.79 | macOS (aarch64) |
+| 18ns | napi.rs | 17.7.1 | macOS (aarch64) |
+| 580ns | Deno | 1.21.1 | macOS (aarch64) |
+
+The native [function](./plus100.c) called in Deno & Bun are the same. The function called with napi.rs is from napi's official [package-template](https://github.com/napi-rs/package-template)
diff --git a/bench/ffi/plus100/download-napi-plus100.sh b/bench/ffi/plus100/download-napi-plus100.sh
new file mode 100644
index 000000000..9cd226857
--- /dev/null
+++ b/bench/ffi/plus100/download-napi-plus100.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+rm -rf plus100-napi
+git clone https://github.com/napi-rs/package-template plus100-napi --depth=1
+cd plus100-napi
+npm install
+npm run build
diff --git a/bench/ffi/plus100/libadd.dylib b/bench/ffi/plus100/libadd.dylib
new file mode 100755
index 000000000..a2a1039ee
--- /dev/null
+++ b/bench/ffi/plus100/libadd.dylib
Binary files differ
diff --git a/bench/ffi/plus100/package.json b/bench/ffi/plus100/package.json
new file mode 100644
index 000000000..ba7ef1fd7
--- /dev/null
+++ b/bench/ffi/plus100/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "plus100",
+ "scripts": {
+ "setup": "bun run napi-setup && bun run compile",
+ "bench-deno": "deno run --allow-ffi --unstable -A plus100.deno.js",
+ "napi-setup": "bash download-napi-plus100.sh",
+ "bench-napi": "node plus100.napi.mjs",
+ "bench-bun": "bun run ./plus100.bun.js",
+ "compile": "clang -mtune=native -O3 -shared ./plus100.c -o plus100.dylib",
+ "bench": "echo -e '\n--- Bun:\n' && bun run bench-bun && echo -e '\n--- Node:\n' && bun run bench-napi && echo -e '\n--- Deno:\n' && bun run bench-deno"
+ }
+}
diff --git a/bench/ffi/plus100/plus100.bun.js b/bench/ffi/plus100/plus100.bun.js
new file mode 100644
index 000000000..ca4bf0f64
--- /dev/null
+++ b/bench/ffi/plus100/plus100.bun.js
@@ -0,0 +1,19 @@
+import { run, bench, group, baseline } from "mitata";
+import { dlopen } from "bun:ffi";
+
+const {
+ symbols: { plus100: plus100 },
+ close,
+} = dlopen("./plus100.dylib", {
+ plus100: {
+ params: ["int32_t"],
+ returns: "int32_t",
+ },
+});
+bench("plus100(1) (Bun FFI)", () => {
+ plus100(1);
+});
+
+// collect option collects benchmark returned values into array
+// prevents gc and can help with jit optimizing out functions
+run({ collect: false, percentiles: true });
diff --git a/bench/ffi/plus100/plus100.c b/bench/ffi/plus100/plus100.c
new file mode 100644
index 000000000..c5b7933ea
--- /dev/null
+++ b/bench/ffi/plus100/plus100.c
@@ -0,0 +1,6 @@
+// clang -mtune=native -O3 -shared ./plus100.c -o plus100.dylib
+#include <stdint.h>
+
+int32_t plus100(int32_t a);
+
+int32_t plus100(int32_t a) { return a + 100; }
diff --git a/bench/ffi/plus100/plus100.deno.js b/bench/ffi/plus100/plus100.deno.js
new file mode 100644
index 000000000..e6104efdd
--- /dev/null
+++ b/bench/ffi/plus100/plus100.deno.js
@@ -0,0 +1,18 @@
+import { run, bench, group, baseline } from "https://esm.sh/mitata";
+
+const {
+ symbols: { plus100: plus100 },
+ close,
+} = Deno.dlopen("./plus100.dylib", {
+ plus100: {
+ parameters: ["i32"],
+ result: "i32",
+ },
+});
+bench("plus100(1) (Deno FFI)", () => {
+ plus100(1);
+});
+
+// collect option collects benchmark returned values into array
+// prevents gc and can help with jit optimizing out functions
+run({ collect: false, percentiles: true });
diff --git a/bench/ffi/plus100/plus100.dylib b/bench/ffi/plus100/plus100.dylib
new file mode 100755
index 000000000..030d1afef
--- /dev/null
+++ b/bench/ffi/plus100/plus100.dylib
Binary files differ
diff --git a/bench/ffi/plus100/plus100.napi.mjs b/bench/ffi/plus100/plus100.napi.mjs
new file mode 100644
index 000000000..f4adda8d3
--- /dev/null
+++ b/bench/ffi/plus100/plus100.napi.mjs
@@ -0,0 +1,10 @@
+import { bench, run } from "mitata";
+
+import module from "module";
+
+const { plus100 } = module.createRequire(import.meta.url)("./plus100-napi");
+
+bench("plus100(1) (napi.rs)", () => {
+ plus100(1);
+});
+run({ collect: false, percentiles: true });
diff --git a/examples/add.rs b/examples/add.rs
new file mode 100644
index 000000000..8ff8676cc
--- /dev/null
+++ b/examples/add.rs
@@ -0,0 +1,7 @@
+#[no_mangle]
+pub extern "C" fn add(a: isize, b: isize) -> isize {
+ a + b
+}
+
+// to compile:
+// rustc --crate-type cdylib add.rs
diff --git a/examples/add.ts b/examples/add.ts
new file mode 100644
index 000000000..e975b122e
--- /dev/null
+++ b/examples/add.ts
@@ -0,0 +1,12 @@
+import { dlopen, suffix } from "bun:ffi";
+
+const {
+ symbols: { add },
+} = dlopen(`./libadd.${suffix}`, {
+ add: {
+ args: ["i32", "i32"],
+ returns: "i32",
+ },
+});
+
+console.log(add(1, 2));
diff --git a/examples/add.zig b/examples/add.zig
new file mode 100644
index 000000000..24b78bec7
--- /dev/null
+++ b/examples/add.zig
@@ -0,0 +1,6 @@
+pub export fn add(a: i32, b: i32) i32 {
+ return a + b;
+}
+
+// to compile:
+// zig build-lib -OReleaseFast ./add.zig -dynamic --name add
diff --git a/integration/bunjs-only-snippets/ffi.test.js b/integration/bunjs-only-snippets/ffi.test.js
index 56e36d6e0..258ee93ec 100644
--- a/integration/bunjs-only-snippets/ffi.test.js
+++ b/integration/bunjs-only-snippets/ffi.test.js
@@ -18,7 +18,7 @@ it("ffi print", async () => {
import.meta.dir + "/ffi.test.fixture.callback.c",
viewSource(
{
- return_type: "bool",
+ returns: "bool",
args: ["ptr"],
},
true
@@ -29,7 +29,7 @@ it("ffi print", async () => {
viewSource(
{
not_a_callback: {
- return_type: "float",
+ returns: "float",
args: ["float"],
},
},
@@ -39,7 +39,7 @@ it("ffi print", async () => {
expect(
viewSource(
{
- return_type: "int8_t",
+ returns: "int8_t",
args: [],
},
true
@@ -49,7 +49,7 @@ it("ffi print", async () => {
viewSource(
{
a: {
- return_type: "int8_t",
+ returns: "int8_t",
args: [],
},
},
@@ -61,221 +61,221 @@ it("ffi print", async () => {
it("ffi run", () => {
const types = {
returns_true: {
- return_type: "bool",
+ returns: "bool",
args: [],
},
returns_false: {
- return_type: "bool",
+ returns: "bool",
args: [],
},
returns_42_char: {
- return_type: "char",
+ returns: "char",
args: [],
},
returns_42_float: {
- return_type: "float",
+ returns: "float",
args: [],
},
returns_42_double: {
- return_type: "double",
+ returns: "double",
args: [],
},
returns_42_uint8_t: {
- return_type: "uint8_t",
+ returns: "uint8_t",
args: [],
},
returns_neg_42_int8_t: {
- return_type: "int8_t",
+ returns: "int8_t",
args: [],
},
returns_42_uint16_t: {
- return_type: "uint16_t",
+ returns: "uint16_t",
args: [],
},
returns_42_uint32_t: {
- return_type: "uint32_t",
+ returns: "uint32_t",
args: [],
},
returns_42_uint64_t: {
- return_type: "uint64_t",
+ returns: "uint64_t",
args: [],
},
returns_neg_42_int16_t: {
- return_type: "int16_t",
+ returns: "int16_t",
args: [],
},
returns_neg_42_int32_t: {
- return_type: "int32_t",
+ returns: "int32_t",
args: [],
},
returns_neg_42_int64_t: {
- return_type: "int64_t",
+ returns: "int64_t",
args: [],
},
identity_char: {
- return_type: "char",
+ returns: "char",
args: ["char"],
},
identity_float: {
- return_type: "float",
+ returns: "float",
args: ["float"],
},
identity_bool: {
- return_type: "bool",
+ returns: "bool",
args: ["bool"],
},
identity_double: {
- return_type: "double",
+ returns: "double",
args: ["double"],
},
identity_int8_t: {
- return_type: "int8_t",
+ returns: "int8_t",
args: ["int8_t"],
},
identity_int16_t: {
- return_type: "int16_t",
+ returns: "int16_t",
args: ["int16_t"],
},
identity_int32_t: {
- return_type: "int32_t",
+ returns: "int32_t",
args: ["int32_t"],
},
identity_int64_t: {
- return_type: "int64_t",
+ returns: "int64_t",
args: ["int64_t"],
},
identity_uint8_t: {
- return_type: "uint8_t",
+ returns: "uint8_t",
args: ["uint8_t"],
},
identity_uint16_t: {
- return_type: "uint16_t",
+ returns: "uint16_t",
args: ["uint16_t"],
},
identity_uint32_t: {
- return_type: "uint32_t",
+ returns: "uint32_t",
args: ["uint32_t"],
},
identity_uint64_t: {
- return_type: "uint64_t",
+ returns: "uint64_t",
args: ["uint64_t"],
},
add_char: {
- return_type: "char",
+ returns: "char",
args: ["char", "char"],
},
add_float: {
- return_type: "float",
+ returns: "float",
args: ["float", "float"],
},
add_double: {
- return_type: "double",
+ returns: "double",
args: ["double", "double"],
},
add_int8_t: {
- return_type: "int8_t",
+ returns: "int8_t",
args: ["int8_t", "int8_t"],
},
add_int16_t: {
- return_type: "int16_t",
+ returns: "int16_t",
args: ["int16_t", "int16_t"],
},
add_int32_t: {
- return_type: "int32_t",
+ returns: "int32_t",
args: ["int32_t", "int32_t"],
},
add_int64_t: {
- return_type: "int64_t",
+ returns: "int64_t",
args: ["int64_t", "int64_t"],
},
add_uint8_t: {
- return_type: "uint8_t",
+ returns: "uint8_t",
args: ["uint8_t", "uint8_t"],
},
add_uint16_t: {
- return_type: "uint16_t",
+ returns: "uint16_t",
args: ["uint16_t", "uint16_t"],
},
add_uint32_t: {
- return_type: "uint32_t",
+ returns: "uint32_t",
args: ["uint32_t", "uint32_t"],
},
does_pointer_equal_42_as_int32_t: {
- return_type: "bool",
+ returns: "bool",
args: ["ptr"],
},
ptr_should_point_to_42_as_int32_t: {
- return_type: "ptr",
+ returns: "ptr",
args: [],
},
identity_ptr: {
- return_type: "ptr",
+ returns: "ptr",
args: ["ptr"],
},
add_uint64_t: {
- return_type: "uint64_t",
+ returns: "uint64_t",
args: ["uint64_t", "uint64_t"],
},
cb_identity_true: {
- return_type: "bool",
+ returns: "bool",
args: ["ptr"],
},
cb_identity_false: {
- return_type: "bool",
+ returns: "bool",
args: ["ptr"],
},
cb_identity_42_char: {
- return_type: "char",
+ returns: "char",
args: ["ptr"],
},
cb_identity_42_float: {
- return_type: "float",
+ returns: "float",
args: ["ptr"],
},
cb_identity_42_double: {
- return_type: "double",
+ returns: "double",
args: ["ptr"],
},
cb_identity_42_uint8_t: {
- return_type: "uint8_t",
+ returns: "uint8_t",
args: ["ptr"],
},
cb_identity_neg_42_int8_t: {
- return_type: "int8_t",
+ returns: "int8_t",
args: ["ptr"],
},
cb_identity_42_uint16_t: {
- return_type: "uint16_t",
+ returns: "uint16_t",
args: ["ptr"],
},
cb_identity_42_uint32_t: {
- return_type: "uint32_t",
+ returns: "uint32_t",
args: ["ptr"],
},
cb_identity_42_uint64_t: {
- return_type: "uint64_t",
+ returns: "uint64_t",
args: ["ptr"],
},
cb_identity_neg_42_int16_t: {
- return_type: "int16_t",
+ returns: "int16_t",
args: ["ptr"],
},
cb_identity_neg_42_int32_t: {
- return_type: "int32_t",
+ returns: "int32_t",
args: ["ptr"],
},
cb_identity_neg_42_int64_t: {
- return_type: "int64_t",
+ returns: "int64_t",
args: ["ptr"],
},
return_a_function_ptr_to_function_that_returns_true: {
- return_type: "ptr",
+ returns: "ptr",
args: [],
},
};
@@ -426,7 +426,7 @@ it("ffi run", () => {
// const first = native.callback(
// {
- // return_type: "bool",
+ // returns: "bool",
// },
// identityBool
// );
@@ -440,7 +440,7 @@ it("ffi run", () => {
// cb_identity_false(
// callback(
// {
- // return_type: "bool",
+ // returns: "bool",
// },
// () => false
// )
@@ -451,7 +451,7 @@ it("ffi run", () => {
// cb_identity_42_char(
// callback(
// {
- // return_type: "char",
+ // returns: "char",
// },
// () => 42
// )
@@ -461,7 +461,7 @@ it("ffi run", () => {
// cb_identity_42_uint8_t(
// callback(
// {
- // return_type: "uint8_t",
+ // returns: "uint8_t",
// },
// () => 42
// )
@@ -471,7 +471,7 @@ it("ffi run", () => {
// cb_identity_neg_42_int8_t(
// callback(
// {
- // return_type: "int8_t",
+ // returns: "int8_t",
// },
// () => -42
// )
@@ -480,7 +480,7 @@ it("ffi run", () => {
// cb_identity_42_uint16_t(
// callback(
// {
- // return_type: "uint16_t",
+ // returns: "uint16_t",
// },
// () => 42
// )
@@ -489,7 +489,7 @@ it("ffi run", () => {
// cb_identity_42_uint32_t(
// callback(
// {
- // return_type: "uint32_t",
+ // returns: "uint32_t",
// },
// () => 42
// )
@@ -498,7 +498,7 @@ it("ffi run", () => {
// cb_identity_neg_42_int16_t(
// callback(
// {
- // return_type: "int16_t",
+ // returns: "int16_t",
// },
// () => -42
// )
@@ -507,7 +507,7 @@ it("ffi run", () => {
// cb_identity_neg_42_int32_t(
// callback(
// {
- // return_type: "int32_t",
+ // returns: "int32_t",
// },
// () => -42
// )
diff --git a/src/javascript/jsc/ffi.exports.js b/src/javascript/jsc/ffi.exports.js
index 949226436..fea92e0cd 100644
--- a/src/javascript/jsc/ffi.exports.js
+++ b/src/javascript/jsc/ffi.exports.js
@@ -214,11 +214,11 @@ export function dlopen(path, options) {
var symbol = result.symbols[key];
if (
options[key]?.args?.length ||
- FFIType[options[key]?.return_type] === FFIType.cstring
+ FFIType[options[key]?.returns] === FFIType.cstring
) {
result.symbols[key] = FFIBuilder(
options[key].args ?? [],
- options[key].return_type ?? FFIType.void,
+ options[key].returns ?? FFIType.void,
symbol,
// in stacktraces:
// instead of
diff --git a/src/javascript/jsc/javascript.zig b/src/javascript/jsc/javascript.zig
index efb0481f7..894b7bea5 100644
--- a/src/javascript/jsc/javascript.zig
+++ b/src/javascript/jsc/javascript.zig
@@ -937,6 +937,8 @@ pub const VirtualMachine = struct {
this.resolved_count = 0;
}
+ const shared_library_suffix = if (Environment.isMac) "dylib" else if (Environment.isLinux) "so" else "";
+
inline fn _fetch(
_: *JSGlobalObject,
_specifier: string,
@@ -1081,7 +1083,14 @@ pub const VirtualMachine = struct {
} else if (strings.eqlComptime(_specifier, "bun:ffi")) {
return ResolvedSource{
.allocator = null,
- .source_code = ZigString.init("export const FFIType = " ++ JSC.FFI.ABIType.map_to_js_object ++ ";\n\n" ++ @embedFile("ffi.exports.js") ++ "\n"),
+ .source_code = ZigString.init(
+ "export const FFIType = " ++
+ JSC.FFI.ABIType.map_to_js_object ++
+ ";\n\n" ++
+ "export const suffix = '" ++ shared_library_suffix ++ "';\n\n" ++
+ @embedFile("ffi.exports.js") ++
+ "\n",
+ ),
.specifier = ZigString.init("bun:ffi"),
.source_url = ZigString.init("bun:ffi"),
.hash = 0,
diff --git a/types/bun/ffi.d.ts b/types/bun/ffi.d.ts
index afd75103b..67cbfb593 100644
--- a/types/bun/ffi.d.ts
+++ b/types/bun/ffi.d.ts
@@ -314,7 +314,7 @@ declare module "bun:ffi" {
void = 13,
/**
- * When used as a `return_type`, this will automatically become a {@link CString}.
+ * When used as a `returns`, this will automatically become a {@link CString}.
*
* When used in `args` it is equivalent to {@link FFIType.pointer}
*
@@ -365,7 +365,7 @@ declare module "bun:ffi" {
* const lib = dlopen('add', {
* // FFIType can be used or you can pass string labels.
* args: [FFIType.i32, "i32"],
- * return_type: "i32",
+ * returns: "i32",
* });
* lib.symbols.add(1, 2)
* ```
@@ -389,7 +389,7 @@ declare module "bun:ffi" {
* ```js
* const lib = dlopen('z', {
* version: {
- * return_type: "ptr",
+ * returns: "ptr",
* }
* });
* console.log(new CString(lib.symbols.version()));
@@ -402,18 +402,18 @@ declare module "bun:ffi" {
* }
* ```
*/
- return_type?: FFITypeOrString;
+ returns?: FFITypeOrString;
}
type Symbols = Record<string, FFIFunction>;
- /**
- * Compile a callback function
- *
- * Returns a function pointer
- *
- */
- export function callback(ffi: FFIFunction, cb: Function): number;
+ // /**
+ // * Compile a callback function
+ // *
+ // * Returns a function pointer
+ // *
+ // */
+ // export function callback(ffi: FFIFunction, cb: Function): number;
export interface Library {
symbols: Record<string, CallableFunction>;
@@ -583,4 +583,21 @@ declare module "bun:ffi" {
*/
export function viewSource(symbols: Symbols, is_callback?: false): string[];
export function viewSource(callback: FFIFunction, is_callback: true): string;
+
+ /**
+ * Platform-specific file extension name for dynamic libraries
+ *
+ * "." is not included
+ *
+ * @example
+ * ```js
+ * "dylib" // macOS
+ * ```
+ *
+ * @example
+ * ```js
+ * "so" // linux
+ * ```
+ */
+ export const suffix: string;
}