Skip to main content
Bun includes TinyCC, a fast C compiler that can compile and execute C code at runtime. This is useful for high-performance computation, calling system APIs, and prototyping native code.

Basic Usage

import { cc } from "bun:ffi";

const lib = cc({
  source: `
    int add(int a, int b) {
      return a + b;
    }
  `,
  symbols: {
    add: {
      args: ["i32", "i32"],
      returns: "i32",
    },
  },
});

const result = lib.symbols.add(5, 3);
console.log(result); // 8

Source Options

Inline Source

const lib = cc({
  source: `
    #include <stdio.h>
    
    void greet(const char* name) {
      printf("Hello, %s!\\n", name);
    }
  `,
  symbols: {
    greet: {
      args: ["cstring"],
      returns: "void",
    },
  },
});

lib.symbols.greet("World");

Source Files

const lib = cc({
  source: "math_utils.c",
  symbols: {
    factorial: {
      args: ["i32"],
      returns: "i64",
    },
  },
});

Multiple Files

const lib = cc({
  source: [
    "helpers.c",
    "algorithms.c",
    "main.c",
  ],
  symbols: { /* ... */ },
});

Headers and Includes

Standard Headers

TinyCC includes standard C library headers:
const lib = cc({
  source: `
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <math.h>
    
    double calculate(double x) {
      return sqrt(x * x + sin(x));
    }
  `,
  symbols: {
    calculate: {
      args: ["f64"],
      returns: "f64",
    },
  },
});

Custom Include Paths

const lib = cc({
  source: `
    #include "my_header.h"
    #include "utils/helpers.h"
    
    int process() {
      return helper_function();
    }
  `,
  include: [
    "./include",
    "./vendor/lib/include",
  ],
  symbols: { /* ... */ },
});

Linking Libraries

System Libraries

const lib = cc({
  source: `
    #include <sqlite3.h>
    
    const char* get_version() {
      return sqlite3_libversion();
    }
  `,
  library: ["sqlite3"],
  symbols: {
    get_version: {
      args: [],
      returns: "cstring",
    },
  },
});

console.log(lib.symbols.get_version()); // "3.43.0"

Multiple Libraries

const lib = cc({
  source: `/* ... */`,
  library: ["m", "pthread", "dl"],
  symbols: { /* ... */ },
});

Library Search Paths

const lib = cc({
  source: `/* ... */`,
  library: ["mylib"],
  libraryPath: [
    "/usr/local/lib",
    "./build/lib",
  ],
  symbols: { /* ... */ },
});

Preprocessor Defines

const lib = cc({
  source: `
    #ifdef DEBUG
    #define LOG(x) printf("%s\\n", x)
    #else
    #define LOG(x)
    #endif
    
    void process() {
      LOG("Processing...");
      // do work
    }
  `,
  define: {
    DEBUG: "1",
    VERSION: "\"1.0.0\"",
    MAX_SIZE: "1024",
  },
  symbols: { /* ... */ },
});

Compiler Flags

Optimization

const lib = cc({
  source: `/* ... */`,
  flags: "-O2", // Optimize
  symbols: { /* ... */ },
});

Warnings

const lib = cc({
  source: `/* ... */`,
  flags: "-Wall -Wextra",
  symbols: { /* ... */ },
});

Multiple Flags

const lib = cc({
  source: `/* ... */`,
  flags: [
    "-O2",
    "-Wall",
    "-Wextra",
    "-std=c11",
  ],
  symbols: { /* ... */ },
});

Symbol Types

See the FFI documentation for a complete list of supported types.

Common Patterns

const lib = cc({
  source: `
    // Integers
    int add_ints(int a, int b) { return a + b; }
    
    // Floats
    double add_floats(double a, double b) { return a + b; }
    
    // Strings
    const char* concat(const char* a, const char* b) {
      static char result[256];
      sprintf(result, "%s%s", a, b);
      return result;
    }
    
    // Pointers
    void fill_array(int* arr, int size) {
      for (int i = 0; i < size; i++) {
        arr[i] = i * i;
      }
    }
  `,
  symbols: {
    add_ints: {
      args: ["i32", "i32"],
      returns: "i32",
    },
    add_floats: {
      args: ["f64", "f64"],
      returns: "f64",
    },
    concat: {
      args: ["cstring", "cstring"],
      returns: "cstring",
    },
    fill_array: {
      args: ["ptr", "i32"],
      returns: "void",
    },
  },
});

