AphrodyBun GitHub

Runtime docs · Runtime · Interop & Tooling

C Compiler

Compile and run C from JavaScript with low overhead

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:

hello.ts
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:

hello.c
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.

FFITypeC TypeAliases
bufferchar*
cstringchar*
function(void*)(*)()fn, callback
ptrvoid*pointer, void*, char*
i8int8_tint8_t
i16int16_tint16_t
i32int32_tint32_t, int
i64int64_tint64_t, isize
i64_fastint64_t
u8uint8_tuint8_t
u16uint16_tuint16_t
u32uint32_tuint32_t
u64uint64_tuint64_t, usize
u64_fastuint64_t
f32floatfloat
f64doubledouble
boolbool
charchar
napi_envnapi_env
napi_valuenapi_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:

hello.ts
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:

hello.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:

hello.c
#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".