C interop
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:
| Alias | C type | Rux type on Windows | Rux type on Linux, macOS, FreeBSD |
|---|---|---|---|
c_int | int | int32 | int32 |
c_long | long | int32 | int64 |
size_t | size_t | uint | uint |
time_t | time_t | int64 | int64 |
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);
| Code | Reads | Passed as |
|---|---|---|
%s | the address of zero-ended bytes | "spider".data |
%d | a C int | 8 as c_int |
%.1f | a 64-bit float, one decimal | 0.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:
| Function | On success | On failure |
|---|---|---|
malloc | an address | null |
sprintf | the bytes it wrote | a negative count |
gmtime | the address of a tm | null |
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.
// 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
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'.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'.% 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.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
- Set
stampto0, then to2000000000. Predict both dates before you run. - Format
255 as c_intand48879 as c_intwith"%x %X"into the buffer and print the result. - Import
strlenfromCand check thatstrlen(buffer)agrees withwritten. - Print
calendar.tm_wday, the day of the week counted from Sunday as 0. Which day was 9 September 2001?
Learn more
- The C package — every declaration it offers, with C types,
malloc,sprintfandgmtime - Pointers and
externin the Rux Reference - Pointer slice and Layout — the memory ideas this lesson leans on
- ABI — the next lesson: names, calling conventions, and a Rux function that C calls back