Working with Arrays

Passing Arrays

import { ptr } from "bun:ffi";

const lib = cc({
  source: `
    int sum_array(int* arr, int length) {
      int sum = 0;
      for (int i = 0; i < length; i++) {
        sum += arr[i];
      }
      return sum;
    }
  `,
  symbols: {
    sum_array: {
      args: ["ptr", "i32"],
      returns: "i32",
    },
  },
});

const arr = new Int32Array([1, 2, 3, 4, 5]);
const total = lib.symbols.sum_array(ptr(arr), arr.length);
console.log(total); // 15

Returning Arrays

const lib = cc({
  source: `
    #include <stdlib.h>
    
    int* create_range(int size) {
      int* arr = malloc(size * sizeof(int));
      for (int i = 0; i < size; i++) {
        arr[i] = i;
      }
      return arr;
    }
    
    void free_array(int* arr) {
      free(arr);
    }
  `,
  symbols: {
    create_range: {
      args: ["i32"],
      returns: "ptr",
    },
    free_array: {
      args: ["ptr"],
      returns: "void",
    },
  },
});

// Get pointer from C
const arrPtr = lib.symbols.create_range(10);

// Read from pointer
const arr = new Int32Array(new ArrayBuffer(40), arrPtr);
console.log(arr); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

// Free C memory
lib.symbols.free_array(arrPtr);

Memory Management

Stack Allocation

Fast for small, short-lived data:
const lib = cc({
  source: `
    typedef struct {
      int x;
      int y;
    } Point;
    
    Point create_point(int x, int y) {
      Point p = { x, y };
      return p; // Returned by value
    }
  `,
  symbols: { /* ... */ },
});

Heap Allocation

Required for large or long-lived data:
const lib = cc({
  source: `
    #include <stdlib.h>
    
    typedef struct {
      int* data;
      int size;
    } Buffer;
    
    Buffer* create_buffer(int size) {
      Buffer* buf = malloc(sizeof(Buffer));
      buf->data = malloc(size * sizeof(int));
      buf->size = size;
      return buf;
    }
    
    void free_buffer(Buffer* buf) {
      free(buf->data);
      free(buf);
    }
  `,
  symbols: {
    create_buffer: {
      args: ["i32"],
      returns: "ptr",
    },
    free_buffer: {
      args: ["ptr"],
      returns: "void",
    },
  },
});

const buf = lib.symbols.create_buffer(1024);
// Use buffer...
lib.symbols.free_buffer(buf);

Real-World Examples

Fast Math

const math = cc({
  source: `
    #include <math.h>
    
    void vector_add(double* a, double* b, double* result, int n) {
      for (int i = 0; i < n; i++) {
        result[i] = a[i] + b[i];
      }
    }
    
    double vector_dot(double* a, double* b, int n) {
      double sum = 0.0;
      for (int i = 0; i < n; i++) {
        sum += a[i] * b[i];
      }
      return sum;
    }
  `,
  library: ["m"],
  symbols: {
    vector_add: {
      args: ["ptr", "ptr", "ptr", "i32"],
      returns: "void",
    },
    vector_dot: {
      args: ["ptr", "ptr", "i32"],
      returns: "f64",
    },
  },
});

const a = new Float64Array([1, 2, 3]);
const b = new Float64Array([4, 5, 6]);
const result = new Float64Array(3);

math.symbols.vector_add(ptr(a), ptr(b), ptr(result), 3);
console.log(result); // [5, 7, 9]

const dot = math.symbols.vector_dot(ptr(a), ptr(b), 3);
console.log(dot); // 32

Image Processing

