Build Directives

Build directives let each module declare its own compilation and linking requirements. Instead of maintaining a separate build script, libraries, flags, and C sources are specified right in the source code. The compiler collects directives from all imported modules and applies them automatically.

The Four Key Directives

DirectivePurpose
@c_include("header.h")Include a C header in the generated code
@cflags("-I...")Add compiler flags (include paths, defines)
@link("libname")Link a system library (-llibname)
@cfile("path.c")Compile a C source file as part of the build

@c_include

Pulls in a C header so you can use its declarations from GX via extern fn:

module mylib

@c_include("mylib.h")
@cflags("-Imodules/mylib/c")

extern fn mylib_init:i32()
extern fn mylib_compute:f32(x:f32)
extern fn mylib_shutdown()

When you import mylib, the generated C file will #include "mylib.h", and the extern declarations let you call those C functions from GX.

@cflags

Passes flags to the C compiler. Most common use: adding include paths so C headers can find each other:

module json

@cfile("../c/yyjson.c")
@cflags("-Imodules/json/c")
@c_include("gx_json.h")

This says: “when compiling this module, add modules/json/c to the include search path, and compile yyjson.c as part of the build.”

You can also define C preprocessor symbols:

@cflags("-DNDEBUG -DCUSTOM_BUILD")

Links a system library. The compiler adds -llibname to the linker command:

module audio

@link("winmm")      // Windows audio library
@link("opengl32")   // Windows OpenGL

When your program is built, these get added to the linker flags. No need to set up anything in a build script — just declare the dependency in the module that needs it.

@cfile

Compiles a C source file as part of the build:

module clay

@cfile("../c/clay_impl.c")
@cflags("-Imodules/clay/c -DCLAY_DISABLE_SIMD")
@c_include("gx_clay.h")

extern fn Clay_SetLayoutDimensions(dims:Clay_Dimensions)
// ... more externs ...

The compiler finds clay_impl.c (path relative to the .gx file containing the directive), feeds it to the C compiler, and links the result. This is how GX modules ship their C implementations alongside the GX bindings.

Platform-Conditional Directives

Combine with #if (@os == ...) to link different libraries on different platforms:

module raylib.core

@c_include("gx_raylib_types.h")
@cflags("-Imodules/raylib/c")

#if (@os == "windows") {
    @compiler("clang")
    @cflags("-DNOGDI -DNOUSER")
    @ldflags("modules/raylib/lib/raylib.lib -lopengl32 -lgdi32 -luser32 -lwinmm -lshell32")
}
#if (@os == "linux") {
    @ldflags("modules/raylib/lib/libraylib.a -lGL -lm -lpthread -ldl -lrt -lX11")
}
#if (@os == "macos") {
    @ldflags("modules/raylib/lib/libraylib.a -framework CoreVideo -framework IOKit -framework Cocoa -framework OpenGL")
}

A single module file handles all platforms. The compiler evaluates the #if conditions at compile time and only applies the matching directives.

@ldflags

Like @cflags but for the linker. Used for library paths and linker-specific flags:

@ldflags("-Lmodules/mylib/lib -lcustomlib")

Or, more commonly, passing an absolute path to a static archive:

@ldflags("modules/raylib/lib/libraylib_web.a")

@compiler

Forces a specific C compiler for this module. Useful when a module’s C code only works with clang or gcc:

@compiler("clang")

This overrides the default (TCC on Windows, cc on macOS) for the whole build.

Directive Propagation

Directives propagate from imported modules to the main program. If you import json and the json module has @cfile("yyjson.c"), that .c file gets compiled as part of your build. You don’t need to know or care about the module’s internal dependencies — just import it and the directives handle the rest.

Practical Example: A Custom Library Module

You’re wrapping a C library called snazzy:

modules/snazzy/c/snazzy.h (your C header) modules/snazzy/c/snazzy.c (your C source) modules/snazzy/gx/snazzy.gx (GX bindings):

module snazzy

@cfile("../c/snazzy.c")
@cflags("-Imodules/snazzy/c")
@c_include("snazzy.h")

#if (@os == "windows") {
    @link("kernel32")
}
#if (@os == "linux") {
    @link("m")
    @link("pthread")
}

extern fn snazzy_init:i32()
extern fn snazzy_process:f32(input:f32)
extern fn snazzy_cleanup()

User code:

import snazzy

fn main() {
    if (snazzy.snazzy_init() != 0) {
        print("init failed\n")
        return
    }

    var result = snazzy.snazzy_process(3.14)
    print("result: {result}\n")

    snazzy.snazzy_cleanup()
}

Build:

gx main.gx -I modules -o myapp

The compiler automatically:

  1. Finds modules/snazzy/gx/snazzy.gx
  2. Compiles ../c/snazzy.c alongside the generated main
  3. Adds -Imodules/snazzy/c to include paths
  4. Links kernel32 (Windows) or m pthread (Linux)
  5. Includes snazzy.h in the generated code

Try it — Create a module that uses @c_include to pull in <math.h> and declares extern fn sinf:f32(x:f32). Call it from main in the Playground (scope dependent on playground capabilities).


Expert Corner

Why directives live in the source: In C projects, build configuration lives in a Makefile, CMakeLists.txt, or similar. When you import a library, you have to update the build file too. With directives in the source, all the info travels with the module — importing a module automatically pulls in its build requirements. One less file to sync, one less place for drift.

Directives vs. pkg-config: Unix systems use pkg-config --cflags --libs foo to get the build flags for library foo. GX directives do the same thing, but declaratively in source. If a future version of a module needs a new flag, update the .gx file — users don’t have to change their build scripts.

Path resolution: Paths in @cfile and @ldflags are relative to the .gx file containing the directive, not the current working directory. This lets modules refer to their own C files without worrying about where the user is building from. The compiler resolves ../c/yyjson.c relative to modules/json/gx/json.gx, so the final path is always correct.

Why @compiler("clang") exists: TCC is fast and zero-dependency, but it lacks modern optimizations. Some libraries (raylib with MSVC’s raylib.lib, for example) require features TCC doesn’t support — incompatible object formats, missing features, etc. The @compiler directive lets a module say “I need clang” without forcing every module in the project to use clang.

Directives are collected, not merged: If two modules both do @cflags("-DFOO"), the final compile line has -DFOO listed twice. This is usually harmless, but if you see duplicate warnings, that’s why. Keep directives minimal and specific to your module.

Debugging build issues: Use gx main.gx --verbose to see every directive collected from every module and the exact emcc/clang/tcc command line that gets executed. This is invaluable for diagnosing link errors, missing libraries, or wrong flags. The directive log shows which module contributed each flag.

Security note: Directives like @cfile and @link are essentially “run this C compiler with these options.” Only import modules from sources you trust — a malicious module could include arbitrary C code via @cfile, or link a backdoored shared library via @link. GX doesn’t sandbox module imports.