Binding C Libraries (`@ffi("c")`)
Hale binds external C libraries directly through @ffi("c") function declarations, a small C glue file when needed, and linker flags in hale.toml or on the CLI. No compiler modifications or external code generators are required to call native libraries.
@ffi("c") fn doubler_double(x: Int) -> Int;
fn main() { println(doubler_double(21)); // 42}Declaring Foreign Functions
Section titled “Declaring Foreign Functions”Annotate a bodiless top-level function with @ffi("c") to emit an unmangled LLVM declare symbol with the exact function identifier:
@ffi("c") fn sqlite3_libversion_number() -> Int;@ffi("c") fn raylib_init_window(w: Int, h: Int, title: String);By convention, prefix FFI function names with the target library’s name (raylib_, sqlite3_) to avoid collisions in the flat C symbol namespace.
Signature Restrictions
Section titled “Signature Restrictions”An @ffi("c") declaration must adhere to the C ABI boundary rules:
- No
fallible(E)channel: C functions cannot raise directly on Hale’s error channel. Return an integer or sentinel value from the C shim and wrap it in a regular Hale function that returnsfallible(E)(see Fallible Functions). - No default parameter values or generics: Every parameter must be explicitly typed and passed at each call site.
Type Mapping Across the C Boundary
Section titled “Type Mapping Across the C Boundary”Only portable ABI types may cross an @ffi("c") boundary:
| Hale Type | C ABI Representation | Notes |
|---|---|---|
Int |
int64_t |
Signed 64-bit integer. |
Float |
double |
IEEE-754 64-bit floating point. |
Bool |
int32_t |
0 for false, 1 for true. |
Duration / Time |
int64_t |
Signed 64-bit nanoseconds. |
String |
const char * |
NUL-terminated UTF-8 byte string. |
Bytes |
const void * / void * |
Pointer to [int64_t len][uint8_t payload...]. |
User type |
struct Foo * |
Pointer to a layout-matching C struct (sret when returned by value). |
() |
void |
Valid as a return type only. |
Compiling C Glue and Linking Libraries
Section titled “Compiling C Glue and Linking Libraries”Write any thin C wrapper functions in a standard .c file:
#include <stdint.h>
int64_t doubler_double(int64_t x) { return x * 2;}CLI Build Flags (--csrc and --link)
Section titled “CLI Build Flags (--csrc and --link)”For single-project builds, pass C source files with --csrc and system libraries (-l<name>) with --link:
hale build mydir/ --csrc glue.chale build mydir/ --csrc raylib_glue.c --link raylibPackage Configuration (hale.toml)
Section titled “Package Configuration (hale.toml)”For reusable binding packages, declare the C sources and libraries in hale.toml so downstream consumers do not need to pass CLI flags manually:
[ffi]csrc = ["glue.c"]link = ["raylib"]When an application imports your binding seed, hale build, hale run, hale test, and hale replay compile csrc and pass link flags automatically:
- Deduplication: If a user also passes
--csrc glue.con the CLI for a file already listed inhale.toml, the compiler deduplicates the path and compiles the file once. - Direct-import scope: Only
[ffi]sections from packages directly imported by the entry seed are collected automatically. If your entry imports libraryA, which transitively imports C bindingB, repeatB’s[ffi]entries in the entry’shale.toml(or pass--csrc/--linkon the CLI).
Memory Ownership & Arena Conventions
Section titled “Memory Ownership & Arena Conventions”Memory crossing the C boundary follows strict caller-ownership rules:
- Borrowed input pointers: Every pointer passed from Hale to C (
String,Bytes, or usertypestruct pointer) is owned by the caller’s arena or scratch region and is read-only. The C function must not retain the pointer after returning; if the C library needs to keep the data, it mustmallocand copy the bytes during the call. - Returning heap data to Hale: When a C function needs to allocate a
String,Bytes, or struct to return to Hale, it must allocate directly from the caller’s active arena:Values allocated in the caller’s arena obey Hale’s normal scratch and locus lifetime rules without manualvoid *ptr = lotus_arena_alloc(lotus_caller_arena_or_global(), size, align);freecalls. - No non-local jumps: C++ exceptions and
setjmp/longjmpmust never unwind across an@ffi("c")call boundary.
For the low-level ABI specification—including the sret struct-return convention and exact BytesView header layout—see spec/ffi.md. For packaging conventions when publishing C bindings to Pond, see agents/binding-packages.md.