Talking to C
Goose compiles to C, so calling C is not a foreign-function interface in the usual sense. A Goose call to a C function compiles to a plain C call, with no marshalling and no wrapper. This chapter shows the three ways to reach C code, which Goose values can cross the boundary, and how to go the other way and call Goose from a C program.
#extern fn22.1
A function declared with extern fn and no body is a C function. The
Goose signature describes it. If the C name differs from the Goose name,
give it after an =:
extern fn cube_root(x: f64) -> f64 = "cbrt";
extern fn hypot(x: f64, y: f64) -> f64;
fn main() {
print(cube_root(27.0), " ", hypot(3.0, 4.0));
}
3.0000000000000004 5.0
Both functions come from the C maths library, which every Goose program
links. The standard library's math and os modules are written this way.
#What can cross22.2
Only values with a plain C shape can be passed to or returned from C:
- the scalars and
bool; - fixed-size flat values (structs, fixed arrays, fixed-mode enums) by value, as their packed C layout;
- a reference to such a value, as a pointer;
- a slice of such values, as a
{ data, len }struct; - a
u8[>..]&builder, which C appends to through a runtime function.
Results are a single scalar or flat fixed-size value, or nothing. Anything else is rejected at the declaration, before any call is compiled:
struct Label { text: u8[] }
extern fn show(l: Label);
fn main() {}
error: extern fn show: parameter l of type Label cannot cross to C: only fixed-size values cross by value
Writability is part of the signature too. A parameter declared u8[:]
promises C may write through it, so it cannot be given a string literal.
Declare it const u8[:] if C only reads:
extern fn shout(s: u8[:]);
fn main() { shout("honk"); }
error: extern fn shout: parameter s of type u8[:] takes a writable value; a read-only one needs the parameter declared const (§9.5)
C must not keep a pointer it was given after the call returns. Goose cannot check what C does, so this is the one place where the safety of a program depends on code the compiler cannot see.
#A program's own C22.3
C code that belongs to your program goes in files listed in a sidecar,
main.build beside main.goose. goose run compiles it into a library
that the in-process run loads, and goose build links it into the
executable. Here is a small C file that summarizes an array of weights and
hashes a string:
#include <stdint.h>
typedef struct { const int32_t *data; int64_t len; } i32_slice;
typedef struct { const uint8_t *data; int64_t len; } u8_slice;
typedef struct __attribute__((packed)) {
int32_t lo, hi;
double mean;
} Stats;
void weigh_all(i32_slice xs, Stats *out) {
int64_t sum = 0;
out->lo = out->hi = xs.data[0];
for (int64_t i = 0; i < xs.len; i++) {
if (xs.data[i] < out->lo) out->lo = xs.data[i];
if (xs.data[i] > out->hi) out->hi = xs.data[i];
sum += xs.data[i];
}
out->mean = (double)sum / (double)xs.len;
}
uint32_t checksum(u8_slice s) {
uint32_t h = 0;
for (int64_t i = 0; i < s.len; i++) h = h * 31 + s.data[i];
return h;
}
The sidecar names the C files. It uses Goose-like syntax:
c_sources = ["impl.c"];
And the Goose side declares the functions:
struct Stats { lo: i32, hi: i32, mean: f64 }
extern fn weigh_all(xs: i32[:], out: Stats&);
extern fn checksum(s: const u8[:]) -> u32;
fn main() {
let weights: i32[5] = [45, 52, 38, 41, 60];
var s = Stats { 0, 0, 0.0 };
weigh_all(weights, s);
print(s, " ", checksum("goose"));
}
Stats { 38, 60, 47.2 } 98539801
Two details matter. The C struct is declared packed, because Goose structs
have no padding (Chapter 10). Here the fields happen to line up anyway, but
the attribute makes the C layout match whatever the fields are. And the
slice arrives as a two-field struct passed by value.
A sidecar can also list include directories, defines and system libraries,
with per-platform variants, and a package can build its C with CMake
instead. The official docs/packages.md has the details.
#Shared libraries, bound at startup22.4
An extern "library" { … } block declares functions to be looked up in a
shared library when the program starts. No C is compiled at all, so this
works even with no C compiler installed:
extern "libm.so.6" {
fn cbrt(x: f64) -> f64;
fn root(x: f64) -> f64 = "sqrt";
}
fn main() { print(cbrt(8.0), " ", root(2.0)); }
2.0 1.4142135623730951
A plain name such as "SDL2" is turned into the platform's file name
(SDL2.dll, libSDL2.dylib or libSDL2.so), and a dotted name like the
one above is used as written. Because the right name often differs between
platforms, the sidecar can map names per platform:
dylibs_linux = ["SDL2=libSDL2-2.0.so.0"];. A missing library or symbol is
reported when the program starts, naming what it tried.
When a library hands back a pointer to memory it owns, such as a
const char *, declare it as a u64: a handle that Goose can pass back
but not read through. The ffi module's cstr and bytes copy what the
pointer points at into Goose storage.
#Calling Goose from C22.5
export fn goes the other way: it makes a Goose function callable from C.
Compile the program with --header and the compiler writes a header for a
C host program, and C code with no main of its own:
var calls: i32 = 0;
export fn honk_count(times: i32) -> i32 {
calls += 1;
return times * 2 + calls;
}
export fn greeting() -> const u8[:] { return "Hellope from Goose" }
$ goose --standalone --header main.h -o main.c main.goose
$ cc main.c host.c -o host -lm -lpthread
The host calls goose_init once, which sets up the runtime, initializes the
globals and runs the Goose main if there is one. A const u8[:] result
comes back as a pointer, with its length written through an extra
argument. The bytes are not NUL-terminated.
#include <stdio.h>
#include "main.h"
int main(int argc, char **argv) {
goose_init(argc, argv);
int64_t len;
const uint8_t *msg = greeting(&len);
fwrite(msg, 1, (size_t)len, stdout);
int first = honk_count(3);
int second = honk_count(3);
printf("\n%d %d\n", first, second);
return 0;
}
Hellope from Goose 7 8
--standalone puts the runtime inside the generated C file, so the C
compiler needs nothing else. Exported functions must be called from the
thread that called goose_init.
#Exercises22.6
-
Declare
extern fn floor(x: f64) -> f64;andextern fn fmod(x: f64, y: f64) -> f64;and comparefmod(-7.0, 3.0)with Goose's own-7.0 % 3.0and-7 % 3. -
Write a C function
void count_vowels(u8_slice s, int64_t *out)in a sidecar file, and call it from Goose on a few words. -
Why can C not be given a
Goose?or au8[]directly? What would you pass instead?