# Chapter 19: `@unsafe` and calling C

Every check in chapters 15 to 18 stops at the edge of what the compiler can see. C libraries, inline assembly and claims about thread safety that rest on a protocol the compiler cannot read are on the other side. Loup marks that edge with one annotation, `@unsafe("reason")`, whose reason string is mandatory, so an audit of every unchecked assertion in a code base is one `grep`.

This chapter covers `@unsafe` and its statement form, declaring C functions with `extern native`, passing structs across the boundary, wrapping C resources in classes, inline assembly in `host.native`, and the header tools. The normative text is in the Language Reference §3-2.4.6 (`@unsafe`), §2-3.4 (`extern native`) and §3-2.7 (headers).

Two facts shape everything below:

- **C calls need a native build.** `loup run` type-checks `extern native` declarations and calls, but its interpreter cannot call into C. Build with `loup build --target native` (chapter 24) to run FFI code.
- **The FFI library is planned.** The specified helpers in `native.ffi` (`OwnedHandle`, `BorrowedHandle`, `PinnedRef`, pointer arithmetic) and `native.mem` are not in the 1.25.1 standard library. What ships is the language part: `extern native`, `NativeHandle`, `@repr(C)` and `@unsafe`.

## `@unsafe` and its reason string

`@unsafe` takes one string: a category, a colon, and the assertion being made. The category tells the compiler which check the annotation answers for, and tells a reviewer what kind of claim it is:

| Category | Asserts | Section |
|---|---|---|
| `threadSafe:` | a C function may be called from any thread | §2-3.5 |
| `escapingBorrow:` | a pointer given to C outlives the call | §3-2.4.6 |
| `sendable:` | a class is safe to share across threads although its fields do not prove it | §2-1.8.1 |
| `asm:` | an inline assembly block is correct | §3-3.7 |
| `distinct:` | arguments do not overlap (for `requires distinct`, planned in chapter 15) | §3-2.4.10 |
| `overlay:`, `mmio:`, `init:`, `transmute:` | raw memory claims, mostly used in freestanding code | §3-3 |

It comes in three forms:

- an **annotation on a declaration**: `@unsafe("threadSafe: …")` before an `extern native func`, a `class` or a `func`;
- a **statement region**: `unsafe("reason") { … }` around statements in any function body;
- an **annotation on one statement**: `@unsafe("…")` on the line before a call or an `asm` block, which is shorthand for a one-statement region.

The audit is a single search that finds all three:

```sh
grep -rnE '@?unsafe\(' src/
```

### Mistake: an `@unsafe` without a reason

```loup,does_not_compile
@mode(host.native)

@unsafe
extern native "c" func getpid(): int32;

func main(): void {
    println("declared");
}
```

```text
main.lphc:3:1: error[E_UNSAFE_NO_REASON]: E_UNSAFE_NO_REASON: `@unsafe` requires a non-empty reason string — `@unsafe("<category>: <reason>")`
```

An empty string, `@unsafe("")`, is rejected the same way, and so is a statement region `unsafe("") { … }`, although its message is less polished (`… has empty reason string (E_UNSAFE_NO_REASON)`). Write what you are asserting: `@unsafe("threadSafe: getpid only reads process state")`.

The single-purpose annotations that `@unsafe` replaced are hard errors that name their replacement:

```loup,does_not_compile
@mode(host.native)

@threadSafe
extern native "c" func getpid(): int32;

func main(): void {
}
```

```text
main.lphc:3:1: error[E_RETIRED_ANNOTATION]: E_RETIRED_ANNOTATION: `@threadSafe` is retired — use `@unsafe("threadSafe: <reason>")` instead (§3-2.4.6 consolidates the ad-hoc safety annotations into one auditable surface)
```

`@escapingBorrow` and `@unsafeEscapingBorrow` are reported the same way. Note the spelling of the replacement: `@unsafe("…")` takes the string directly, not `@unsafe(reason: "…")`.

## Opting a class out of the `Sendable` check

A class that declares `implements Sendable` may only have `var` fields of type `Mutex[T]`, `RwLock[T]` or `Atomic[T]` (chapter 20). That rule rejects a class that is safe for a reason the compiler cannot see, such as a configuration that is filled before any thread starts and never written again:

```loup,does_not_compile
@mode(host.native)

class Settings implements Sendable {
    var entries: list[string] = [];
    pub modifier func add(e: string): void { this.entries.push(e); }
    pub func count(): int { return this.entries.length(); }
}

func main(): void {
    let s = Settings();
    safe { s.add("verbose"); }
    let t = Thread.spawn(func(): void { println(s.count()); });
    t.join();
}
```

```text
main.lphc:4:5: error[E_SENDABLE_UNPROTECTED_VAR]: var field 'entries' on Sendable class 'Settings' must be of type Mutex[T], RwLock[T], or Atomic[T], not 'list'
```

If the field really is only written before the object is shared, say so, and the compiler accepts the claim:

```loup
#~ @mode(host.native)
#~
@unsafe("sendable: entries is filled before the object is shared and never written afterwards")
class Settings implements Sendable {
    var entries: list[string] = [];
    pub modifier func add(e: string): void { this.entries.push(e); }
    pub func count(): int { return this.entries.length(); }
}
#~
#~ func main(): void {
#~     let s = Settings();
#~     safe { s.add("verbose"); }
#~     let t = Thread.spawn(func(): void { println(s.count()); });
#~     t.join();
#~ }
```

*Listing 19-1: A `sendable:` assertion on a class*

```text
1
```

The compiler now trusts you. If something does write `entries` from a thread later, the result is a data race that no check will report, which is why the reason has to be specific enough for a reviewer to verify. Prefer a `synchronized class` or `Mutex`/`Atomic` fields (chapters 15 and 20) whenever they fit; `@unsafe` is for the cases they do not cover.

## Declaring C functions

`extern native` declares a function that lives in a C library. This program calls two of them, and running it with `loup run` shows the first thing to know about FFI code:

```loup,throws
@mode(host.native)

extern native "c" func getpid(): int32;
extern native "c" func strlen(s: string): int;

func main(): void {
    println(i"pid is positive: {$ getpid() > 0 $}");
    println(strlen("hello"));
}
```

*Listing 19-2: Calling two C library functions*

```text
Uncaught RuntimeError: internal interpreter limitation: unsupported instruction: CallExternNative
```

The program compiled: the declarations and calls were type-checked. Then the interpreter reached the first C call, which it cannot make. Build the program instead. On Linux x86-64, or on macOS with the LLVM backend:

```sh
loup build --target native -o pid
./pid
```

```text
pid is positive: true
5
```

Use `loup run` to check FFI code for type errors while you write it, and a native build to run it.

The string after `native` names the library (`"c"` for the C library; `"sqlite3"` for `libsqlite3.so` / `.dylib`). The parameter and return types are Loup types that the compiler maps to the C ABI:

| Loup | C |
|---|---|
| `int8` … `int64`, `uint8` … `uint64` | the integer type of the same width |
| `int` | `int64_t` (`long` on 64-bit Unix) |
| `float32`, `float` | `float`, `double` |
| `bool` | `bool` |
| `string` (parameter) | `const char *` to a NUL-terminated UTF-8 copy that lives for the duration of the call |
| `NativeHandle` | any pointer, treated as opaque |
| a `@repr(C)` struct | the C struct of the same layout |

`extern native` declarations are accepted in `host.native`, `host.dual` and `freestanding` files. In a script they are `E_EXTERN_NATIVE_WASM: 'extern native' is not available in 'web.scripted', 'web.compiled', or 'host.scripted' modes; use 'host.native', 'host.dual', or 'freestanding'`.

### Structs across the boundary

A struct passed to or returned from a C function must have C layout, which `@repr(C)` guarantees: fields in declaration order, each at its C alignment. The compiler refuses a struct without it:

```loup,does_not_compile
@mode(host.native)

struct Point {
    var x: float;
    var y: float;
}

extern native "geo" func geo_distance(a: Point, b: Point): float;

func main(): void {
    println("declared");
}
```

