Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ae4a0ca
docs(ffi): cover cc, viewSource, Node-API types, and fix CFunction usage
robobun May 28, 2026
88f050d
test(ffi): add type coverage for CFunction, cc, and viewSource
robobun May 28, 2026
dcc1b50
docs(ffi): scope to documentation only
robobun May 28, 2026
2676221
docs(ffi): clarify isize/usize typing and read.intptr
robobun May 28, 2026
92be529
[autofix.ci] apply automated fixes
autofix-ci[bot] May 28, 2026
faf42d9
fix(ffi): apply cstring return wrapper to cc symbols
robobun May 28, 2026
b6467dd
fix(ffi): guard cc stack-trace label against array source
robobun May 28, 2026
8796fff
fix(ffi): pass napi_value/napi_env args through without coercion
robobun May 28, 2026
082e402
test(ffi): only tolerate missing node_api.h in cc napi test
robobun May 28, 2026
af218c2
test(ffi): make napi_value arg test header-free; drop unused include
robobun May 28, 2026
4681615
fix(ffi): add reverse-lookup keys for napi_env/napi_value/buffer
robobun May 28, 2026
00964c5
docs(cc): sync FFIType table with the ffi page (void, usize)
robobun May 28, 2026
d9b46d2
test(ffi): reuse beforeAll temp dir in numeric napi_value test
robobun May 28, 2026
6486ac2
docs(ffi): print the cc cstring result as a string
robobun May 28, 2026
f890234
fix(ffi): map the size_t type string in the FFIType table
robobun May 29, 2026
9a250b4
types(ffi): accept "size_t" as a string alias of uint64_t
robobun May 29, 2026
53d0a71
docs(ffi): sync d.ts JSDoc examples with the docs page
robobun May 29, 2026
232dfc2
docs(ffi): list size_t in the u64 alias row
robobun May 29, 2026
a352458
[autofix.ci] apply automated fixes
autofix-ci[bot] May 29, 2026
df6f525
ci: retrigger
robobun May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 41 additions & 21 deletions docs/runtime/c-compiler.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,27 +53,28 @@ What is the answer to the universe? 42

`cc` supports the same `FFIType` values as [`dlopen`](/runtime/ffi).

| `FFIType` | C Type | Aliases |
| ---------- | -------------- | --------------------------- |
| cstring | `char*` | |
| function | `(void*)(*)()` | `fn`, `callback` |
| ptr | `void*` | `pointer`, `void*`, `char*` |
| i8 | `int8_t` | `int8_t` |
| i16 | `int16_t` | `int16_t` |
| i32 | `int32_t` | `int32_t`, `int` |
| i64 | `int64_t` | `int64_t` |
| i64_fast | `int64_t` | |
| u8 | `uint8_t` | `uint8_t` |
| u16 | `uint16_t` | `uint16_t` |
| u32 | `uint32_t` | `uint32_t` |
| u64 | `uint64_t` | `uint64_t` |
| u64_fast | `uint64_t` | |
| f32 | `float` | `float` |
| f64 | `double` | `double` |
| bool | `bool` | |
| char | `char` | |
| napi_env | `napi_env` | |
| napi_value | `napi_value` | |
| `FFIType` | C Type | Aliases |
| ---------- | -------------- | ----------------------------- |
| cstring | `char*` | |
| function | `(void*)(*)()` | `fn`, `callback` |
| ptr | `void*` | `pointer`, `void*`, `char*` |
| i8 | `int8_t` | `int8_t` |
| i16 | `int16_t` | `int16_t` |
| i32 | `int32_t` | `int32_t`, `int` |
| i64 | `int64_t` | `int64_t` |
| i64_fast | `int64_t` | |
| u8 | `uint8_t` | `uint8_t` |
| u16 | `uint16_t` | `uint16_t` |
| u32 | `uint32_t` | `uint32_t` |
| u64 | `uint64_t` | `uint64_t`, `usize`, `size_t` |
| u64_fast | `uint64_t` | |
| f32 | `float` | `float` |
| f64 | `double` | `double` |
| bool | `bool` | |
| char | `char` | |
| void | `void` | |
| napi_env | `napi_env` | |
| napi_value | `napi_value` | |

