Table of Contents

FFI — Calling C code

Why call C?

Nyx is powerful, but the world runs on C. Operating systems, graphics libraries, databases, compression algorithms — decades of battle-tested C code exist. Instead of rewriting everything, Nyx lets you call C functions directly. This is called FFI — Foreign Function Interface.

Declaring an external function

To call a C function from Nyx, you declare it with extern "C":

extern "C" fn abs(x: int) -> int

fn main() {
    print(abs(-42))    // 42
    print(abs(10))     // 10
}

This tells the compiler: "there is a C function called abs that takes an int and returns an int. I will provide it at link time."

The C standard library is linked automatically, so functions like abs just work.

Type mapping

When you call C, types must match. Here is how Nyx types map to C:

Nyx type C type LLVM IR
int long long (64-bit) i64
float double (64-bit) double
bool _Bool i1
String const char* i8*
*int int64_t* i64*
i32 int (32-bit) i32
i8 char (8-bit) i8

When you pass a Nyx String to a C function, the compiler automatically converts it to a null-terminated char*.

Calling math functions

extern "C" fn sqrt(x: float) -> float
extern "C" fn pow(base: float, exp: float) -> float
extern "C" fn log(x: float) -> float

fn main() {
    let root: float = sqrt(144.0)
    print(root)    // 12.0

    let result: float = pow(2.0, 10.0)
    print(result)    // 1024.0
}

Link with -lm (the math library) when compiling.

Calling string functions

extern "C" fn strlen(s: String) -> int
extern "C" fn strcmp(a: String, b: String) -> int

fn main() {
    print(strlen("Hello"))     // 5

    let cmp: int = strcmp("apple", "banana")
    if cmp < 0 {
        print("apple comes first")
    }
}

C-compatible structs

To pass structs to C, they must have C-compatible memory layout. Use #[repr(C)]:

#[repr(C)]
struct Point {
    x: i64,
    y: i64
}

extern "C" fn process_point(p: *Point) -> int

fn main() {
    var p: Point = Point { x: 10, y: 20 }
    // Pass pointer to C function
    unsafe {
        let result: int = process_point(&p)
    }
}

Without #[repr(C)], Nyx structs are heap-allocated with GC pointers — not compatible with C. With it, the struct is laid out exactly as C expects.

How the runtime uses FFI

Nyx's own runtime is built on FFI. When you call tcp_listen, thread_spawn, or print, those are C functions in the runtime:

// runtime/net.c
int64_t nyx_tcp_listen(const char* host, int64_t port) {
    // ... create socket, bind, listen
}

The compiler knows about these functions and generates calls to them. Your FFI declarations use the exact same mechanism.

Practical example: calling zlib for compression

extern "C" fn nyx_compress(data: String) -> String
extern "C" fn nyx_decompress(data: String, original_size: int) -> String

fn main() {
    let original: String = "Hello Hello Hello Hello Hello"
    let compressed: String = nyx_compress(original)
    print("Original size: " + int_to_string(original.length()))
    print("Compressed size: " + int_to_string(compressed.length()))

    let restored: String = nyx_decompress(compressed, original.length())
    print("Restored: " + restored)
}

Practical example: getting the time

extern "C" fn time(ptr: int) -> int

fn main() {
    let now: int = time(0)
    print("Unix timestamp: " + int_to_string(now))
}

Safety considerations

FFI is inherently unsafe. C functions can:

Always validate inputs before passing them to C, and test thoroughly. FFI is a bridge between safe Nyx and unsafe C — treat it with care.

Exercises

  1. Declare and call atoi(s: String) -> int from the C standard library to convert a string to an integer. Compare with string_to_int().
  1. Call rand() -> int and srand(seed: int) from C to generate random numbers.
  1. Declare getenv(name: String) -> String and use it to read an environment variable.
  1. Write a program that calls strlen on several strings and compares the results with Nyx's .length().
  1. Create a #[repr(C)] struct representing a 2D rectangle and write a C function (in a .c file) that computes its area. Call it from Nyx.

Summary

Next chapter: Unsafe and raw pointers →

← Previous: LLVM and performance Next: Unsafe and raw pointers →