```text
main.lphc:8:1: error[E_EXTERN_STRUCT_NOT_REPR_C]: E_EXTERN_STRUCT_NOT_REPR_C: struct `Point` crosses the `extern native` boundary of `geo_distance` in host.native mode without `@repr(C)` — a struct passed to or returned from an `extern native` function must carry `@repr(C)` so its layout matches the C ABI (§2-3.4)
main.lphc:8:1: error[E_EXTERN_STRUCT_NOT_REPR_C]: E_EXTERN_STRUCT_NOT_REPR_C: struct `Point` crosses the `extern native` boundary of `geo_distance` in host.native mode without `@repr(C)` — a struct passed to or returned from an `extern native` function must carry `@repr(C)` so its layout matches the C ABI (§2-3.4)
```

The error appears once for each parameter of type `Point`. Without the annotation the compiler is free to lay the struct out as it likes, and the C side would read the wrong bytes. Add it:

```loup
#~ @mode(host.native)
#~
@repr(C)
struct Point {
    var x: float;
    var y: float;
}

extern native "geo" func geo_distance(a: Point, b: Point): float;
#~
#~ func main(): void {
#~ }
```

*Listing 19-3: A `@repr(C)` struct passed by value to C*

> ⚠ **Planned.** Pointer types such as `*mut T` and the `&mut x` address-of operator that earlier editions used for C out-parameters are not part of the language. Today a C function that writes through a pointer is declared with a `NativeHandle` parameter, and the specified `native.ffi` helpers for obtaining such pointers safely are not in 1.25.1.

### Wrapping a C resource in a class

C hands back resources as pointers that must be released exactly once. The Loup way is a class that owns the `NativeHandle` and releases it in `finalize()`, so the reference count decides when the C resource is freed. A wrapper for a small C library that keeps a running total:

```loup
#~ @mode(host.native)
#~
extern native "tally" func tally_new(): NativeHandle;
extern native "tally" func tally_add(t: NativeHandle, n: int): void;
extern native "tally" func tally_total(t: NativeHandle): int;
extern native "tally" func tally_free(t: NativeHandle): void;

class Tally {
    let handle: NativeHandle;

    new() {
        this.handle = tally_new();
    }

    pub func add(n: int): void {
        tally_add(this.handle, n);
    }

    pub func total(): int {
        return tally_total(this.handle);
    }

    func finalize(): void {
        tally_free(this.handle);
    }
}
#~
#~ func main(): void {
#~ }
```

*Listing 19-4: A class that owns a C handle*

Users of `Tally` never see the handle, cannot free it twice, and cannot use it after it is freed: the object holds the only copy, and `finalize()` runs once, when the last reference goes away (or at `del`, chapter 9). Making the class `unique` (chapter 17) goes further and rules out sharing the handle at all.

`NativeHandle` is deliberately opaque. It has no members, no literal and no operators, not even `==`; values come only from `extern native` returns and go only into `extern native` parameters, fields and bindings. One consequence is that there is no tidy way yet to test a returned pointer for NULL. Prefer C APIs that report failure through a return code, and keep such checks inside small wrappers like this one until the `native.ffi` helpers land.

### Pointers that outlive the call

A C function that stores a pointer it is given (a callback registry, a stream buffer) must say so at its declaration with an `escapingBorrow:` assertion. Every call site must then acknowledge the escape:

```loup,does_not_compile
@mode(host.native)

@unsafe("escapingBorrow: setvbuf keeps using buf until the stream is closed")
extern native "c" func setvbuf(stream: NativeHandle, buf: NativeHandle, mode: int32, size: uint64): int32;

func useBuffer(stream: NativeHandle, buf: NativeHandle): void {
    setvbuf(stream, buf, 0, 4096);
}

func main(): void {
}
```

```text
main.lphc:7:5: error[E_ESCAPING_BORROW_UNACKNOWLEDGED]: E_ESCAPING_BORROW_UNACKNOWLEDGED: call to 'setvbuf' requires a call-site @unsafe("escapingBorrow: ...") acknowledgment — the declaration asserts the borrowed argument escapes the native call
```

The acknowledgement records, at that call, why the pointer lives long enough:

