C FFI
extern function declares a function implemented in C. Sun calls it directly — no
wrapper layer, no marshalling cost beyond what the C ABI itself requires.
extern "C" function abs(x: i32) i32;
function main() i32 {
unsafe { return abs(-42); };
}Calling an extern requires an unsafe block. C code is outside everything the
type system and borrow checker guarantee, so the boundary is marked the same
way the equivalent intrinsics (_malloc, _free) already are.
Declaring
The ABI string is optional and only "C" is supported:
extern function puts(s: raw_ptr<u8>) i32; // same as extern "C"
extern "C" function puts(s: raw_ptr<u8>) i32;A return type is required — there is no inference without a body. Use void when
the C function returns nothing.
Declarations are hoisted, so an extern may be used before it appears in the file, and may live inside a module:
public module libc {
public extern "C" function abs(x: i32) i32;
}
using libc;Renaming a symbol
as "symbol" binds a Sun-side name to a different C symbol. Useful when the C
name would collide or reads poorly:
extern "C" function c_strlen(s: raw_ptr<u8>) i64 as "strlen";Only the Sun-side name (c_strlen) comes into scope; the C name does not.
as is only a keyword inside an extern declaration, so it remains usable as an
ordinary identifier elsewhere.
Type mapping
| C | Sun |
|---|---|
int, long, char, float, double | i32, i64, i8, f32, f64 |
T*, void* | raw_ptr<T>, raw_ptr<u8> |
struct T* | ref T |
struct T (by value) | T |
struct T (returned) | T |
void | void |
Class layout matches C: fields in declaration order with the platform's natural padding. A Sun class and the C struct it mirrors have identical layout, so no annotation is needed to keep them in sync.
Types with no C spelling — arrays and slices (fat pointers), interfaces (vtable pairs), lambdas (closures) — are rejected at the declaration rather than miscompiled.
Strings
A string literal is a static_ptr<u8>, a { pointer, length } pair. Passing one
where a raw_ptr<u8> is expected narrows it to the data pointer automatically:
extern "C" function strlen(s: raw_ptr<u8>) i64;
function main() i32 {
unsafe { return strlen("hello"); }; // 5
}Sun string literals are NUL-terminated, so they are safe to hand to C. When the C
function wants the pointer and the length separately, take them apart with
s.raw() and s.length():
extern "C" function write(fd: i32, buf: raw_ptr<u8>, n: i64) i64;
function main() i32 {
var s: static_ptr<u8> = "hello\n";
unsafe { write(1, s.raw(), s.length()); };
return 0;
}Structs
Pass a pointer with ref, which is exactly C's T*:
struct TS { long sec; long nsec; };
void fill(struct TS* t);class TS {
var sec: i64;
var nsec: i64;
}
extern "C" function fill(t: ref TS) void;
function main() i32 {
var t: TS = { sec: 0, nsec: 0 };
unsafe { fill(t); };
return t.sec + t.nsec;
}Structs by value work too, in both directions. The compiler applies the target's C argument classification (x86-64 System V, or AAPCS64 when compiling for AArch64), so a small struct travels in registers and a large one through memory, matching what a C compiler emits:
class Pair { var a: i32; var b: i32; }
extern "C" function take_pair(p: Pair) i32;
extern "C" function make_pair(a: i32, b: i32) Pair;
function main() i32 {
var p: Pair = { a: 3, b: 7 };
unsafe { return take_pair(p); };
}Returning a ref is not supported: Sun's ref return auto-dereferences, which
has no C equivalent. Use raw_ptr<T> to return a pointer.
Integer widths
C's int is i32. A value computed as i64 does not narrow on its own, so
convert it at the call: _convert<i32>(x) when it is known to fit, or
safe_convert<i32>(x) from the stdlib to get a ConversionError when it does
not.
extern "C" function poll(fds: raw_ptr<u8>, nfds: i64, timeout: i32) i32;
function wait_for(fds: raw_ptr<u8>, interval_ms: i64) i32 {
unsafe { return poll(fds, 1, _convert<i32>(interval_ms)); };
}Varargs
A trailing ... declares a C-variadic function. Arguments past the named
parameters get C's default promotions — f32 widens to double, integers
narrower than int widen to int, and string literals narrow to their data
pointer:
extern "C" function printf(fmt: raw_ptr<u8>, ...) i32;
function main() i32 {
unsafe { printf("%d and %s\n", 42, "text"); };
return 0;
}... is only valid on an extern declaration. Sun has no va_arg, so a Sun
function could not read the arguments.
Linking
-l names a library and -L adds a search directory. Both work when compiling
and when running under the JIT:
sun -lm program.sun # JIT
sun -c -L/opt/lib -lsqlite3 -o app app.sunUnder the JIT the libraries are loaded into the compiler process; when compiling they are passed to the linker. libc and libm are already present in the process, so they usually need no flag at all under the JIT.
-L/-l are for native libraries. --lib-path and --moon are unrelated —
they locate Sun .moon libraries.
Safe wrappers
The intended pattern is to contain unsafe in a thin wrapper and give the rest
of the program a safe API:
extern "C" function abs(x: i32) i32;
function absolute(x: i32) i32 {
var r = 0;
unsafe { r = abs(x); };
return r;
}
function main() i32 { return absolute(-5); } // no unsafe needed hereExterns can be declared in a .moon library, so a wrapper module can be
published and consumed like any other Sun code. Bundling prefixes a module's
symbols to isolate versions, but a C extern's name is its ABI, so it is left
alone and still resolves against libc in the importing program.
An extern's Sun-side name is scoped to its module like any other item, so a library can wrap a C function without exporting it:
public module wrap {
// Private: importers get the wrapper, not the raw call.
extern "C" function c_getenv(name: raw_ptr<u8>) raw_ptr<u8> as "getenv";
public function lookup(alloc: ref HeapAllocator, name: raw_ptr<u8>) Option<String> {
var value = unsafe { c_getenv(name); };
if (value == null) { return Option.None; }
return Option.Some(from_c_str(alloc, value));
}
}The symbol is still global. Two modules may each declare the same C function
under their own Sun name, but their signatures must agree — the second
declaration of a symbol is checked against the first and rejected on a
mismatch. This is why the standard library keeps every one of its externs in a
single private file, stdlib/sys.sun.
Limitations
- Argument classification implements x86-64 System V and AArch64 AAPCS64
(ELF,
gnuandmuslenvironments). Other targets — including Apple's AArch64 variant — are rejected at compile time until they grow their own rules. Cross-compile withsun --target aarch64-linux-musl -c -o prog program.sunwhen a cross toolchain is installed, or stop at--emit-objand link on the target machine. - Linking is static by default (musl-preferred), so
-llibraries must ship static archives (.a). Libraries distributed only as shared objects — most proprietary vendor SDKs — need--dynamic. - The borrow checker does not model a pointer escaping into C. Handing C a
refgives it an address with no lifetime tracking across the call — theunsafeblock is the only marker. - There is no automatic binding generation from C headers; structs, enums and signatures are declared by hand.
- There is no
externdeclaration for a C global, only for functions. That is why the standard library has no way to enumerate the environment, which would needenviron.