### Strings, objects, and non-primitive types

Expand Down Expand Up @@ -200,3 +201,22 @@ cc({
},
});
```

#### `include: string | string[]`

Use `include` to add directories to the compiler's header search path. Equivalent to the `-I` option in gcc/clang.

```ts
type Include = string | string[];

cc({
source: "hello.c",
include: ["./vendor/include"],
symbols: {
hello: {
args: [],
returns: "int",
},
},
});
```
155 changes: 118 additions & 37 deletions docs/runtime/ffi.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -125,35 +125,81 @@ clang++ -dynamiclib add.cpp -o libadd.dylib

---

## Compiling C from JavaScript (`cc`)

Instead of compiling a shared library ahead of time, you can compile and run ISO C11 source code directly from JavaScript with `cc`. Bun compiles the source with [TinyCC](https://github.com/TinyCC/tinycc) and exposes the requested `symbols` as JavaScript functions, just like `dlopen`.

```ts
import { cc } from "bun:ffi";
import source from "./hello.c" with { type: "file" };

const {
symbols: { hello },
} = cc({
source,
symbols: {
hello: {
returns: "cstring",
args: [],
},
},
});

console.log(`${hello()}`); // "Hello, World!"
```

```c hello.c icon="file-code"
const char* hello() {
return "Hello, World!";
}
```

See the [C Compiler](/runtime/c-compiler) page for the full `cc` reference, including the `source`, `symbols`, `define`, `include`, `library`, and `flags` options.
Comment thread
claude[bot] marked this conversation as resolved.

---

## FFI types

The following `FFIType` values are supported.

| `FFIType` | C Type | Aliases |
| ---------- | -------------- | --------------------------- |
| buffer | `char*` | |
| cstring | `char*` | |
| function | `(void*)(*)()` | `fn`, `callback` |
| ptr | `void*` | `pointer`, `void*`, `char*` |
| i8 | `int8_t` | `int8_t` |
| i16 | `int16_t` | `int16_t` |
| i32 | `int32_t` | `int32_t`, `int` |
| i64 | `int64_t` | `int64_t` |
| i64_fast | `int64_t` | |
| u8 | `uint8_t` | `uint8_t` |
| u16 | `uint16_t` | `uint16_t` |
| u32 | `uint32_t` | `uint32_t` |
| u64 | `uint64_t` | `uint64_t` |
| u64_fast | `uint64_t` | |
| f32 | `float` | `float` |
| f64 | `double` | `double` |
| bool | `bool` | |
| char | `char` | |
| napi_env | `napi_env` | |
| napi_value | `napi_value` | |
| `FFIType` | C Type | Aliases |
| ---------- | -------------- | ----------------------------- |
| buffer | `char*` | |
| cstring | `char*` | |
| function | `(void*)(*)()` | `fn`, `callback` |
| ptr | `void*` | `pointer`, `void*`, `char*` |
| i8 | `int8_t` | `int8_t` |
| i16 | `int16_t` | `int16_t` |
| i32 | `int32_t` | `int32_t`, `int` |
| i64 | `int64_t` | `int64_t` |
| i64_fast | `int64_t` | |
| u8 | `uint8_t` | `uint8_t` |
| u16 | `uint16_t` | `uint16_t` |
| u32 | `uint32_t` | `uint32_t` |
| u64 | `uint64_t` | `uint64_t`, `usize`, `size_t` |
| u64_fast | `uint64_t` | |
| f32 | `float` | `float` |
| f64 | `double` | `double` |
| bool | `bool` | |
| char | `char` | |
| void | `void` | |
| napi_env | `napi_env` | |
| napi_value | `napi_value` | |

`buffer` arguments must be a `TypedArray` or `DataView`.

Note: `void` is only valid as a `returns` type — it is the default return type and cannot be used as an argument.

Note: `usize` and `isize` are pointer-sized integer aliases for `u64` and `i64`. Both are accepted at runtime, but only `"usize"` is currently in the TypeScript string types — if you need `isize` while type-checking, use `i64` (or `FFIType.i64`).

---

## Node-API types

The `napi_env` and `napi_value` types let `bun:ffi` interoperate with [Node-API](/runtime/node-api). A C function that receives a `napi_env` argument is called with Bun's current N-API environment, and a function that returns `napi_value` has its result converted directly into a JavaScript value — which is useful for passing strings, objects, and other non-primitive values that don't map 1:1 to C types.

These types work with `cc`. See [Strings, objects, and non-primitive types](/runtime/c-compiler#strings-objects-and-non-primitive-types) on the C Compiler page for worked examples.

---

## Strings
Expand Down Expand Up @@ -233,7 +279,7 @@ import { CFunction } from "bun:ffi";

let myNativeLibraryGetVersion = /* somehow, you got this pointer */

const getVersion = new CFunction({
const getVersion = CFunction({
returns: "cstring",
args: [],
ptr: myNativeLibraryGetVersion,
Expand Down Expand Up @@ -335,7 +381,7 @@ const onResolve = new JSCallback(arg => arg === 42, {
returns: "bool",
args: ["i32"],
});
const setOnResolve = new CFunction({
const setOnResolve = CFunction({
returns: "bool",
args: ["function"],
ptr: myNativeLibrarySetOnResolve,
Expand All @@ -352,6 +398,38 @@ setOnResolve(onResolve);

---

## Viewing generated bindings

Bun just-in-time compiles C wrappers for each symbol. To inspect the generated C code — for debugging a binding or out of curiosity — use `viewSource`. Given a map of symbols it returns an array of strings (one per symbol); for a single callback definition, pass `true` as the second argument to get a single string.

```ts
import { viewSource } from "bun:ffi";

