Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Native C Imports (FFI)

Pith scripts import C source directly. The exported (non-static) functions become callable through the unit’s namespace, with the compiler handling type conversions and ARC at every boundary.

Syntax

import "ffi/math.c"

sum = math.addInts(3, 4)
msg = math.greet("world")     # owned string return, ARC-released
math.logNote(42)              # void call as a statement

The namespace is the file’s basename sans .c, ffi/math.c becomes math.

How it works

JIT (in-memory)

Each imported unit compiles into its own libtcc state (per-state isolation prevents symbol collisions between imports). The pre-baked <pith.h> virtual header is staged into a temp include dir, and the runtime symbols (pithRetain, pithNewString, etc.) are registered into the import’s state before compilation. After relocation, the exported function addresses are linked into the main execution state under their real symbol names.

AOT (standalone binary)

Each unit compiles to an object file via libtcc TCC_OUTPUT_OBJ (or the platform C compiler when libtcc is absent). The object joins the QBE-generated .o and runtime/libruntime.a at link time.

Codegen

The compiler emits a private namespaced shim per foreign function with the typed System V AMD64 / AAPCS64 signature:

function s $c_math_halfFloat(s %a0) {
@c_math_halfFloat.start
    %r =s call $halfFloat(s %a0)
    ret %r
}

Call sites emit typed calls through the shim, with widening/narrowing conversions at the boundary.

Type mapping

C typeQBE typePith type
int, enum, bool, int32_twinteger (widened via extsw)
long, int64_t, size_t, T*linteger or string
floatsfloat (narrowed via truncd)
doubledfloat
void,(statement only)
PithValue*lstring (owned, +1 reference)

The prototype scanner (src/cffi.c) discovers non-static function definitions at compile time, parsing return types and parameter lists from the C source. Multi-line signatures, pointer params, and (void) parameter lists are handled.

ABI contract

Imported C code includes <pith.h> and follows these rules:

  • Parameters are borrowed, the callee must call pithRetain() before storing a PithValue* beyond the call, and owns that extra reference afterwards.
  • Returns must be +1, a function returning PithValue* must return a new reference; the Pith compiler injects the matching release at scope boundaries.
  • Handoffs are leak-free, the compiler’s ASan-verified test suite confirms zero leaks across the FFI boundary.

Writing an import module

#include <pith.h>
#include <stdio.h>

/* 32-bit ints: w */
int addInts(int a, int b)
{
    return a + b;
}

/* borrowed string param */
long stringLength(PithValue *s)
{
    return (long)pithStringLength(s);
}

/* owned string return: +1 reference */
PithValue *greet(PithValue *name)
{
    char buf[256];
    snprintf(buf, sizeof(buf), "hello, %s!", pithStringData(name));
    return pithNewString(buf);
}

/* void: call as a statement */
void logNote(int code)
{
    fprintf(stderr, "note: %d\n", code);
}

Runtime helpers available to imports

From pith.h:

FunctionDescription
pithRetain(PithValue*)Atomically bump the refcount
pithRelease(PithValue*)Drop a ref; frees at zero
pithNewString(const char*)Allocate a string (+1 reference)
pithNewStringN(const char*, uint32_t)Allocate with explicit length
pithStringData(PithValue*)Borrowed payload pointer
pithStringLength(PithValue*)Payload byte length
pithStringEquals(PithValue*, PithValue*)Content equality
pithStringConcat(PithValue*, PithValue*)New concatenated string (+1)