v0.4.0

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, inside an unsafe context. 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 a library defines: storage the program reads and writes but does not own. A read or a write needs an unsafe context, while @Name does not; all three reach the library's own storage:

#Link("libc.so.6")
extern opterr: int32;   // nonzero: getopt prints its own error messages

func Quiet() {
    // Safety: nothing else in this program touches 'opterr' while getopt is not running.
    unsafe { opterr = 0; }
}

As with a module variable, nothing moves out of one: move opterr is cannot move out of extern variable 'opterr'.

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";
    _ => #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];
    // Safety: the literals are NUL-terminated, and the result is far shorter than 'buffer'.
    let written = unsafe { sprintf(@buffer[0], "%s has %d legs".data, "spider".data, 8i32) };
    PrintLine("{}", buffer[..written as uint]);   // spider has 8 legs

    // Safety: 'div' only reads its two integer arguments.
    let d = unsafe { div(17i32, 5i32) };
    PrintLine("{} r {}", d.quot, d.rem);           // 3 r 2

    // Safety: 'malloc' returns null or a block of at least 16 bytes, which is freed once.
    unsafe {
        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:

CRux
int, unsigned intint32, uint32
short, long long, int64_t, …the integer of the same width
longint32 on Windows, int64 elsewhere — choose with when #target.dataModel
size_t, uintptr_tuint
double, floatfloat64, 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 valuea Rux struct with the same fields in the same order
a function pointera function type: func(*opaque, *opaque) -> int32
void resultno ->

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 also looks inside a structure, union, tuple or fixed array passed by value, through nested fields, because C lays such a value out field by field. One field of such a width anywhere inside it is refused at the same boundary, naming the field — extern parameter 'value' has type 'Sample', whose field 'level' holds 'float16', which has no C ABI mapping — with the help to store the field as float32 or float64, or to pass a pointer to the value.

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";
// Safety: a string literal ends with a NUL.
let n = unsafe { 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 — a cast that needs an unsafe context — and checked for null. C reports failure with a sentinel, never with a panic or a thrown error:

FunctionSuccessFailure
mallocan addressnull
sprintfthe bytes writtena 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.

Unsafe code

The compiler checks references, bounds, ownership and borrows, but it cannot check what a raw pointer addresses, what a foreign function does, or what an unsafe func expects of its caller. The operations that depend on such facts are allowed only in an unsafe context — an unsafe { … } block or the body of an unsafe func — so every place a program's soundness rests on a promise is found by searching for unsafe:

OperationError outside an unsafe context
calling an extern function, an asm func or an unsafe func, or naming one as a function valuecall to extern function 'write' requires 'unsafe'
reading or writing an extern variableaccess to extern variable 'opterr' requires 'unsafe'
*p, *p = v, p[i], p[a..b], and a field reached through *T or *var Tdereferencing a raw pointer requires 'unsafe'
pointer arithmetic that produces a pointer: p + n, p - n, p += n, p -= npointer arithmetic requires 'unsafe'
a cast that gives an address a type it did not have: an integer to a pointer, *opaque to *T, *A to *B, an address to a function typecasting '*opaque' to '*var Node' requires 'unsafe'

Everything else about raw pointers is safe: taking an address with @, copying, comparing and null-testing pointers, converting a pointer to an integer, and a cast that only weakens what a pointer says, such as *var T to *T or anything to *opaque. Each error comes with a note and a help:

Src/Main.rux:5:12: error: dereferencing a raw pointer requires 'unsafe'
  note: the compiler cannot prove that a raw pointer addresses a live value of its type
  help: wrap it in 'unsafe { ... }' and say why it holds in a '// Safety:' comment above it

unsafe { … } is a value block: in value position it yields its last expression written without a ;, so a single call is let n = unsafe { strlen(text) };. At the start of a statement it is the statement and yields nothing. Otherwise it is exactly the statements it holds — its locals are destroyed at its end, and return, break and continue leave it as they leave any block. It changes nothing about what the program does; it only grants permission. A block that holds no operation needing it, or one inside another unsafe context, is the warning unnecessary 'unsafe' block.

unsafe func declares a function whose caller carries an obligation the compiler cannot check, such as Memory::Copy, whose ranges must be valid for length bytes. A call of it needs an unsafe context, and its body is one. The keyword stands directly before func, as in pub unsafe func. An extern declaration and an asm func are always unsafe to call, so unsafe on one is an error, and a destructor, an operator, an interface requirement and its implementation cannot be unsafe, because nothing marks where they are called. A function whose safety does not depend on its caller stays safe and keeps its raw operations in unsafe blocks inside its body.

rux lint asks for the reason: its rule unsafe.comment warns about an unsafe block unless the comment directly above the line holding unsafe has a line starting Safety:, and about an unsafe func unless its documentation does. The comment says why the operation is sound there, as in // Safety: index is below self.length, checked above. #Allow("unsafe.comment") on a function silences the rule for its body.

See also