tripon[github]

extern "C"

There is exactly one way to reach outside the language: declare a C function in an extern "C" block. No bodies, no generics, no variadic parameters — a declaration, and a promise that the symbol is there at link time.

The shape

tripon
use tier core;

extern "C" {
    fn puts(s: str) -> i32;
    fn putchar(c: i32) -> i32;
}

The block is top-level, next to fn, struct and enum. It may appear more than once, and the order relative to the call does not matter: declarations are collected before any body is checked. The name in the declaration is the C symbol name; there is no rename and no link-name attribute.

Each entry takes the same parameter and return syntax as a Tripon function. A return type of () maps to C void. Calling one is an ordinary call:

tripon
use tier core;

extern "C" {
    fn puts(s: str) -> i32;
}

fn main() -> i32 {
    let wrote = puts("via extern");
    print_i32(wrote);
    return 0;
}
via extern
11

puts printed the string and then returned 11 — the ten characters plus the newline it appended. The empty line is that newline arriving before print_i32 wrote 11. An extern call's return value is a normal Tripon value; you can bind it, compare it, or drop it.

The type mapping

The declaration is emitted as a C prototype with these types:

TriponC
i8 i16 i32 i64int8_t int16_t int32_t int64_t
u8 u16 u32 u64uint8_t uint16_t uint32_t uint64_t
isize usizeintptr_t uintptr_t
f32 f64float double
boolbool
strconst char *
()void
struct, enumits tp_-prefixed typedef

The block above emits:

c
/* ---- extern blocks ---- */
extern int32_t puts(const char *);
extern int32_t putchar(int32_t);

That is the whole bridge. A str is passed as const char *; no length is carried, because Tripon's str does not carry one either.

The prototype must match the real one

The generated C includes the platform headers — stdio.h, stdlib.h, string.h, math.h — before the extern block. If your declaration disagrees with the header's prototype, the C compiler sees two declarations of one symbol and refuses:

ralt build printf.tp
build/printf.c:48:16: error: conflicting types for ‘printf’; have ‘int32_t(const char *)’ {aka ‘int(const char *)’}
   48 | extern int32_t printf(const char *);
      |                ^~~~~~
In file included from build/printf.c:4:
/usr/include/stdio.h:370:12: note: previous declaration of ‘printf’ with type ‘int(const char *, ...)’

printf is variadic, and Tripon has no ... in a parameter list, so there is no correct declaration to write. Use puts or putchar for output, or declare a non-variadic wrapper that your C side provides.

The same thing happens when the types are merely wider or unsigned: fn srand(seed: i32) -> () conflicts with the header's void srand(unsigned int). The fix is to match the real signature, or leave the function alone.

Functions with non-variadic prototypes that already match work without ceremony:

tripon
use tier core;

extern "C" {
    fn fabs(x: f64) -> f64;
    fn fmod(x: f64, y: f64) -> f64;
}

fn main() -> i32 {
    print_f64(fabs(-2.5));
    print_f64(fmod(7.0, 2.0));
    return 0;
}
2.50
1.00

Both symbols come from the C library the build already links. There is no flag in v0 for adding a library or an object file to the link; if the symbol is not in the default link, the call fails at cc and that is the end of it.

Tier

extern is available at kernel, not just core. A kernel-tier file that declares puts and calls it builds:

ralt run kernel_extern.tp
kernel extern

That is deliberate: the lowest tier is exactly where a program needs to talk to the platform. What the tier buys is that nothing else is available.

What this costs

Everything else in Tripon is built to keep a promise: no undefined behaviour, a defined trap for the four arithmetic edges, a span for every diagnostic. An extern call is where that promise stops. The compiler checks the arguments against the declaration, and then the call is a C call:

  • the C function may read past a string you passed;
  • it may return a value that is not in the range you expect;
  • it may not return at all.

The declaration cannot be verified, only declared. Treat an extern block as an audit point: each one is a function whose behaviour is outside the language, and the review question is whether you trust it. The no-UB page covers what is guaranteed everywhere else.

What is refused at check time

writtenresult
call to a name with no declarationE0201 call to unknown function 'puts'
wrong argument countE0202 'puts' takes 1 argument(s), got 0
wrong argument typeE0202, naming both types
an extern declaration with a bodyparse error
variadic ...parse error

The first row is the one that surprises C programmers: there is no implicit libc prelude. A .tp file that calls puts without the block is not linked against libc by accident; it is E0201 at check time. Importing a symbol is a line of source, and that is the point.