// the generated wrappers for a set of symbols
const [getVersionSource] = viewSource(
{
getVersion: {
returns: "cstring",
args: [],
},
},
false,
);

// the generated wrapper for a single callback
const callbackSource = viewSource(
{
returns: "bool",
args: ["ptr", "usize"],
},
true,
);
```

You typically won't need this unless there's a bug in the FFI bindings generator.

---

## Pointers

Bun represents [pointers](<https://en.wikipedia.org/wiki/Pointer_(computer_programming)>) as a `number` in JavaScript.
Expand Down Expand Up @@ -416,19 +494,22 @@ console.log(

The `read` function behaves similarly to `DataView`, but it's usually faster because it doesn't need to create a `DataView` or `ArrayBuffer`.

| `FFIType` | `read` function |
| --------- | --------------- |
| ptr | `read.ptr` |
| i8 | `read.i8` |
| i16 | `read.i16` |
| i32 | `read.i32` |
| i64 | `read.i64` |
| u8 | `read.u8` |
| u16 | `read.u16` |
| u32 | `read.u32` |
| u64 | `read.u64` |
| f32 | `read.f32` |
| f64 | `read.f64` |
| Type | `read` function |
| ------ | --------------- |
| ptr | `read.ptr` |
| intptr | `read.intptr` |
| i8 | `read.i8` |
| i16 | `read.i16` |
| i32 | `read.i32` |
| i64 | `read.i64` |
| u8 | `read.u8` |
| u16 | `read.u16` |
| u32 | `read.u32` |
| u64 | `read.u64` |
| f32 | `read.f32` |
| f64 | `read.f64` |

`read.ptr` and `read.intptr` both read a pointer-sized integer. They are `read` helpers only — `intptr` is not an `FFIType`, so it can't be used in `args`/`returns`.

### Memory management

Expand Down
7 changes: 3 additions & 4 deletions packages/bun-types/ffi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ declare module "bun:ffi" {
["cstring"]: FFIType.cstring;
["function"]: FFIType.pointer; // for now
["usize"]: FFIType.uint64_t; // for now
["size_t"]: FFIType.uint64_t; // for now
Comment thread
robobun marked this conversation as resolved.
["callback"]: FFIType.pointer; // for now
["napi_env"]: FFIType.napi_env;
["napi_value"]: FFIType.napi_value;
Expand Down Expand Up @@ -602,13 +603,11 @@ declare module "bun:ffi" {
* },
* },
* });
* // "Hello, World!"
* console.log(hello());
* console.log(`${hello()}`); // "Hello, World!"
* ```
*
* `./hello.c`:
* ```c
* #include <stdio.h>
* const char* hello() {
* return "Hello, World!";
* }
Expand Down Expand Up @@ -702,7 +701,7 @@ declare module "bun:ffi" {
* ```js
* import {CFunction} from 'bun:ffi';
*
* const getVersion = new CFunction({
* const getVersion = CFunction({
* returns: "cstring",
* args: [],
* ptr: myNativeLibraryGetVersion,
Expand Down
25 changes: 21 additions & 4 deletions src/js/bun/ffi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const FFIType = {
"15": 15,
"16": 16,
"17": 17,
"18": 18,
"19": 19,
"20": 20,
bool: 11,
c_int: 5,
c_uint: 6,
Expand Down Expand Up @@ -45,6 +48,7 @@ const FFIType = {
uint64_t: 8,
uint8_t: 2,
usize: 8,
size_t: 8,
"void*": 12,
ptr: 12,
pointer: 12,
Expand Down Expand Up @@ -337,6 +341,11 @@ ffiWrappers[FFIType.function] = `{
return ptr;
}`;

// Node-API arguments are passed through as raw JSValues. `napi_value` is read
// directly on the native side, and `napi_env` is substituted there for the
// module's env, so neither must go through the default `val|0` coercion.
ffiWrappers[FFIType.napi_env] = ffiWrappers[FFIType.napi_value] = "val";
Comment thread
robobun marked this conversation as resolved.

function FFIBuilder(params, returnType, functionToCall, name) {
const hasReturnType = typeof FFIType[returnType] === "number" && FFIType[returnType as string] !== FFIType.void;
var paramNames = new Array(params.length);
Expand Down Expand Up @@ -497,19 +506,27 @@ function cc(options) {
const result = ccFn(options);
if (Error.isError(result)) throw result;

// `source` may be an array of files; use the first one for stack-trace labels.
const displayPath = $isJSArray(path) ? path[0] : path;

for (let key in result.symbols) {
var symbol = result.symbols[key];
if (options[key]?.args?.length || FFIType[options[key]?.returns as string] === FFIType.cstring) {
// `cc` nests the symbol definitions under `options.symbols` (unlike
// `dlopen`/`linkSymbols`, where `options` is the symbol map itself), so we
// must read from `options.symbols[key]` — otherwise the `cstring` return
// wrapper is never applied and the function returns a raw pointer.
const definition = options.symbols?.[key];
if (definition?.args?.length || FFIType[definition?.returns as string] === FFIType.cstring) {
result.symbols[key] = FFIBuilder(
Comment thread
robobun marked this conversation as resolved.
options[key].args ?? [],
options[key].returns ?? FFIType.void,
definition.args ?? [],
definition.returns ?? FFIType.void,
symbol,
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.
// in stacktraces:
// instead of
// "/usr/lib/sqlite3.so"
// we want
// "sqlite3_get_version() - sqlit3.so"
path.includes("/") ? `${key} (${path.split("/").pop()})` : `${key} (${path})`,
displayPath.includes("/") ? `${key} (${displayPath.split("/").pop()})` : `${key} (${displayPath})`,
);
} else {
// consistentcy
Expand Down
8 changes: 8 additions & 0 deletions test/integration/bun-types/fixture/ffi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ const lib = dlopen(
args: [FFIType.function],
returns: FFIType.function,
},
size_type: {
// "size_t" is accepted as a string alias of uint64_t
args: ["size_t"],
returns: "size_t",
},
allArgs: {
args: [
FFIType.char, // string
Expand Down Expand Up @@ -71,6 +76,9 @@ tsd.expectType<Pointer | null>(lib.symbols.ptr_type(ptr));

tsd.expectType<Pointer | null>(lib.symbols.fn_type(new JSCallback(() => {}, {})));

// "size_t" resolves to uint64_t: accepts number|bigint, returns bigint
tsd.expectType<bigint>(lib.symbols.size_type(1n));

function _arg(
...params: [
number,
Expand Down
Loading
Loading