Lesson 22 of 25 · Larger programs

Working with C

Salam talks to C libraries directly, with no glue code and no wrapper generator. That puts the C standard library and every other library written in C within reach of a Salam program.

extern

Inside an extern: block, declare C functions with their parameter and result types, then call them like any Salam function. The declarations have no body: the code lives in the C library, and Salam only needs to know how to call it.

ffi-extern.salam
extern:
    func sqrt(x: f64): f64
    func abs(x: int): int
end

func main:
    println sqrt(2.0)
    println abs(-42)
end
Output1.4142135623730951 42

The compiler trusts these declarations completely. If a declaration does not match the real C function, for example an int where C expects a double, nothing warns you and the program reads garbage, so copy each signature carefully from the library's header or manual page.

Matching C types

CSalam
int, unsigned intint (i32), uint (u32)
long long, int64_t, uint8_t, ...i64, i64, u8, ...
size_tusize
ssize_t, intptr_tsize
double, floatf64, f32
const char* holding textstr
char* or unsigned char* holding bytesu8*, or void*
void*, T*void*, T*
a function pointervoid*, or extern func (...) R

A Salam str already ends with a zero byte, so it can be passed straight to any C function that wants a string.

printf and other variadic functions

A C function that takes a variable number of arguments, such as printf, is declared with ... after its fixed parameters. The C format rules apply: %d for an int, %f for an f64, %s for a str, and widths such as %-8s and %6.2f for columns:

ffi-printf.salam
extern:
    func printf(format: str, ...): int
    func strlen(s: str): usize
    func atoi(s: str): int
    func toupper(c: int): int
end

func main:
    println "Receipt"
    printf("%-8s %3d x %6.2f\n", "coffee", 2, 3.5)
    printf("%-8s %3d x %6.2f\n", "bagel", 1, 2.25)
    qty := atoi("12") + atoi("30")
    println "items:", qty
    word := "salam"
    println "letters:", strlen(word)
    println "first:", char_from_code(toupper(char_code(word)))
end
OutputReceipt coffee 2 x 3.50 bagel 1 x 2.25 items: 42 letters: 5 first: S

Output from printf and from println comes out in the order the program wrote it, because Salam empties its own output buffer before calling a C function that writes. The %-8s padding counts bytes, not letters, so text in Persian or any other non-Latin script lines up differently; for anything but plain ASCII, println or the fmt package is the better tool.

Pointers, buffers and the zero byte

Many C functions write into memory you give them. Allocate it with mem, pass the pointer, and free it when you are done; defer right after the allocation keeps that from being forgotten. Index a pointer like an array: buf[i] reads or writes one element.

ffi-buffer.salam
import mem

extern:
    func snprintf(buf: void*, size: usize, format: str, ...): int
    func getenv(name: str): str
end

func main:
    buf := mem.AllocateZeroed(32 as u64) as u8*
    defer mem.Free(buf as void*)

    n := snprintf(buf as void*, 32 as usize, "%s-%05d", "INV", 42)
    label := buf as str
    println n, label

    bytes := [72, 105, 0, 33, 33]
    repeat 5 in i:
        buf[i] = bytes[i] as u8
    end
    text := buf as str
    println "as str:", len(text), "bytes"
    mut sum := 0
    repeat 5 in i:
        sum += buf[i] as int
    end
    println "through the pointer:", sum

    value := getenv("SALAM_EXAMPLE_UNSET")
    if (value as void*) == null:
        println "SALAM_EXAMPLE_UNSET is not set"
    end
end
Output9 INV-00042 as str: 2 bytes through the pointer: 243 SALAM_EXAMPLE_UNSET is not set

Watch the middle of that output. A str ends at its first zero byte, so the five bytes H i 0 ! ! turn into the two-byte string Hi, and nothing reports it: the rest is simply gone. Text from C is fine as a str; binary data such as images, compressed files or network packets is not. Keep binary data as a pointer and a length, and read it through the pointer, as the loop that adds up all five bytes does.

A C function that may return no string, such as getenv for a variable that is not set, returns a null pointer. Check it with (value as void*) == null before you use the value. A string returned by C belongs to the C library, so never free it yourself.