const image = cc({
  source: `
    void grayscale(unsigned char* pixels, int width, int height) {
      for (int i = 0; i < width * height; i++) {
        int offset = i * 4;
        unsigned char r = pixels[offset];
        unsigned char g = pixels[offset + 1];
        unsigned char b = pixels[offset + 2];
        
        unsigned char gray = (r + g + b) / 3;
        
        pixels[offset] = gray;
        pixels[offset + 1] = gray;
        pixels[offset + 2] = gray;
      }
    }
  `,
  symbols: {
    grayscale: {
      args: ["ptr", "i32", "i32"],
      returns: "void",
    },
  },
});

const width = 1920;
const height = 1080;
const pixels = new Uint8Array(width * height * 4);
// ... load image data ...

image.symbols.grayscale(ptr(pixels), width, height);

Sorting

const sort = cc({
  source: `
    #include <stdlib.h>
    
    int compare_ints(const void* a, const void* b) {
      return (*(int*)a - *(int*)b);
    }
    
    void sort_array(int* arr, int n) {
      qsort(arr, n, sizeof(int), compare_ints);
    }
  `,
  symbols: {
    sort_array: {
      args: ["ptr", "i32"],
      returns: "void",
    },
  },
});

const arr = new Int32Array([5, 2, 8, 1, 9]);
sort.symbols.sort_array(ptr(arr), arr.length);
console.log(arr); // [1, 2, 5, 8, 9]

Error Handling

Compile Errors

try {
  const lib = cc({
    source: `
      int broken() {
        return "not a number"; // Type error
      }
    `,
    symbols: { /* ... */ },
  });
} catch (err) {
  console.error("Compilation failed:");
  console.error(err.message);
}

Runtime Errors

const lib = cc({
  source: `
    #include <stdio.h>
    #include <errno.h>
    
    int read_file(const char* path) {
      FILE* f = fopen(path, "r");
      if (!f) return -errno;
      fclose(f);
      return 0;
    }
  `,
  symbols: {
    read_file: {
      args: ["cstring"],
      returns: "i32",
    },
  },
});

const result = lib.symbols.read_file("/nonexistent");
if (result !== 0) {
  console.error(`Error code: ${-result}`);
}

Performance

Compilation Speed

TinyCC is extremely fast:
const start = performance.now();
const lib = cc({
  source: `/* 1000 lines of C */`,
  symbols: { /* ... */ },
});
console.log(`Compiled in ${performance.now() - start}ms`);
// Typically < 50ms

Runtime Performance

Compiled code runs at native speed:
const lib = cc({
  source: `
    long fibonacci(int n) {
      if (n <= 1) return n;
      return fibonacci(n-1) + fibonacci(n-2);
    }
  `,
  symbols: {
    fibonacci: {
      args: ["i32"],
      returns: "i64",
    },
  },
});

console.time("C");
const result = lib.symbols.fibonacci(40);
console.timeEnd("C"); // ~500ms

// vs pure JavaScript (much slower)

Limitations

TinyCC Constraints

  • No C++ support (C only)
  • Limited optimization (no -O3)
  • No inline assembly on some platforms
  • Smaller subset of GNU C extensions

Platform Support

  • macOS: x86_64, ARM64
  • Linux: x86_64, ARM64
  • Windows: x86_64 (limited)

Best Practices

  1. Check return values
    const result = lib.symbols.do_something();
    if (result !== 0) {
      throw new Error(`Failed with code ${result}`);
    }
    
  2. Free allocated memory
    const ptr = lib.symbols.allocate(1024);
    try {
      // Use pointer
    } finally {
      lib.symbols.free(ptr);
    }
    
  3. Use const for read-only data
    source: `
      const int MAX_SIZE = 1024;
      void process(const char* input) { }
    `
    
  4. Document ABIs
    symbols: {
      // C: int add(int a, int b)
      add: {
        args: ["i32", "i32"],
        returns: "i32",
      },
    }
    

Debugging

// Enable debug output
process.env.BUN_DEBUG_FFI = "1";

const lib = cc({
  source: `
    #include <stdio.h>
    void debug_info() {
      printf("Debug: called from C\\n");
    }
  `,
  flags: "-Wall -Wextra", // Enable warnings
  symbols: { /* ... */ },
});

Build docs developers (and LLMs) love