Platform · Lesson 24.2

C interop

Source
Call the C runtime through the C package. The program uses C's own types, turns an opaque pointer into a typed one, reads a C struct, makes a variadic call and checks every result for failure.
Runs on Windows, Linux, macOS and FreeBSD.
The C package picks each system's own C runtime, so the same source builds everywhere.

The C runtime is the one library almost every system has: memory allocation, formatted text, dates and times, sorting. The Extern lesson declared foreign functions by hand. For the C runtime you do not have to — the C package already declares much of it, and you import functions from it like from any other package:

import C::{ c_int, free, gmtime, malloc, size_t, sprintf, time_t };

What the package cannot do is make C feel like Rux. Every C interface is built from four shapes that Rux code does not otherwise meet, and this lesson uses each of them once.

C's own types

C's int is 32 bits, but Rux's int is 64. C's long is 32 bits on Windows and 64 everywhere else. So bindings are written in aliases named after C, which stand for whichever Rux type matches on the target being built:

AliasC typeRux type on WindowsRux type on Linux, macOS, FreeBSD
c_intintint32int32
c_longlongint32int64
size_tsize_tuintuint
time_ttime_tint64int64

The C package chooses c_long with a when #target.dataModel — the same field the Target lesson printed. Write a C binding's types in these aliases, never as int or int64 directly, and it stays right on every system.

Opaque pointers

malloc returns C's void *: an address with no type. Rux spells it *opaque. You convert it with as to the type you meant, and check it, because C reports "no memory" by returning null:

let capacity: size_t = 64;
let buffer = malloc(capacity) as *var char8;
if buffer == null {
    PrintLine("malloc failed");
    return 1;
}
defer free(buffer as *var opaque);

The defer hands the memory back on every path out of Main. Handles such as C's FILE * go one step further: the package declares FILE as an empty struct, so you can hold a *FILE and pass it back to C, but there is nothing inside for Rux code to look at.

Variadic functions

sprintf writes formatted text into a buffer. The ... at the end of its C declaration accepts any number of arguments of any type, and nothing checks them against the format string. You must pass each one at the type its % code reads:

let written = sprintf(buffer, "%s has %d legs and weighs %.1f g".data,
    "spider".data, 8 as c_int, 0.5);
CodeReadsPassed as
%sthe address of zero-ended bytes"spider".data
%da C int8 as c_int
%.1fa 64-bit float, one decimal0.5

sprintf is never told how big the buffer is, so the buffer must be big enough for any result — 64 bytes for 34 here. It returns how many bytes it wrote, and buffer[..written as uint] turns the pointer and that count back into a Rux slice that PrintLine can print.

C structs

gmtime turns a count of seconds since 1970 into a calendar date. It takes the address of a time_t — that is what @stamp gives it — and returns the address of a tm struct:

var stamp: time_t = 1000000000;
let calendar = gmtime(@stamp);
if calendar == null {
    PrintLine("gmtime failed");
    return 1;
}

The package declares tm field for field in C's order, so Rux reads the struct exactly where C wrote it: calendar.tm_hour is C's tm_hour. Two of the fields keep C's own habits — tm_year counts from 1900 and tm_mon from zero — which is why the program adds 1900 and 1. The struct belongs to the C runtime, so the program reads it and does not free it.

Failure is a value you must check

None of these calls panics or returns a fallible. C reports failure with a sentinel value, and the caller has to check for it:

FunctionOn successOn failure
mallocan addressnull
sprintfthe bytes it wrotea negative count
gmtimethe address of a tmnull

Skip a check and the program carries on with a null pointer or a negative length, and fails somewhere far from the cause.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// The C runtime is the library almost every system has, and the `C` package declares much of it
// for you. Using it means meeting the four shapes every C interface is made of.
//
// C's own types. C's `int` is 32 bits, but Rux's `int` is 64, and C's `long` is 32 bits on
// Windows and 64 elsewhere. So bindings are written in aliases named after C, such as `c_int`,
// `size_t` and `time_t`, which stand for whichever Rux type matches on the target being built.
//
// Opaque pointers. `malloc` returns C's `void *`, which Rux spells `*opaque`: an address with no
// type, which you convert to the type you meant. Handles such as C's `FILE *` go one step
// further: `FILE` is declared as an empty struct, so you can hold one and pass it back, but
// never look inside.
//
// C structs. `tm` is declared field for field in C's order, so Rux reads the struct exactly
// where C wrote it.
//
// Variadic functions. The `...` in `sprintf` accepts any arguments, and nothing checks them
// against the format. You must pass each one at the type its `%` code reads.
//
// None of these calls panics or returns a fallible: C reports failure with a sentinel value,
// such as null or a negative count, and the caller has to check for it.
import C::{ c_int, free, gmtime, malloc, size_t, sprintf, time_t };
import Io::PrintLine;

func Main() -> int {
    // An opaque pointer becomes a typed one with `as`. Null means there was no memory.
    let capacity: size_t = 64;
    let buffer = malloc(capacity) as *var char8;
    if buffer == null {
        PrintLine("malloc failed");
        return 1;
    }
    defer free(buffer as *var opaque);

    // `%s` reads the address of zero-terminated bytes, `%d` a C int and `%f` a 64-bit float.
    // sprintf is never told how big the buffer is, so it must be big enough for any result.
    let written = sprintf(buffer, "%s has %d legs and weighs %.1f g".data,
        "spider".data, 8 as c_int, 0.5);
    if written < 0 {
        PrintLine("sprintf failed");
        return 1;
    }
    PrintLine("{} ({} bytes)", buffer[..written as uint], written);

    // gmtime fills a `tm` struct that the C runtime owns, and returns its address, or null.
    var stamp: time_t = 1000000000;
    let calendar = gmtime(@stamp);
    if calendar == null {
        PrintLine("gmtime failed");
        return 1;
    }
    PrintLine("{} seconds after 1970 began was {}-{}-{}, {}:{}:{} UTC", stamp,
        calendar.tm_year + 1900, calendar.tm_mon + 1, calendar.tm_mday,
        calendar.tm_hour, calendar.tm_min, calendar.tm_sec);
    return 0;
}

Besides Io, its Rux.toml lists C under [Dependencies].

Run it

cd Examples/Platform/CInterop
rux run
spider has 8 legs and weighs 0.5 g (34 bytes)
1000000000 seconds after 1970 began was 2001-9-9, 1:46:40 UTC

Common mistakes

Using the opaque pointer as it is.
Without the as *var char8, the buffer is still a *var opaque, and the sprintf call fails with error: argument 1 to 'sprintf' has type '*var opaque', but parameter 'buffer' requires '*char8'.
Passing a Rux string as the format.
A C function takes the address of the bytes, not a Rux slice. Without .data the call fails with error: argument 2 to 'sprintf' has type 'char8[..]', but parameter 'format' requires '*char8'.
An argument that does not match its % code.
After the format, the compiler checks nothing. Pass the integer 1 where %.1f expects a float, or "spider" without .data where %s expects an address, and the program still builds. What it prints is undefined — on Windows x86-64 the first gave 0.0 and the second a few bytes of garbage.
Skipping the null check.
malloc and gmtime return null when they fail. Reading through a null pointer is never valid, and when it goes wrong it goes wrong a long way from the call that caused it.

Try it yourself

  1. Set stamp to 0, then to 2000000000. Predict both dates before you run.
  2. Format 255 as c_int and 48879 as c_int with "%x %X" into the buffer and print the result.
  3. Import strlen from C and check that strlen(buffer) agrees with written.
  4. Print calendar.tm_wday, the day of the week counted from Sunday as 0. Which day was 9 September 2001?

Learn more