diff options
-rw-r--r-- | Makefile | 9 | ||||
-rw-r--r-- | README.md | 335 | ||||
-rw-r--r-- | bench/ffi/ffi-overhead.js | 106 | ||||
-rw-r--r-- | bench/ffi/noop.c | 6 | ||||
-rwxr-xr-x | bench/ffi/noop.dylib | bin | 16759 -> 16759 bytes | |||
-rw-r--r-- | bench/ffi/noop.js | 5 | ||||
-rw-r--r-- | bench/ffi/plus100/.gitignore | 1 | ||||
-rw-r--r-- | bench/ffi/plus100/README.md | 27 | ||||
-rw-r--r-- | bench/ffi/plus100/download-napi-plus100.sh | 7 | ||||
-rwxr-xr-x | bench/ffi/plus100/libadd.dylib | bin | 0 -> 396768 bytes | |||
-rw-r--r-- | bench/ffi/plus100/package.json | 12 | ||||
-rw-r--r-- | bench/ffi/plus100/plus100.bun.js | 19 | ||||
-rw-r--r-- | bench/ffi/plus100/plus100.c | 6 | ||||
-rw-r--r-- | bench/ffi/plus100/plus100.deno.js | 18 | ||||
-rwxr-xr-x | bench/ffi/plus100/plus100.dylib | bin | 0 -> 16778 bytes | |||
-rw-r--r-- | bench/ffi/plus100/plus100.napi.mjs | 10 | ||||
-rw-r--r-- | examples/add.rs | 7 | ||||
-rw-r--r-- | examples/add.ts | 12 | ||||
-rw-r--r-- | examples/add.zig | 6 | ||||
-rw-r--r-- | integration/bunjs-only-snippets/ffi.test.js | 132 | ||||
-rw-r--r-- | src/javascript/jsc/ffi.exports.js | 4 | ||||
-rw-r--r-- | src/javascript/jsc/javascript.zig | 11 | ||||
-rw-r--r-- | types/bun/ffi.d.ts | 39 |
23 files changed, 629 insertions, 143 deletions
@@ -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 && \ @@ -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 & 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 Binary files differindex 74a1d3155..66c00c17c 100755 --- a/bench/ffi/noop.dylib +++ b/bench/ffi/noop.dylib 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 Binary files differnew file mode 100755 index 000000000..a2a1039ee --- /dev/null +++ b/bench/ffi/plus100/libadd.dylib 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 Binary files differnew file mode 100755 index 000000000..030d1afef --- /dev/null +++ b/bench/ffi/plus100/plus100.dylib 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; } |