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 type | C equivalent |
|---|---|
c_int | int |
c_uint | unsigned int |
c_size | size_t |
c_char | char |
cstr | const 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)fromstdlib.hand 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_intbut C returnsc_long) - Missing or extra parameters
- Wrong parameter types (e.g.
f32instead off64) - Missing
@c_abion 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.