Add docs for FFI

This commit is contained in:
Jarred Sumner
2022-05-03 01:25:46 -07:00
parent c6d732eee2
commit 8b1924f6c2
23 changed files with 629 additions and 143 deletions

View File

@@ -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 && \

335
README.md
View File

@@ -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)

View File

@@ -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: [],
},
};

View File

@@ -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; }
void noop() {}

Binary file not shown.

View File

@@ -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 });

1
bench/ffi/plus100/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
./napi-plus100

View File

@@ -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)

View File

@@ -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

BIN
bench/ffi/plus100/libadd.dylib Executable file

Binary file not shown.

View File

@@ -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"
}
}

View File

@@ -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 });

View File

@@ -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; }

View File

@@ -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 });

BIN
bench/ffi/plus100/plus100.dylib Executable file

Binary file not shown.

View File

@@ -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 });

7
examples/add.rs Normal file
View File

@@ -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

12
examples/add.ts Normal file
View File

@@ -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));

6
examples/add.zig Normal file
View File

@@ -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

View File

@@ -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
// )

View File

@@ -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

View File

@@ -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,

39
types/bun/ffi.d.ts vendored
View File

@@ -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;
}