Foreign function interface (FFI)

mu can call native libraries via dlopen, dlsym, and dlcall, or use the higher-level ffi.fn helper for constant bindings.

Example

import "ffi"

puts := ffi.fn("/usr/lib/libSystem.B.dylib", "puts", "i32", ["cstr"])
fflush := ffi.fn("/usr/lib/libSystem.B.dylib", "fflush", "i32", ["ptr"])

puts("hello from mu")
fflush(nil)

The fflush is not decoration. puts writes into libc’s own buffered stdout, and mu exits through the raw exit syscall without running libc’s atexit handlers — so without the flush the program prints nothing at all and looks broken. Anything that writes through libc’s stdio needs the same treatment.

Where FFI works

Raw syscall works on every supported backend/target. Calling C libraries is a separate FFI layer with explicit platform limits:

Execution path C FFI status
Bytecode VM (Go host) darwin and linux when the host is built with cgo; otherwise the FFI builtins return ffi not supported on this platform
Native build (Go backend) supported on the Go native targets; tests cover darwin/arm64, linux/amd64, and linux/arm64
Self-hosted VM supported through the VM’s FFI bridge where the host FFI builtins are available
Self-hosted native supported on darwin/arm64 and linux/amd64; linux/riscv64 currently omits the direct FFI builtins

ffi.fn is the convenient path for stable bindings: the library path, symbol, return type and argument types must be compile-time constants so native builds can lower the call. The raw dlopen / dlsym / dlcall builtins are still available for dynamic calls where the execution path supports them.

Notes

See docs/Specification.md for full FFI rules.

Next steps