C FFI

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

CSun
int, long, char, float, doublei32, 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
voidvoid

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.sun

Under 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 here

Externs 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, gnu and musl environments). Other targets — including Apple's AArch64 variant — are rejected at compile time until they grow their own rules. Cross-compile with sun --target aarch64-linux-musl -c -o prog program.sun when a cross toolchain is installed, or stop at --emit-obj and link on the target machine.
  • Linking is static by default (musl-preferred), so -l libraries 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 ref gives it an address with no lifetime tracking across the call — the unsafe block is the only marker.
  • There is no automatic binding generation from C headers; structs, enums and signatures are declared by hand.
  • There is no extern declaration for a C global, only for functions. That is why the standard library has no way to enumerate the environment, which would need environ.