bun:ffi has experimental support for compiling and running C from JavaScript with low overhead.
Usage (cc in bun:ffi)
See the introduction blog post for background.
JavaScript:
import { cc } from "bun:ffi";
import source from "./hello.c" with { type: "file" };
const {
symbols: { hello },
} = cc({
source,
symbols: {
hello: {
args: [],
returns: "int",
},
},
});
console.log("What is the answer to the universe?", hello());
C source:
int hello() {
return 42;
}
Running hello.ts prints:
bun hello.ts
What is the answer to the universe? 42
cc uses TinyCC to compile the C code, then links it with the JavaScript runtime, converting types in-place.
Primitive types
cc supports the same FFIType values as dlopen, except buffer_length. Only cc supports napi_env and 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, isize |
| 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 |
| u64_fast | uint64_t | |
| f32 | float | float |
| f64 | double | double |
| bool | bool | |
| char | char | |
| napi_env | napi_env | |
| napi_value | napi_value |
Strings, objects, and non-primitive types
For strings, objects, and other non-primitive types that don't map 1:1 to C types, cc supports N-API.
Use napi_value to pass or receive JavaScript values from a C function without any type conversions.
You can also pass a napi_env to receive the N-API environment used to call the JavaScript function.
Returning a C string to JavaScript
For example, to return a string from C to JavaScript:
import { cc } from "bun:ffi";
import source from "./hello.c" with { type: "file" };
const {
symbols: { hello },
} = cc({
source,
symbols: {
hello: {
args: ["napi_env"],
returns: "napi_value",
},
},
});
const result = hello();
And in C:
#include <node/node_api.h>
napi_value hello(napi_env env) {
napi_value result;
napi_create_string_utf8(env, "Hello, Napi!", NAPI_AUTO_LENGTH, &result);
return result;
}
The same approach returns other types like objects and arrays:
#include <node/node_api.h>
napi_value hello(napi_env env) {
napi_value result;
napi_create_object(env, &result);
return result;
}
cc Reference
library: string | string[]
Use library to specify the libraries to link with the C code.
type Library = string | string[];
cc({
source: "hello.c",
library: ["sqlite3"],
});
symbols
Use the symbols object to specify the functions and variables to expose to JavaScript.
type Symbols = {
[key: string]: {
args: FFIType[];
returns: FFIType;
};
};
source
source is the path to the C code to compile and link with the JavaScript runtime.
type Source = string | URL | BunFile;
cc({
source: "hello.c",
symbols: {
hello: {
args: [],
returns: "int",
},
},
});
source can also be an array of paths, which are compiled into one library.
code: string
Instead of source, pass the C source as a string. Diagnostics name it <inline>.
const { symbols } = cc({
code: "int twice(int x) { return x * 2; }",
symbols: { twice: { args: ["int"], returns: "int" } },
});
flags: string | string[]
flags is an optional string or array of strings passed to the TinyCC compiler.
type Flags = string | string[];
These are flags like -I for include directories and -D for preprocessor definitions. They are appended after the defaults (-std=c11 -Wl,--export-all-symbols -g -O2), so a later -std= or -O wins. -std= accepts c99, c11, c17 and c23, and their gnu variants; -std=c23 defines bool, true, false, nullptr, static_assert, alignas, alignof and thread_local.
Language support
TinyCC compiles C11: _Static_assert, _Generic, _Alignas, _Thread_local and _Atomic. Bun ships the freestanding headers (<stdbool.h>, <stddef.h>, <stdarg.h>, <stdalign.h>, <stdnoreturn.h>, <float.h>, <stdatomic.h>, <tgmath.h>). Atomic operations are sequentially consistent whatever memory order is requested. Other headers (<stdio.h>, <string.h>, …) come from the system: on Linux, including musl distributions such as Alpine, Bun looks in /usr/include and links against the C library in /usr/lib.
Arguments and errors
Every argument is checked against its declared type before the C function runs. A value of the wrong type, or a missing argument, throws a TypeError (ERR_INVALID_ARG_TYPE) naming arguments[i]. ptr and cstring arguments accept a TypedArray, DataView, ArrayBuffer, CString, number, bigint or null.
When compilation fails, cc() throws an Error whose errors property lists each compiler diagnostic as { file, line, severity, message }.
try {
cc({ code: "int f(void) { return 1 +; }", symbols: { f: { returns: "int" } } });
} catch (error) {
console.log(error.errors[0]); // { file: "<inline>", line: 1, severity: "error", message: "..." }
}
define: Record<string, string>
define is an optional object of preprocessor definitions passed to the TinyCC compiler.
type Defines = Record<string, string>;
cc({
source: "hello.c",
define: {
NDEBUG: "1",
},
});
Disabling cc
Pass --no-ffi-cc to disable the C compiler for a process. Any call to cc() then throws an error with the code ERR_FFI_CC_DISABLED. The --no-addons flag also disables cc(), in addition to process.dlopen.
bun --no-ffi-cc ./app.ts
Workers inherit the setting from their parent. A Worker can also set it for itself with execArgv: ["--no-ffi-cc"]. Standalone executables can bake it in with bun build --compile --compile-exec-argv="--no-ffi-cc".