Functions from the C standard library need nothing extra. For any other library, tell the linker which one to use with link, written before the extern block. The kind is always written out:

  • link dynamic "sqlite3" links the shared library, like -lsqlite3 in a C build
  • link static "z" asks for the static library libz.a, which is copied into the program
  • link framework "Cocoa" links a macOS framework

A bare link "m" is an error that names the three kinds. Since a framework exists only on macOS, wrap it in a compile-time if, as in this example, which links the C math library:

ffi-link.salam
link dynamic "m"

if SALAM_OS_MAC:
    link framework "CoreFoundation"
end

extern:
    func fmod(x: f64, y: f64): f64
    func hypot(x: f64, y: f64): f64
    func round(x: f64): f64
end

func main:
    println "17 minutes in 5 minute slots leaves", fmod(17.0, 5.0)
    println "diagonal of a 3 by 4 screen:", hypot(3.0, 4.0)
    println "2.5 rounded:", round(2.5)
end
Output17 minutes in 5 minute slots leaves 2 diagonal of a 3 by 4 screen: 5 2.5 rounded: 3

export: Salam functions for C

Some C functions call you back. qsort sorts an array of any type, and asks a function of yours to compare two elements. A Salam function that C has to call goes in an export: block: it is then a real C function under its plain name, and &name is its address, which you pass where C expects a function pointer:

ffi-qsort.salam
import mem

extern:
    func qsort(base: void*, count: usize, width: usize, compare: void*)
end

export:
    func by_score(a: void*, b: void*): int:
        x := (a as i32*)[0]
        y := (b as i32*)[0]
        if x < y:
            ret -1
        else x > y:
            ret 1
        end
        ret 0
    end
end

func main:
    scores := [42, 7, 19, 88, 3]
    n := len(scores)
    p := mem.Allocate((n * 4) as u64) as i32*
    defer mem.Free(p as void*)
    repeat n in i:
        p[i] = scores[i]
    end

    qsort(p as void*, n as usize, 4 as usize, &by_score)

    mut line := ""
    repeat n in i:
        line += " " + p[i]
    end
    println "sorted:" + line
end
Outputsorted: 3 7 19 42 88

The comparison gets two void* pointers to the elements, casts them to i32* and reads them with [0]. The numbers have to sit in memory that C can reach, so the program copies them into a buffer from mem, lets qsort sort them in place, and reads the result back through the pointer.

An exported function is always an ordinary public C symbol, which brings a few rules:

  • every function in the block needs a body; declarations without one belong in extern
  • no variables, no generic functions, and no ... parameters
  • no pub, inline or noinline; only deprecated, pure and noret are allowed
  • the name must be a single word, because it becomes the C name, and is best kept to Latin letters: the LLVM backend cannot emit a non-ASCII export name yet

Calling through a C function pointer

The other direction works too. (&name) as extern func (f64) f64 turns the address of a C function, or of an exported Salam one, into a typed value you can store, pass around and call. extern func (...) R is also a parameter type, so one Salam function can work with any C function of that shape:

ffi-pointer.salam
extern:
    func sqrt(x: f64): f64
    func cbrt(x: f64): f64
end

export:
    func halve(x: f64): f64:
        ret x / 2.0
    end
end

func show(name: str, f: extern func (f64) f64, x: f64):
    println name + "(" + x + ") =", f(x)
end

func main:
    show("sqrt", (&sqrt) as extern func (f64) f64, 64.0)
    show("cbrt", (&cbrt) as extern func (f64) f64, 64.0)
    show("halve", (&halve) as extern func (f64) f64, 64.0)
end
Outputsqrt(64) = 8 cbrt(64) = 4 halve(64) = 32

A plain Salam function name used as a value is its address as an i64, which is what callback tables such as a web router take. For a void* slot, or for a cast to a typed C function, write &name.

Rules of thumb

  • Copy every C signature exactly; the compiler cannot check it.
  • Use str for text and a pointer plus a length for bytes.
  • Free what you allocate with mem, and nothing that C hands back unless its documentation says so.
  • Check pointers that C may return as null before using them.
  • Prefer the Salam standard library when it already covers the job: it does the C calls for you and works the same on every system.

Different systems sometimes need different C functions. The extern block can go inside a top-level if SALAM_OS_WINDOWS:, as shown in Conditional compilation.