The Tessera Book
Chapter 15

Calling C

Sooner or later a program needs a library that only exists in C: a database engine, a compression library, a system API. Tessera can call C functions directly. This chapter shows how, and also why Tessera treats the permission to do so differently from every other capability: C code runs outside all of Tessera's checks, so the foreign grant is full trust.

A foreign block

A foreign block declares the C functions of one library, by their signatures:

foreign "c" {
    // long strtol(const char *s, char **end, int base)
    fn strtol(s: Str, end: Out[Handle], base: Int32) -> Int
    // size_t strlen(const char *s)
    fn strlen(s: Str) -> Int
}

fn main(root: Root) {
    let c = root.foreign
    let (n, _) = strtol(c, "42 apples", 10)
    let (hex, _) = strtol(c, "ff", 16)
    root.stdout.print("{n} {hex} {strlen(c, "hello")}")
}
Listing 15-1: Calling two functions from the C library examples/ch15-01-strtol/main.tsr
$ tessera run main.tsr --grant foreign=c
tessera: warning: --grant foreign=c is full access: C code can do anything this process can, past every other grant
42 255 5
$ tessera run main.tsr
panic: foreign: library `c` is not granted (grant it with --grant foreign=c)
  --> main.tsr:10:18

foreign "c" names the library: "c" is the C library itself, and foreign "sqlite3" would be libsqlite3. Each function inside is declared with Tessera types that map to C types: Int is int64_t, Int32 is a C int, Float is double, Str is const char *, Handle is void *. The function name is the C symbol.

Calling a foreign function takes the Foreign capability first, before the other arguments: strtol(c, "42 apples", 10). That's what makes C reachable only from code that holds Foreign, and tessera caps lists every function that does.

Out[Handle] is an out-parameter: C writes it, and Tessera returns it after the result, so strtol returns a tuple (Int, Handle) and the caller doesn't pass end at all.

Without the grant, the program still compiles, and the first foreign call stops it:

$ tessera run main.tsr
panic: foreign: library `c` is not granted (grant it with --grant foreign=c)
  --> main.tsr:10:18

Why the foreign grant is different

Every other capability is checked: an Fs refuses paths outside its directory, a Net refuses other hosts. A C function, once called, can do anything the process can: open any file, connect anywhere, read and change the program's memory, including every other capability the program holds. Tessera checks the types you declared, but it can't check that they match the real C function, and a wrong declaration is undefined behaviour.

So the foreign grant is treated as what it is:

  • tessera run prints a warning every time it's given, as you saw above.
  • Only the program can grant it, with --grant foreign=LIB or in its own tessera.toml. A dependency's caps can't include it. A dependency may declare foreign blocks only if the program names it under [trust]:
[dependencies]
zstd = { git = "https://example.com/zstd.git", tag = "v1.0.0" }

[trust]
foreign = ["zstd"]
  • tessera.lock records which C libraries each trusted dependency calls. If an update makes it call a new one, the build stops until you've looked.
  • tessera check lists every part of the program that calls C.
  • The operating-system sandbox (chapter 12) still applies, so C code is limited to roughly what the program was granted.

The right shape for C code is the one the standard library's sql module uses: a small module whose foreign functions are private, wrapped in a safe Tessera API that takes ordinary capabilities. Programs using sql grant an Fs, not foreign, and the module keeps SQLite inside that directory.

Structs, variadic functions and callbacks

Three more things cross the boundary:

foreign "c" {
    // a struct, passed and returned by value
    struct Div { quot: Int32, rem: Int32 }
    fn div(a: Int32, b: Int32) -> Div

    // a variadic function, declared with the arguments this call passes
    fn snprintf(buf: Handle, size: Int, format: Str, ..., n: Int, x: Float) -> Int32

    // a callback: C calls a Tessera function during the call
    fn qsort(base: Handle, n: Int, size: Int, compare: fn(Handle, Handle) -> Int32)

    fn malloc(size: Int) -> Handle
    fn free(p: Handle)
    fn memcpy(dst: Handle, src: Bytes, n: Int) -> Handle
}

// Compares two bytes in C memory; the Foreign capability lets it read them.
fn by_byte(f: Foreign, a: Handle, b: Handle) -> Int {
    f.copy(a, 1)[0] - f.copy(b, 1)[0]
}

fn main(root: Root) {
    let f = root.foreign
    let out = root.stdout

    out.print("{div(f, 17, 5)}")

    let buf = malloc(f, 64)
    let len = snprintf(f, buf, 64, "%lld items at %.2f", 3, 9.5)
    out.print(f.copy(buf, len).to_str())

    let data = "tessera".to_bytes()
    memcpy(f, buf, data, data.len())
    qsort(f, buf, data.len(), 1, by_byte)
    out.print(f.copy(buf, data.len()).to_str())
    free(f, buf)
}
Listing 15-2: A C struct, printf-style formatting and a sort callback examples/ch15-02-more-c/main.tsr
$ tessera run main.tsr --grant foreign=c
tessera: warning: --grant foreign=c is full access: C code can do anything this process can, past every other grant
Div { quot: 3, rem: 2 }
3 items at 9.50
aeersst

Structs by value. A struct inside a foreign block is laid out as C lays it out, with fields of Int, Int32, Float, Bool and Handle. In Tessera it's an ordinary struct you can print and compare.

Variadic functions. C can't tell what a function like snprintf was passed, so the declaration fixes it: the parameters before ... are the function's own, and the ones after are the arguments this declaration passes. To call it with other argument types, declare it again under another name, with = "snprintf" after the signature to name the C symbol.

Callbacks. A parameter of a function type is a pointer to a function C can call. The argument must be a top-level function, named, and it may take a Foreign first, which gives it the same Foreign the call was given, here to read C memory with f.copy. A closure doesn't work, because C calls the function with its arguments alone, and a closure would have nowhere to keep what it captured:

This program does not compile
foreign "c" {
    fn qsort(base: Handle, n: Int, size: Int, compare: fn(Handle, Handle) -> Int32)
}

fn main(root: Root) {
    let reverse = true
    let buf = Handle { addr: 0 }
    qsort(root.foreign, buf, 0, 1, |a, b| if reverse { 1 } else { -1 })
}
Listing 15-3: A closure as a C callback examples/ch15-03-closure-callback/main.tsr
$ tessera check main.tsr
error[E0332]: a callback to C is a top-level function, passed by its name
  --> main.tsr:8:36
   |
 8 |     qsort(root.foreign, buf, 0, 1, |a, b| if reverse { 1 } else { -1 })
   |                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  = note: C calls it with its arguments alone, so it has nowhere to keep a closure's captured values; pass what it needs through C (most C functions that take a callback also take a pointer for it)

1 error

C may call a callback only during the call it was given to. A panic in a callback ends the program, since nothing can unwind through C's frames.

Memory

Arguments are lent to C for the duration of the call: a Str or Bytes pointer is valid only until the call returns, so C must copy what it keeps. Memory C allocates belongs to C, and you free it with the library's own function, as free above. f.copy(p, n) copies n bytes of C memory into Tessera Bytes, which is how results that are a pointer and a length come back.

The interpreter loads libraries with dlopen; native builds link them with -l. Both call the same functions the same way, so a program that calls C runs in either.

Summary

Foreign blocks declare C functions, and calling one takes the Foreign capability. Because C runs outside every check, granting it is full trust: only the program grants it, dependencies need [trust], tessera run warns, and the sandbox still limits the process. Keep C behind a small, safe module, the way sql does.

You now know the whole language. In the last chapter we'll use it to build a small web service.