C Interop

GX compiles to C, so interop is seamless. You can call any C function, use any C struct, and work with any C enum — with full type safety and zero overhead. This is how GX binds to raylib, SDL, OpenGL, and any other C library.

Declaring a C Function

Use extern fn to declare a function that exists in a C library or header:

@c_include("math.h")

extern fn sinf:f32(x:f32)
extern fn cosf:f32(x:f32)
extern fn sqrtf:f32(x:f32)

fn main() {
    var angle:f32 = 0.5
    print("sin({angle}) = {sinf(angle)}\n")
    print("cos({angle}) = {cosf(angle)}\n")
    print("sqrt(2.0) = {sqrtf(2.0)}\n")
}

extern fn tells GX: “this function exists in C code we’ll link against, here’s its signature, trust me.” The @c_include directive brings in the C header that actually declares sinf etc.

Declaring a C Struct

Use extern struct to mirror a C struct layout so GX code can access its fields:

@c_include("raylib.h")

extern struct Color {
    r:u8
    g:u8
    b:u8
    a:u8
}

extern struct Vector2 {
    x:f32
    y:f32
}

extern fn DrawCircle(centerX:c_int, centerY:c_int, radius:f32, color:Color)

The field order and types MUST match the C struct exactly — otherwise you’ll read the wrong bytes. Check the C header and copy the fields in order.

Declaring a C Enum

Use extern enum for C enum types:

@c_prefix("KEY_")
extern enum KeyboardKey {
    SPACE = 32
    A = 65
    B = 66
    C = 67
    // ...
}

fn main() {
    var k = KeyboardKey.A
    print("key A = {k}\n")
}

The @c_prefix("KEY_") tells GX that the C names are KEY_SPACE, KEY_A, etc. — so when the compiler emits the reference, it prepends the prefix. In GX code you just write KeyboardKey.A or, since enum members are globally available, just KEY_A directly.

C ABI Types

GX has special type names that match C’s platform-dependent types:

GX typeC equivalent
c_intint
c_uintunsigned int
c_sizesize_t
c_charchar
cstrconst char* (null-terminated)

Use these when binding to C functions that take these types:

@c_include("stdio.h")

extern fn strlen:c_size(s:cstr)
extern fn printf:c_int(format:cstr, ...)

fn main() {
    var s = "hello"
    var n = strlen(s.cstr)    // .cstr extracts the C pointer from a GX str
    print("length = {n}\n")
}

Note the .cstr accessor: GX strings are fat (pointer + length), so to pass them to a C function expecting const char*, you access the .cstr field.

Complete Example: Binding sinf and cosf

@c_include("math.h")

extern fn sinf:f32(x:f32)
extern fn cosf:f32(x:f32)

fn main() {
    var angle:f32 = 0.0
    while (angle < 6.28) {
        var y = sinf(angle)
        var x = cosf(angle)
        print("angle={angle}, x={x}, y={y}\n")
        angle = angle + 0.5
    }
}

Build and run:

gx circle.gx -o circle
./circle

Works because math.h is always available — no extra linking needed on most platforms (the C runtime provides these functions).

Variadic Functions

For C variadic functions like printf, use ... in the parameter list:

@c_include("stdio.h")

extern fn printf:c_int(format:cstr, ...)

fn main() {
    var x:i32 = 42
    var y:f32 = 3.14
    printf("int: %d, float: %f\n", x, y)
}

Opaque Pointer Types

Sometimes a C library returns a pointer to a struct whose internals you don’t care about. Use *void for opaque handles:

@c_include("stdio.h")

extern fn fopen:*void(path:cstr, mode:cstr)
extern fn fclose:c_int(file:*void)
extern fn fputs:c_int(s:cstr, file:*void)

fn main() {
    var f = fopen("hello.txt", "w")
    if (f != 0) {
        fputs("Hello from GX!\n", f)
        fclose(f)
        print("wrote file\n")
    }
}

The FILE* handle is just *void to GX — you pass it around but never look inside.

Callbacks to GX Functions

To let a C library call back into your GX code, mark your function @c_abi:

@c_abi
fn my_callback(x:i32) {
    print("callback called with {x}\n")
}

@c_include("stdlib.h")

extern fn atexit:c_int(func:fn())

fn main() {
    // Register a C-callable cleanup
    // (simplified — real atexit signature differs)
    print("main running\n")
}

@c_abi makes the GX function use C’s calling convention so a C library can call it safely.

Practical Example: Calling a C Math Library

Suppose you want to use strtod from stdlib.h:

@c_include("stdlib.h")

extern fn strtod:f64(s:cstr, endp:*void)

fn main() {
    var input = "3.14159"
    var result = strtod(input.cstr, 0)
    print("parsed: {result}\n")
}

No wrappers, no bindings file — just declare the function and call it.

Try it — Declare extern fn abs:c_int(x:c_int) from stdlib.h and call it with a negative number in the Playground.


Expert Corner

How extern generates C: An extern fn foo:T(...) produces a forward declaration in the generated C if no @c_include covers it, or is relied on from an included header. No code is generated — the compiler just trusts your declaration and emits calls. Getting the signature wrong leads to undefined behavior at runtime (stack corruption, wrong return values, crashes). Always double-check against the C header.

Extern struct layout: extern struct tells the compiler “don’t emit a definition for this, use whatever the C header says.” You’re declaring the layout for type-checking purposes. The fields must be in the correct order and have compatible types — GX doesn’t check against the actual C header, it takes your word for it.

Why cstr vs str: GX’s str is a fat pointer (16 bytes: pointer + length). C’s const char* is just a pointer (8 bytes). They’re NOT interchangeable. The .cstr field on a GX string extracts the underlying pointer for C calls. Going the other way (C string to GX str) requires ec_str_make(c_ptr) which walks the string to compute the length.

@c_abi for GX callbacks: Normal GX functions may use any calling convention the C compiler picks — usually the platform default, but with optimizations the compiler might inline or use custom calling conventions. Adding @c_abi locks the function to the standard C ABI so C code can call it reliably. Use this on any function whose address you pass to a C library as a callback.

No wrapper layer: Unlike Python bindings (CFFI, ctypes) or Java JNI, there’s no marshaling layer between GX and C. A GX i32 IS a C int32_t. A GX f32 IS a C float. A GX struct IS the same bytes as the C struct. Calling a C function has zero overhead — it’s just a function call.

Comparison to Rust bindings: Rust requires unsafe { ... } blocks for every C call, explicit #[repr(C)] attributes on structs, and often a build.rs script to link libraries. GX is simpler: declare extern fn, use @link, done. The tradeoff: GX doesn’t prove safety at the extern boundary — you’re trusted to get the C signature right.

Debugging wrong signatures: If your program crashes right after calling a C function, the most likely cause is a mismatched extern declaration. Common mistakes:

  • Wrong return type (e.g. you wrote c_int but C returns c_long)
  • Missing or extra parameters
  • Wrong parameter types (e.g. f32 instead of f64)
  • Missing @c_abi on a callback

Fix the declaration, recompile, the crash goes away.

When to use @c_include vs declaring everything manually: If the C library has a clean header, use @c_include and let the C compiler handle resolution. If you need just one or two functions from a big header (to avoid macro pollution, symbol conflicts, or slow compilation), declare them manually without @c_include — the compiler will emit forward declarations in the generated C.