External functions
mach's foreign function interface (FFI) is a single declaration form: ext fun. It declares a body-less function with the C ABI - a forward reference the linker resolves at link time. Declare the foreign symbol, call it like any other function, and supply its definition (a C object, archive, or shared library) when you build. This is the only body-less function form mach allows.
Grammar
An ext fun is a function signature with no body. The declaration ends with a semicolon instead of a block.
ext fun NAME(args) RET;
- No body block - the declaration ends with a semicolon.
- Argument and return types must be representable in C.
pub ext funexposes the import to other modules; withoutpubit is file-private.
pub ext fun libc_write(fd: i64, buf: *u8, n: i64) i64;
ext fun strlen(s: *u8) i64; # private, file-local
The C ABI boundary
The C ABI is the contract at the boundary: every argument and return type must be representable in C, and values are passed exactly as the target's C calling convention dictates. mach's fixed-width scalars and pointers correspond directly to their C counterparts.
i8throughi64andu8throughu64are the sized C integers.f32andf64are Cfloatanddouble.*Tand**Tare C pointers; a C-style string is a*u8.
The exact calling convention is the one declared for the target (for example sysv64 on Linux or win64 on Windows). See Manifest for how a target's abi is set.
Renaming the linker symbol
By default the linker looks up the declared name. The #[symbol("real_name")] decorator overrides it, binding the declaration to a different external symbol.
#[symbol("write")]
pub ext fun libc_write(fd: i64, buf: *u8, n: i64) i64;
Two common reasons to rename:
- The C name (such as
write) would shadow other things in your namespace. - The target decorates symbol names in its ABI and you need to match the decorated form.
Library attribution
On a format that attributes each import to a specific dependency - the PE (Windows) import directory - the #[library("...")] decorator pins an ext import to the DLL that exports it.
#[library("ws2_32.dll")]
ext fun WSAStartup(ver: u16, data: *u8) i32;
- The named DLL must appear in the target's
[os.*].libsdependency set. Pinning to an absent library is a link error, never a silent fallback. - An import with no
libraryis unattributed; on PE it binds to the first declared dependency. Pin every import that belongs to a different DLL. - On ELF (Linux) the loader resolves imports by global search, so
libraryhas no effect on the emitted binary; the value is still validated against the dependency set.
library composes with symbol: the rename sets the imported symbol's name, library sets the DLL it is imported from.
# imported as `socket` from ws2_32.dll, called as `ws2_socket` in mach.
#[library("ws2_32.dll")] #[symbol("socket")]
ext fun ws2_socket(af: i32, kind: i32, proto: i32) i64;
Linking the definition
An ext fun is only a forward reference. Its definition is supplied at link time, either statically by a precompiled object or archive or dynamically by a shared library bound at load time. Provide those inputs to mach build on the command line or through the manifest. An undefined ext symbol that no input resolves is a link error, so a typo never silently drops a dependency.
mach build . path/to/libfoo.a # static archive (every member is pulled)
mach build . -L build/libs -l foo # search dir + name
mach build . -l c # link libc dynamically
The same inputs live in the manifest's libs overlay, merged with the command-line inputs:
[target.linux]
libs = ["build/libs/libfoo.a", "c"]
A loose .o object or static .a archive is a static input merged into the binary; a shared .so is a dynamic dependency whose undefined ext symbols bind at load time through an emitted PLT. A static definition always wins over a same-named dynamic import. Dynamic linking is implemented for the ELF (Linux) and PE (Windows) targets; the Mach-O (Darwin) import path is not yet implemented.
The link-input resolution rules - explicit paths, -l/-L search order, and static-vs-dynamic selection - are documented once on the CLI page, and the manifest overlay on the Manifest page.
A worked example
This program imports two libc functions, calls them, and prints a line to standard output. strlen measures the message and libc_write (the renamed write) writes it to file descriptor 1.
use std.runtime;
#[symbol("write")]
pub ext fun libc_write(fd: i64, buf: *u8, n: i64) i64;
ext fun strlen(s: *u8) i64;
#[symbol("main")]
fun main(argc: i64, argv: **u8) i64 {
val msg: *u8 = "hello from ffi\n";
val n: i64 = strlen(msg);
libc_write(1, msg, n);
ret 0;
}
libc supplies both symbols, so link it dynamically. The result is a dynamically-linked binary that binds write and strlen at load time.
mach build . -l c # resolves the ext imports against libc.so
See also
- Visibility -
pubandextmodifiers - Decorators - the
symbolandlibrarydirectives - CLI - external link inputs and resolution
- Manifest - the
libslink overlay - Functions - regular function declarations