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
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:
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:
| Tripon | C |
|---|---|
i8 i16 i32 i64 | int8_t int16_t int32_t int64_t |
u8 u16 u32 u64 | uint8_t uint16_t uint32_t uint64_t |
isize usize | intptr_t uintptr_t |
f32 f64 | float double |
bool | bool |
str | const char * |
() | void |
| struct, enum | its tp_-prefixed typedef |
The block above emits:
/* ---- 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:
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
| written | result |
|---|---|
| call to a name with no declaration | E0201 call to unknown function 'puts' |
| wrong argument count | E0202 'puts' takes 1 argument(s), got 0 |
| wrong argument type | E0202, naming both types |
| an extern declaration with a body | parse 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.