Foreign Function Interface
An extern declaration names a function compiled outside Rux — in the operating system or a C library — and gives its signature, with no body. Once declared, it is called like any other function. The compiler takes the declaration on trust: it cannot look inside the library, so a declaration that does not match the real function is not an error but a program that misbehaves.
extern-decl = attributes [ "pub" ] "extern" ( extern-func | extern-var | extern-block )
extern-func = "func" identifier "(" [ parameters [ "," "..." ] ] ")" [ "->" type ] ";"
extern-var = identifier ":" type ";"
extern-block = "{" { [ "pub" ] ( extern-func | extern-var ) } "}"
Every extern function and variable needs a #Link naming its library; Linking covers choosing it per platform. Its calling convention is the target's C convention unless #Abi says otherwise.
Declarations
A single function:
#Link("Kernel32.dll")
extern func GetCurrentProcessId() -> uint32;
A block groups declarations from one library, with the attributes written once. Inside a block, members are written without extern, may each be pub, and take no attributes of their own:
#Link(CRuntime)
extern {
func strlen(text: *char8) -> uint;
func malloc(size: uint) -> *var opaque;
func free(memory: *var opaque);
}
An extern variable, extern Name: Type; or Name: Type; inside a block, declares data defined outside the program. It is read, written and addressed with @ like a module variable:
#Link("libc.so.6")
extern opterr: int32; // nonzero: getopt prints its own error messages
func Quiet() {
opterr = 0;
}
Like a function, it names its library with #Link("library") before it or on its extern { } block, and #Link("library", "symbol") before it gives it a Rux name of its own. Without one it is extern variable 'opterr' must name the library it is imported from. Its type must have a C ABI mapping, as a parameter's does — see Type mapping.
Example
The C runtime, called directly — a formatted write, a struct returned by value, and memory from malloc:
import Core::{ #target, #Error };
import Io::PrintLine;
when #target.os {
.FreeBSD => const CRuntime = "libc.so.7";
.Linux => const CRuntime = "libc.so.6";
.macOS => const CRuntime = "libSystem.B.dylib";
.Windows => const CRuntime = "msvcrt.dll";
else => #Error("unknown C runtime")
}
// div_t: two C ints, in C's field order.
struct Div {
quot: int32;
rem: int32;
}
#Link(CRuntime)
extern {
func sprintf(buffer: *var char8, format: *char8, ...) -> int32;
func div(numerator: int32, denominator: int32) -> Div;
func malloc(size: uint) -> *var opaque;
func free(memory: *var opaque);
}
func Main() -> int {
var buffer: char8[64];
let written = sprintf(@buffer[0], "%s has %d legs".data, "spider".data, 8i32);
PrintLine("{}", buffer[..written as uint]); // spider has 8 legs
let d = div(17i32, 5i32);
PrintLine("{} r {}", d.quot, d.rem); // 3 r 2
let memory = malloc(16) as *var char8;
if memory == null {
return 1;
}
free(memory as *var opaque);
return 0;
}
Type mapping
A C declaration translates type by type. Rux's int is 64 bits, so C's int is int32, never int:
| C | Rux |
|---|---|
int, unsigned int | int32, uint32 |
short, long long, int64_t, … | the integer of the same width |
long | int32 on Windows, int64 elsewhere — choose with when #target.dataModel |
size_t, uintptr_t | uint |
double, float | float64, float32 |
char (as a byte) | char8 or int8/uint8 |
const T * | *T |
T * written through | *var T |
void * | *opaque or *var opaque |
a pointer to a struct the code never looks inside (FILE *) | a pointer to an empty struct, or *opaque |
struct S by value | a Rux struct with the same fields in the same order |
| a function pointer | a function type: func(*opaque, *opaque) -> int32 |
void result | no -> |
Floating-point widths
The C calling convention knows two floating-point types, float and double, which are float32 and float64. Rux's other widths — float8, float16, float80, float128, float256 and float512 — are passed by the compiler's own convention, which matches no C type: not _Float16, not long double, not __float128. One of them as a parameter or result of an extern function, as a parameter or result of a function marked #Abi, or as a C-variadic argument is refused:
error: extern parameter 'value' has type 'float80', which has no C ABI mapping
note: 'float80' is passed by the compiler's own convention, which matches no C type such as '_Float16', 'long double' or '__float128'
help: convert the value to 'float32' or 'float64' at the boundary, or pass a pointer to it
A result is reported as the return value of extern function 'sqrtl' has type 'float80', an #Abi function's parameter as parameter 'value' of '#Abi' function 'Callback' has type 'float16', and a variadic argument as a C-variadic argument has type 'float16', each with the same note and help. Convert the value to float32 or float64 at the boundary, or pass a pointer to it.
The check reads the declared type itself, so a structure passed by value that holds a
float16 or float80 field is not diagnosed. Its layout still matches no C type; do not pass one to C.The C package declares aliases named after C's types — c_int, c_long, size_t, time_t — that resolve to the right width on each target, and binds much of the C runtime; prefer it to hand-written declarations. Struct layout follows the C rules, so a struct declared field for field matches C — see Layout.
Strings
C finds the end of a string by its zero byte. A Rux string literal keeps a NUL after its last unit — not counted by .length — so a literal's .data is a valid C string:
let greeting = "hello";
let n = strlen(greeting.data); // 5, the same as greeting.length
That holds for literals. A slice cut from the middle of a string, or text built at run time, has no NUL after it unless the program writes one; handing its .data to C makes C read past the end. Passing a slice where C wants an address is a type error:
error: argument 1 to 'strlen' has type 'char8[..]', but parameter 'text' requires '*char8'
Pointers
Arguments that C reads or writes through are pointers: @value for one variable, @array[0] for the first element of a buffer, .data for a slice. A result C returns as void * is converted with as to the type it really points to, and checked for null — C reports failure with a sentinel, never with a panic or a fallible:
| Function | Success | Failure |
|---|---|---|
malloc | an address | null |
sprintf | the bytes written | a negative count |
Variadic functions
A trailing ... declares a C variadic function. Any number of arguments of any type may follow the fixed ones, and nothing checks them against what the callee expects: pass each at exactly the type the callee will read — 8i32 for %d, a float64 for %f, .data for %s. A wrong one builds and prints garbage. The one argument type the compiler does refuse is a floating-point width C lacks, such as a float16. Rux can call a C variadic function but not define one; a Rux variadic parameter (args: T...) is a different, type-checked feature — see Parameters.
Callbacks
A Rux function can be passed where C expects a function pointer, by naming it. C then calls it with C's convention, which a Rux function uses by default; mark it #Abi(.C) to keep that promise explicit. The qsort example on that page shows the whole round trip.