Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Calling C

SSPUR calls C directly, with no wrapper library, and builds C libraries from SSPUR code. Both directions keep SSPUR’s checks: C calls are an effect, and every value that crosses the boundary is checked.

Declaring C functions

extern fn hypot(x: F64, y: F64) -> F64 ! ffi from "m"
extern fn cbrt(x: F64) -> F64 ! ffi from "m"
extern fn strlen(s: Str) -> U64 ! ffi
extern fn getenv(name: Str) -> Opt[Str] ! ffi

An extern fn has no body and declares exactly ! ffi. from "m" links libm (a path also works; leave it out for libc), and as "sym" gives a different C symbol name. Signatures use C widths: Int is int64_t, and I8 I16 I32 U8 U16 U32 U64, F32, F64, Bool, Str (const char*), Opt[Str] (a nullable string) and List[W] parameters (const W*) map to what you would expect. Callers see Int and F64.

Out-of-range integers, NUL bytes in a Str, NULL for a Str result and invalid UTF-8 all trap with an ffi: message instead of corrupting memory.

Using them

Wrap C calls in ordinary functions that add types, errors and contracts:

type GeoErr = BadInput{what: Str}

fn dist(x1: F64, y1: F64, x2: F64, y2: F64) -> F64 ! ffi
= hypot(x2 - x1, y2 - y1)

fn cube_side(volume: F64) -> F64 ! ffi, fail[GeoErr]
= do
  if volume < 0.0 then raise BadInput{what: "negative volume {volume}"}
  cbrt(volume)

fn name_len(name: Str) -> Int ! ffi
= strlen(name)

fn has_home() -> Bool ! ffi
= getenv("HOME").is_some
$ sspur run geo.ssp
dist 5.0
side 3.0 0.0
len 5
$ sspur test geo.ssp
2 passed, 0 failed

Native code calls C directly, and so does the interpreter, through dlsym, so tests and --interp work the same.

Generating bindings

sspur bind parses a C header with clang and prints extern declarations, listing what it couldn’t translate:

#include <stdint.h>

typedef struct point point;

double point_norm(double x, double y);
int32_t point_quadrant(double x, double y);
const char *point_label(int32_t quadrant);
void point_free(point *p);
$ sspur bind point.h --lib point
// generated by sspur bind from point.h
extern fn point_norm(x: F64, y: F64) -> F64 ! ffi from "point"
extern fn point_quadrant(x: F64, y: F64) -> I32 ! ffi from "point"
extern fn point_label(quadrant: I32) -> Opt[Str] ! ffi from "point"
// skipped 1 declaration(s):
//   point_free: parameter p has type point * (unsupported pointer)

Calling SSPUR from C

sspur export-c builds a static library (or a shared one with --shared) and a header from every function whose parameters and result are Int, F64, Bool or Str:

$ sspur export-c geo.ssp -o libgeo
export  geo_dist
export  geo_cube_side
export  geo_name_len
export  geo_has_home
export  geo_side_or_zero
export  geo_main
...
$ cat libgeo.h
...
int32_t geo_dist(double x1, double y1, double x2, double y2, double* out);
int32_t geo_cube_side(double volume, double* out);
int32_t geo_name_len(const char* name, int64_t* out);
int32_t geo_has_home(bool* out);
int32_t geo_side_or_zero(double v, double* out);
int32_t geo_main(void);

const char* geo_last_error(void);
void geo_free(void* p);
...

The last line it prints is the linker flags for your platform: -L. -lgeo -lm on macOS, -L. -lgeo -lm -lpthread on Linux.

Each function returns 0, or GEO_RAISED (100) for an unhandled error, or a trap code, and writes its result through the last pointer. geo_last_error() describes the failure, and geo_free releases returned strings:

// sspur export-c geo.ssp -o libgeo && cc main.c -L. -lgeo -lm -o geo && ./geo
#include <stdio.h>
#include "libgeo.h"

int main(void) {
    double d = 0, side = 0;
    if (geo_dist(0, 0, 3, 4, &d) == GEO_OK) printf("dist = %g\n", d);
    if (geo_cube_side(-8, &side) == GEO_RAISED) printf("cube_side: %s\n", geo_last_error());
    return 0;
}

This prints dist = 5 and cube_side: unhandled error: BadInput{what: "negative volume -8.0"}. Calls into the library must not run concurrently. ADR 0014 has the design and the full type mapping.