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. See
Unsafe Blocks for the two forms the block takes
and where the semicolons go.
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;Global variables
A module-level C global is declared with extern var. It has an explicit type,
no initializer, and may be public or private like any other module item:
public extern "C" var c_environ: raw_ptr<raw_ptr<u8>> as "environ";
function first_entry() raw_ptr<u8> {
unsafe { return _load<raw_ptr<u8>>(c_environ, 0); };
}Reading, writing, borrowing, indexing, or taking a field through an extern
global requires unsafe, because native code owns the storage. Declarations
are hoisted, and as "symbol" remains the exact native ABI name through
.moon bundling. Compatible declarations of the same C symbol are
idempotent; conflicting types, a function/global kind mismatch, or a collision
with a Sun definition is an error.
Supported global types are non-void primitives, payload-free enums,
raw_ptr<T>, ref T, by-value classes, and valid non-throwing C function
pointers. Arrays, slices, interfaces, lambdas, error unions, payload enums, and
static_ptr have no supported C global representation.
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> |
R (*)(Args) | function (Args) R |
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.
Pointers
C's void* is raw_ptr<u8> in Sun. A pointer to any other type retypes to it
with _bitcast, so the function is declared once rather than once per argument
type:
extern "C" function c_setsockopt(fd: i32, level: i32, opt: i32, val: raw_ptr<u8>, len: i32) i32 as "setsockopt";
function set_reuse_addr(fd: i32) i32 {
var on: i32 = 1;
var p = _bitcast<raw_ptr<u8>>(_address_of<i32>(on));
unsafe { return c_setsockopt(fd, 1, 2, p, 4); };
}The same cast reads bytes back as a header — see
_bitcast<T> for the rules. The cast itself only renames
the pointer; reading through it needs unsafe.
Callbacks
An extern may accept a named Sun function as a C function pointer:
typedef int (*callback_t)(int);
int call_callback(callback_t callback, int value);extern "C" function call_callback(
callback: function (i32) i32,
value: i32
) i32;
function double(value: i32) i32 { return value * 2; }
function main() i32 {
unsafe { return call_callback(double, 21); };
}The declared pointer signature must match exactly. A C callback uses an ABI-compatible
return value; adapt result enums to a C error code explicitly. Callback
parameters may be primitives, payload-free enums, raw_ptr<T>, or ref T.
The return may be a primitive, a payload-free enum, or raw_ptr<T>. The
compiler rejects throwing callbacks, lambdas, bound methods, by-value classes,
interfaces, payload enums, error unions, callbacks returning another function
pointer, and extern functions that return a function pointer. Aggregate extern
parameters and returns remain supported for ordinary direct calls; they are
only excluded from callback signatures.
Function pointers are non-null. Sun does not add nullable pointers, casts, or function-to-lambda conversions at this boundary.
Registering a callback is unsafe. C may retain the pointer indefinitely, so
only a module function may cross this boundary; it has no captured Sun frame
to expire. The call into C and every later action exposed through the C API
remain outside the borrow checker's guarantees.
C callback APIs commonly carry a separate userdata pointer. Spell it
raw_ptr<u8> on both the registration function and the callback, and recover
the concrete pointer explicitly inside unsafe code:
extern "C" function register_callback(
callback: function (raw_ptr<u8>, i32) void,
userdata: raw_ptr<u8>
) void;A callback may run on a thread created by C. Sun does not insert locks or move the callback onto the registering thread; shared state requires manual synchronization appropriate for that C API.
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.sunWhen compiling, the libraries are passed to the linker. Under the JIT, a shared
library (.so, .dylib) is loaded into the compiler process, and a static
archive (lib<name>.a found in a -L directory, or an explicit .a path) is
linked by the JIT itself. When a directory holds both, the shared library wins.
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.
Carrying the C library along
A bundle can also carry the native static library it binds, with archives:
in its manifest:
manifest {
source_files: ["mylib.sun"]
archives: ["$VENDOR/libfoo.a"]
}The archives travel inside the .moon. Programs importing the bundle link
against them automatically — no -l flags, and nothing to install alongside
the finished binary. Paths accept path variables ($VENDOR), so where the
archives live stays a build-time setting rather than something baked into the
manifest. A program whose own manifest names archives: links them the same
way, and so does the test binary of a library that declares them — which is
how the tls bundle's own tests link OpenSSL.
This is how tls.moon ships OpenSSL; see TLS & HTTPS.
A carried archive keeps its identity. When the bundle is built, every symbol
the archives define is renamed under a hash of all the archives the bundle
lists together, the way the bundle's Sun symbols are renamed under the
bundle's hash, and the
bundle's extern "C" declarations naming those symbols bind to the renamed
copy. Two bundles that carry different versions of one library therefore
link into a single program without colliding: each calls its own version,
and a value from one cannot reach the other's code because their Sun types
are distinct as well. The compiler warns when that happens, since two copies
of a library also means two sets of its global state. Two bundles that carry
the same set of archives, whether one was built on the other or each listed
the same files, spell the symbols the same way and share one copy. Hashing
the set rather than each file keeps libssl.a's calls into libcrypto.a
inside one identity: the same libssl.a next to a different libcrypto.a
is a different set.
Two consequences follow from the binding rule:
- A bundle built on top of
tls.mooncarries the OpenSSL archives forward, already bound to tls, and its own code reaches them only through tls's Sun API. Declaringextern "C" function SSL_newin that bundle, or in a program importing it, does not bind to the carried copy; the compiler warns and the symbol has to come from elsewhere. To bind your own copy, carry the library under your ownarchives:. - An archive has to be self-contained (an archive referencing a library you do not ship fails at link time), built for the target Sun links for (the musl toolchain, for the static default), and made of native objects: an LTO build whose members are LLVM bitcode cannot be renamed and is rejected.
Running a program directly and compiling it behave alike: the JIT loads the
carried archives just as the linker would, so a program that works under
sun program.sun links the same way under sun -c.
Limitations
- Argument classification implements x86-64 System V (Linux and Intel Macs
share it) and AArch64 AAPCS64 in both its ELF (
gnuandmusl) and Apple flavors. On Apple arm64 the caller extends integers narrower than 32 bits (signext/zeroext, on returns too) and HFA arguments carry no stack alignment attribute; aggregates classify exactly as on ELF. Other targets 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 — the only option for--target arm64-apple-darwinfrom Linux, since linking Mach-O needs Apple's SDK (cc -o prog prog.o -lc++on the Mac). - Linking is static by default (musl required on Linux), so
-llibraries must ship static archives (.a). Libraries distributed only as shared objects — most proprietary vendor SDKs — need--dynamic. macOS targets are always dynamic: Apple ships no static libc, and its linker links the C++ runtime as-lc++rather than-lstdc++. - 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.