Albite

Calling C

Albite calls C through extern "C" declarations. These have no body — they name a symbol that the linker resolves:

extern "C" fn tg_string_length(s: String) -> Nat

pub extern "C" fn tg_cstr_eq(a: Nat, b: Nat) -> Bool

Once declared, they are called like any other function, and the type checker holds you to the signature you wrote.

Like everything else, an extern declaration is private to its module unless marked pub.

What this is for

The FFI is how Albite reaches its runtime. String operations, allocation, and the primitives underpinning the built-in types are C functions in albite_core, declared this way and linked in. The same mechanism is available to your own code for calling an existing C library.

The caveat worth stating

The signature you write is a promise the compiler cannot check. Nothing verifies that tg_string_length really takes a string and returns a natural number — if the C side disagrees, you get undefined behaviour rather than a type error.

This is the one place where Albite’s guarantees stop. Everything the type checker tells you about pure Albite code rests on the kernel; everything across an FFI boundary rests on you having declared it correctly. Keep the surface small and concentrated in one module, so that the code needing that kind of care is easy to find.

Building strings: StringBuilder

Since 2.0-alpha — the runtime’s string builder, reached through extern "C".

++ copies both of its operands into a new string, so building a string from many pieces with it is quadratic. The runtime carries a growable buffer for that job. It is not a built-in type: it is six C functions that every natively compiled program links, and you reach them the way this page describes — declare them, then wrap them. This is tests/golden/check/docs_string_builder.tg in the compiler repository:

// `StringBuilder` is not a builtin: it is six runtime functions every
// natively compiled program links, declared with `extern "C"` and wrapped.

pub type StringBuilder = MkStringBuilder(Nat)

extern "C" fn tg_string_builder_new() -> Nat
extern "C" fn tg_string_builder_with_capacity(capacity: Nat) -> Nat
extern "C" fn tg_string_builder_push_str(sb: Nat, s: String) -> Nat
extern "C" fn tg_string_builder_push_char(sb: Nat, code: Nat) -> Nat
extern "C" fn tg_string_builder_len(sb: Nat) -> Nat
extern "C" fn tg_string_builder_to_string(sb: Nat) -> String

pub fn string_builder_new() -> StringBuilder {
    MkStringBuilder(tg_string_builder_new())
}

pub fn push_str(sb: StringBuilder, s: String) -> StringBuilder {
    match sb { MkStringBuilder(h) => MkStringBuilder(tg_string_builder_push_str(h, s)) }
}

// Consumes the builder: any later use of it aborts.
pub fn to_string(sb: StringBuilder) -> String {
    match sb { MkStringBuilder(h) => tg_string_builder_to_string(h) }
}

type Words = NoWords | Word(String, Words)

fn push_all(sb: StringBuilder, ws: Words) -> StringBuilder {
    match ws {
        NoWords => sb,
        Word(w, rest) => push_all(push_str(sb, w), rest),
    }
}

fn main() -> String {
    let words = Word("build ", Word("once, ", Word("copy never", NoWords)));
    to_string(push_all(string_builder_new(), words))
}

albite run prints "build once, copy never"; the compiled binary prints it unquoted. The four functions the example does not wrap — with_capacity(bytes), push_char(code point) and len (in bytes) — follow the same pattern.

What the wrapper is doing, and why:

  • The handle is wrapped in a one-constructor type, not left as a Nat, so that arithmetic on it is a type error.
  • Every push returns the same builder, so a builder threads through a fold or a recursive function as an accumulator, as push_all does.
  • to_string consumes the builder. It hands the buffer over as the String without copying, and the builder is dead afterwards: any later call on it prints one line naming it and aborts the program.
  • When to reach for it: whenever a string is assembled from more than a handful of pieces. Measured on 15,000 pushes of a 44-byte piece, the builder was 384 times faster than the same loop over ++.

The compiler’s own copy of this wrapper is src/compiler/driver/ffi/builder/mod.tg.