```loup
#~ @mode(host.native)
#~
#~ @unsafe("escapingBorrow: setvbuf keeps using buf until the stream is closed")
#~ extern native "c" func setvbuf(stream: NativeHandle, buf: NativeHandle, mode: int32, size: uint64): int32;
#~
func useBuffer(stream: NativeHandle, buf: NativeHandle): void {
    @unsafe("escapingBorrow: buf is a module-level buffer that lives as long as the process")
    setvbuf(stream, buf, 0, 4096);
}
#~
#~ func main(): void {
#~ }
```

*Listing 19-5: Acknowledging an escaping borrow at the call site*

An enclosing `unsafe("escapingBorrow: …") { … }` region also counts, but only with the matching category: a region that says `threadSafe:` does not acknowledge an escaping borrow.

## Inline assembly in `host.native`

`asm { }` embeds machine instructions. It is allowed in `host.native` and `freestanding` files; in `host.dual` it is `E_ASM_NOT_COMPILED` (chapter 2). The specification requires an `asm:` assertion in `host.native`. A timestamp-counter read on x86-64:

```loup,ignore
@mode(host.native)

func cycles(): uint64 {
    var lo: uint32 = 0;
    var hi: uint32 = 0;
    @unsafe("asm: rdtsc only reads the time-stamp counter")
    asm {
        "rdtsc"
        out: lo = "=a", hi = "=d"
    }
    return ((hi as uint64) << 32) | (lo as uint64);
}

func main(): void {
    println(cycles() > 0);
}
```

*Listing 19-6: Reading the x86-64 time-stamp counter (x86-64 native builds only)*

`out:` binds output registers to local variables using GCC/LLVM constraint strings; `in:` passes values in, and `clobbers: [...]` lists what the instructions destroy (§3-3.7). `unsafe("asm: …") asm { … }` is the same thing written as a statement region.

This listing is not checked by the book's tooling: it is specific to x86-64, and the interpreter cannot run assembly (`loup run` stops with `internal interpreter limitation: unsupported instruction: InlineAsm`). Prefer library code where it exists: `native.atomic` ships the x86-64 barriers `hwFence()`, `hwFenceAcquire()`, `hwFenceRelease()` and `compilerFence()`.

> **Known gap.** Stage0 1.25.1 accepts an `asm` block in `host.native` without the `@unsafe("asm: …")` assertion. Write it anyway; the audit depends on it.

## Generating bindings and headers

The specification describes two tools (§3-2.7). `loup import-headers` reads a C header and writes `extern native` declarations; `loup export-headers` reads a Loup module and writes a C header for its `pub func` declarations:

```sh
loup import-headers /usr/include/zlib.h --lib z -o src/zlib.lphc
loup export-headers src/checksum.lphc -o include/checksum.h
```

They ship only in the Linux x86-64 toolchain. Elsewhere `loup` prints `` `loup import-headers` is not yet available in this release `` and exits. The generated header describes Loup functions, but `--lib shared` does not yet produce a native library to link them from (chapter 24), so calling Loup from C is ⚠ planned. The `@cexport` annotation that the specification uses to control exported names is not recognized by 1.25.1 (`warning: Unknown annotation '@cexport'`).

## Summary

- `@unsafe("category: reason")` marks every assertion the compiler cannot check; a missing reason is `E_UNSAFE_NO_REASON`, and the retired `@threadSafe`/`@escapingBorrow` are `E_RETIRED_ANNOTATION`. `unsafe("reason") { … }` is the statement form.
- `@unsafe("sendable: …")` lets a class claim `Sendable` despite unprotected `var` fields (`E_SENDABLE_UNPROTECTED_VAR` otherwise).
- `extern native "lib" func …` declares C functions; strings are passed as NUL-terminated copies, pointers as opaque `NativeHandle`s, and structs must be `@repr(C)` (`E_EXTERN_STRUCT_NOT_REPR_C`).
- C calls and `asm` blocks run only in native builds; `loup run` type-checks them and stops at the call.
- Wrap C resources in a class (or `unique class`) whose `finalize()` releases them.
- A C function that keeps a pointer is declared `escapingBorrow:`, and each call site acknowledges it (`E_ESCAPING_BORROW_UNACKNOWLEDGED`).
- `native.ffi`, `native.mem`, pointer types, `@cexport` and native library output are ⚠ planned; the header tools exist on Linux x86-64 only.
- Chapter 20 covers OS threads and the `Sendable` rules in full.
