Standard library
The standard library is a handful of jihoo modules in lib/, found by import
after the program’s own directories (Modules).
One of them, the prelude, needs no import.
| module | profiles | what |
|---|---|---|
| prelude | all | Option(T), Result(T, E) and the traits Ord, Hash, Show and Iterator, without an import |
strings |
hosted, native | searching, splitting, joining, trimming and parsing text |
lists |
hosted, native | sorting, mapping and searching lists, and fused pipelines (fuse!) |
maps |
hosted, native | hash maps, Map(K, V), for keys with Hash and == |
files |
hosted, native | reading and writing whole files, with messages for what failed |
iters |
all (range, take, count, sum), hosted, native |
iterators: ranges, lists, and map, filter, zip over others |
meta |
all, while compiling | the shape of code, for macros that read the code they are given |
libc |
native | declarations of C library functions |
sys |
native, freestanding | Linux syscall numbers of the target |
alloc |
native, freestanding | an arena allocator over mmap, and a checked Vec(T) |
io |
native, freestanding | output straight to file descriptors |
coro |
native | stackful coroutines |
sync |
native, freestanding | locks for threads: Mutex, Cond, Group |
thread |
native | threads on all cores, with POSIX threads |
channel |
native | channels between threads, with close |
strings, lists, maps and files work on str and lists, which hosted and native programs have.
sys, alloc, io and sync are marked #![freestanding]: they use neither
GC nor libc, only syscall, so native programs can import them too. coro,
thread and channel are marked #![native]: they use the C library to
switch stacks, start threads and allocate. All of them work on both targets,
x86_64 and aarch64 Linux.
prelude
Section titled “prelude”lib/prelude.jh declares the types and traits every program can name without
an import, in every profile:
pub enum Option(T: type) { // a value that may be missing Some(T) None}
pub enum Result(T: type, E: type) { // a value, or why there is none Ok(T) Err(E)}fn find(xs: [i64], x: i64) -> Option(i64) { let i = 0 while i < len(xs) { if xs[i] == x { return Option.Some(i) } i += 1 } return Option.None}
fn main() { let xs: [i64] = [4, 5, 6] match find(xs, 5) { Some(i) => print(i) // 1 None => print("missing") }}The prelude also declares four traits:
| trait | functions | implemented for |
|---|---|---|
Ord |
compare(a, b) -> i64, less(a, b) -> bool |
numbers (floats in IEEE total order), str, and Option, lists and tuples of two or three parts whose parts have one |
Hash |
hash(x, h: Hasher) -> Hasher |
integers, bool, str, and lists, Option and tuples of hashable parts |
Show |
show(x) -> str |
numbers, bool, str, and lists, Option, Result and tuples of parts with one |
Iterator |
type Item, next(it) -> Option((Self, Self.Item)) |
nothing: types of programs and libraries, which for walks |
hash_of(x) -> u64 is the hash of a value with Hash, the same in every
profile. print(x) prints x.show() for a value that is not a number, a
bool or a str. Show and the impls for str and lists need what hosted and
native programs have; Ord and Hash for the other types work in every
profile.
A module’s own item of the same name comes first, so a program that declares
its own Option uses that one. The prelude’s functions, types and impls are
only part of a program that uses them, so it costs nothing otherwise. The
compiler carries the prelude in itself, so it is the one it was built with
whatever the search path holds; in JIR its types are named
prelude.Option(i64) and so on.
strings
Section titled “strings”Text functions for hosted and native programs, written in jihoo on top of len, s[i],
s[lo:hi] and from_bytes (Types). A str is
bytes; these functions treat only ASCII specially (case, spaces, digits), so
they never split a multi-byte character they did not find themselves.
import strings
fn main() { let parts = strings.split("a,b,c", ",") // ["a", "b", "c"] print(strings.join(parts, " + ")) // a + b + c print(strings.to_upper(strings.trim(" hi "))) // HI match strings.parse_int(" -42 ") { Some(n) => print(n + 1) // -41 None => print("not a number") }}| function | description |
|---|---|
index_of(s, sub) -> i64 |
where sub first occurs in s, or -1 |
index_from(s, sub, from) -> i64 |
the same, from byte from on |
last_index_of(s, sub) -> i64 |
where sub last occurs, or -1 |
contains(s, sub), starts_with(s, prefix), ends_with(s, suffix) |
bool |
count(s, sub) -> i64 |
how many times sub occurs, not overlapping |
split(s, sep) -> [str] |
the parts between occurrences of sep: split("a,,b", ",") is ["a", "", "b"] |
fields(s) -> [str] |
the words: the runs of bytes that are not ASCII spaces |
lines(s) -> [str] |
the lines, without \n or \r\n |
join(parts, sep) -> str |
parts with sep between each two |
repeat(s, n) -> str |
s written n times |
replace(s, old, new) -> str |
every old replaced by new |
trim(s), trim_start(s), trim_end(s) |
s without ASCII spaces at both ends, the start, or the end |
to_upper(s), to_lower(s) |
ASCII letters in the other case |
bytes(s) -> [u8] |
the bytes of s |
from_byte(b) -> str |
the one-byte string of b |
is_space(b), is_digit(b), is_letter(b) |
ASCII classes of a byte, bool |
parse_int(s) -> Option(i64) |
the decimal integer s writes, with an optional sign and spaces around it, or None if it is not one or does not fit |
parse_float(s) -> Option(f64) |
the decimal number s writes ([+-]digits[.digits][(e|E)[+-]digits], spaces around it allowed) as the nearest f64, or None if it is not one; too large is infinite (parse_float("1e999")) |
sep, sub and old must not be empty. split, join, repeat and
replace build their results byte by byte in a [u8], so they take time in
proportion to their input and output.
Generic functions on lists for hosted and native programs. Each returns a new list: the one passed in is never changed. The element types are inferred from the arguments (Generics), or can be written first, as in the table.
import lists
fn main() { let xs: [i64] = [3, 1, 2] let sorted = lists.sort(xs) let doubled = lists.map(sorted, fn(x) -> i64 { return x * 2 }) print(lists.index_of(doubled, 4)) // 1}| function | description |
|---|---|
sort(T: Ord, xs) -> [T] |
sorted by the elements’ Ord, least first; stable, O(n log n) (merge sort) |
sort_by(T, xs, less: fn(T, T) -> bool) -> [T] |
sorted by less; stable, O(n log n) |
min(T: Ord, xs) -> Option(T), max(T: Ord, xs) -> Option(T) |
the least or greatest element, the first of equal ones; None for an empty list |
map(T, U, xs, f: fn(T) -> U) -> [U] |
f of each element |
filter(T, xs, keep: fn(T) -> bool) -> [T] |
the elements keep is true for |
fold(T, A, xs, init: A, f: fn(A, T) -> A) -> A |
f(... f(f(init, xs[0]), xs[1]) ...) |
reverse(T, xs) -> [T] |
back to front |
index_of(T, xs, x) -> i64 |
where x first is, by ==, or -1 |
contains(T, xs, x) -> bool |
whether x is in xs |
range(lo, hi) -> [i64] |
lo up to but not including hi |
map to the same type and filter reuse the list’s memory: the module has
rules map_in_place and filter_in_place that
replace them and write the result into the list passed in. They call f or
keep once for each element, in order, as map and filter do, so they
apply whatever the function does, printing and looping included. Natively, xs = lists.map(xs, f) then allocates
nothing; where xs is still used, it is copied once, at the first write.
Either way the result is the same, and the compiler checks the rules
against what they replace.
examples/words.jh puts read_line, strings and lists together: it counts
the words of its input and prints the most common ones.
lists.fuse!(xs, stage...) runs a pipeline over xs as one loop, building no
list between the stages, so it is as fast as the loop written out by hand;
map, filter and fold above make a list each (in place for map to
the same type and filter, as above).
let total = lists.fuse!(xs, filter(fn(x: i64) -> bool { return x > 0 }), map(fn(x: i64) -> f64 { return x as f64 / 2.0 }), sum()) // an f64let names = lists.fuse!(people, filter(adult), map(name_of), collect())| stage | does |
|---|---|
map(f) |
replaces each value v with f(v) |
filter(keep) |
drops v unless keep(v) |
sum() |
ends the pipeline: the sum of the values, of the type the stages end with; also when no end is given |
count() |
ends the pipeline: how many values are left, an i64 |
collect() |
ends the pipeline: a list of the values |
The functions are evaluated once, before the loop. fuse! is a
macro written in jihoo: it reads the stages with
meta and asks type_of for the type the last stage gives. A stage
it does not know stops the compilation with a message that names it.
examples/pipe.jh compares it with map, filter and fold.
Whole files for hosted and native programs, on the builtins read_file,
write_file and append_file (Builtins),
the same on the VM and natively. What failed is a message that names the file
and says why, in the words of C’s strerror.
import filesimport strings
fn main() -> i64 { let a = args() if len(a) != 1 { files.eprint("usage: count FILE") return 2 } match files.read(a[0]) { Ok(text) => print(len(strings.lines(text))) Err(msg) => { files.eprint(msg) // notes.txt: No such file or directory return 1 } } return 0}| function | description |
|---|---|
read(path) -> Result(str, str) |
the whole file, or why it cannot be read |
write(path, text) -> Result(unit, str) |
makes the file hold text, creating it if missing, or says why it could not |
append(path, text) -> Result(unit, str) |
adds text at the end of the file, creating it if missing, or says why it could not |
eprint(text) |
writes text and a newline to standard error |
message(path, err) -> str |
path: and what error number err means |
error_text(err) -> str |
what error number err means ("Permission denied"), or "error N" for one the module does not know |
examples/json.jh, a small jq, puts files, args(), strings and
parse_float together.
The module also names the error numbers the builtins give most often:
ENOENT, EACCES, EISDIR, ENOTDIR, EEXIST, EINVAL, ENOSPC and a
few more, with Linux’s values, which are the same on both targets.
Hash maps for hosted and native programs. A Map(K, V) holds a value for each
of some keys, found by the keys’ Hash
and ==. A map is a value, like a list: set and remove return a new map
and leave the one passed in alone, and m = maps.set(m, k, v) changes m in
place, natively without copying it.
import maps
fn main() { let counts = maps.new(str, i64) for w in ["to", "be", "or", "not", "to"] { match maps.get(counts, w) { Some(n) => counts = maps.set(counts, w, n + 1) None => counts = maps.set(counts, w, 1) } } print(maps.get(counts, "to")) // Some(2) print(maps.size(counts)) // 4}| function | description |
|---|---|
new(K: Hash, V) -> Map(K, V) |
an empty map |
size(m) -> i64 |
how many keys m has |
get(m, k) -> Option(V) |
the value for k, if any |
has(m, k) -> bool |
whether there is a value for k |
set(m, k, v) -> Map(K, V) |
m with v for k |
remove(m, k) -> Map(K, V) |
m without k |
keys(m) -> [K], values(m) -> [V], entries(m) -> [(K, V)] |
its contents, in no useful order but the same in every profile |
The keys are kept in buckets by their hash, two to a bucket on average at most; a lookup hashes the key once and compares it with the few keys in its bucket.
Iterators, values of types with the prelude’s
Iterator, which for
walks, and adapters that make new ones out of them. Nothing runs until
something walks the result, and no list is built between the stages:
import iters
fn main() { let xs: [i64] = [1, 2, 3, 4, 5] let odd = iters.filter(iters.of(xs), fn(x: i64) -> bool { return x % 2 == 1 }) for x in iters.map(odd, fn(x: i64) -> i64 { return x * x }) { print(x) // 1, 9, 25 } print(iters.sum(iters.take(iters.range(0, 1000000), 4))) // 6}| function | description |
|---|---|
range(lo, hi) -> Range |
the integers from lo up to hi - 1 |
of(xs: [T]) -> Of(T) |
the elements of a list |
map(it, f: fn(I.Item) -> U) -> Map(I, U) |
f of each item |
filter(it, keep: fn(I.Item) -> bool) -> Filter(I) |
the items keep is true for |
take(it, n) -> Take(I) |
the first n items |
zip(a, b) -> Zip(A, B) |
pairs of items, until either ends |
collect(it) -> [I.Item] |
the items, in a list |
fold(it, init, f: fn(A, I.Item) -> A) -> A |
the items folded into init |
count(it) -> i64, sum(it) -> I.Item |
how many items, and their sum |
An iterator is a value: walking one leaves it as it was. Natively the inner
iterator of an adapter, and a list it walks, moves from one round to the
next and is never copied. range, take, count and sum work in freestanding programs
too; the others need closures or lists.
The shape of code, one level deep, for macros and
functions on code. meta.node(e) reads the text of e and gives a Node;
its parts are code again, to look into further or to put back with holes.
import meta
// `doubled!(f(a, b))` adds up the arguments twice: `a + a + b + b`.macro doubled(call: expr) -> expr { match meta.node(call) { Call(f, args) => { let out = quote(0) for a in args { out = quote($out + $a + $a) } return out } _ => return call }}Node |
for |
|---|---|
Call(callee, args) |
f(a, b) or m.f(a, b): the callee as written, a str, and the arguments, [expr] |
Name(n) |
a name, such as x or m.f |
Int(n) |
an integer literal, an i64 |
Str(s) |
a string literal: its value |
Binary(op, lhs, rhs) |
the operator that applies last, such as "+" in a + b * c, and both sides |
Func(params, f) |
an anonymous function: the names of its parameters, and the whole function |
Other |
anything else: unary operators, fields, indexing, casts, literals of structs, arrays and floats, match, macro calls |
meta is marked #![comptime]: it only exists while compiling, and none of
it, nor the strings it uses, is part of the program
(Modules), so any program can
import it, freestanding ones included. It is written in jihoo on top of
code_text and to_code (Macro builtins).
lists.fuse! uses it to read the stages of a pipeline.
meta.shape(t) gives the parts of a struct or enum type, where the macro is
called: Struct(fields), the field names, Enum(variants), each a
Variant { name, values } with how many values it holds, or Other. t
may be a generic type’s name alone (Pair), and meta.params(t) then gives
its parameters (["A", "B"]), and none for any other type.
meta.derive!(Show, Hash, Ord, T) is an item macro that writes impls of
those traits for the struct or enum T,
or, for a generic type’s name, generic impls, part by part:
import meta
struct Version { major: i64, minor: i64 }enum Token { Num(i64), Op(str, i64), End }
meta.derive!(Show, Hash, Ord, Version)meta.derive!(Show, Ord, Token)
fn main() { print(Version { major: 1, minor: 2 }) // Version { major: 1, minor: 2 } print(Token.Op("+", 2) < Token.End) // true: variants in declared order}#![native]import libc
fn main() { let p = libc.malloc(16) libc.snprintf(p, 16, "%ld", 42 as i64) libc.puts(p) libc.free(p)}lib/libc.jh declares C functions with pub extern fn, so they are used as
libc.name. Only the ones a program calls are linked in. C types map as
described in Calling C.
| group | functions |
|---|---|
| stdio | printf, snprintf, puts, putchar, getchar, fflush |
| stdlib | malloc, calloc, realloc, free, exit, abort, atoi, strtol, strtod, atof, getenv, qsort |
| string | strlen, strcmp, strncmp, strchr, memcpy, memmove, memset, memcmp |
| unistd | read, write, close |
math (f64) |
sqrt, cbrt, pow, exp, log, log2, log10, sin, cos, tan, asin, acos, atan, atan2, hypot, fabs, floor, ceil, round, trunc, fmod |
math (f32) |
sqrtf, sinf, cosf |
Anything missing can be declared with extern fn in the program itself;
declarations of the same function in several modules must agree.
The Linux system call numbers of the target, in one file per target:
lib/sys.x86_64.jh and lib/sys.aarch64.jh
(per-target modules).
#![freestanding]import sys
fn _start() -> i64 { syscall(sys.WRITE, 1, "hi\n", 3) return 0}| item | x86_64 | aarch64 |
|---|---|---|
WRITE |
1 | 64 |
MMAP |
9 | 222 |
MUNMAP |
11 | 215 |
EXIT |
60 | 93 |
FUTEX |
202 | 98 |
#![freestanding]import alloc
fn _start() -> i64 { let arena = alloc.arena_new(1 << 20) // 1 MiB from mmap let v = alloc.vec_new(i64, &arena) alloc.push(i64, &v, 42) alloc.push(i64, &v, 7) let n = alloc.get(i64, &v, 0) + alloc.pop(i64, &v) // 49 alloc.arena_free(&arena) return n}Misuse stops the program with trap(), as an array
index out of bounds does: an index outside the Vec, pop on an empty Vec, an
alignment that is not a power of two, a negative size or count. Running out
of memory is not misuse: alloc gives null and push gives false, and
nothing changes. The structs’ fields are private, so only the module changes
them, and every size computation is checked for overflow.
An arena hands out memory from one block, and everything is freed at once by
arena_free. Nothing it handed out may be used afterwards.
| item | description |
|---|---|
struct Arena |
a block of memory, of which a part is handed out; its fields are private |
fn arena_new(cap: i64) -> Arena |
a new arena of cap bytes from mmap, or one with no room (arena_cap is 0) if cap is not positive or the kernel refused |
fn arena_free(a: *Arena) |
gives the memory back with munmap; afterwards the arena has no room. Freeing twice does nothing |
fn arena_used(a: *Arena) -> i64 |
the bytes handed out so far, padding included |
fn arena_cap(a: *Arena) -> i64 |
the bytes the arena has in all |
fn alloc(a: *Arena, size: i64, align: i64) -> *u8 |
size bytes aligned to align, or null when the arena has no room; traps if size is negative or align is not a power of two |
fn alloc_array(comptime T: type, a: *Arena, n: i64) -> *T |
room for n values of type T, or null; traps if n is negative |
PROT_READ, PROT_WRITE, MAP_PRIVATE, MAP_ANONYMOUS |
the mmap constants it uses |
Vec(T)
Section titled “Vec(T)”A growable array in an arena. Its storage doubles when full (4, 8, 16, …); the old storage stays in the arena until the arena is freed.
| item | description |
|---|---|
struct Vec(T: type) |
elements in an arena; its fields are private |
fn vec_new(comptime T: type, a: *Arena) -> Vec(T) |
an empty vector allocating from a |
fn vec_len(comptime T: type, v: *Vec(T)) -> i64 |
the number of elements |
fn vec_cap(comptime T: type, v: *Vec(T)) -> i64 |
room before it grows |
fn vec_data(comptime T: type, v: *Vec(T)) -> *T |
the elements as a raw pointer, for a syscall or C; not checked, and valid only until the next push |
fn push(comptime T: type, v: *Vec(T), x: T) -> bool |
appends x; false, changing nothing, if the arena has no room left |
fn get(comptime T: type, v: *Vec(T), i: i64) -> T |
element i; traps unless 0 <= i < vec_len |
fn set(comptime T: type, v: *Vec(T), i: i64, x: T) |
replaces element i; traps unless 0 <= i < vec_len |
fn pop(comptime T: type, v: *Vec(T)) -> T |
removes and returns the last element; traps if the vector is empty |
fn clear(comptime T: type, v: *Vec(T)) |
removes every element, keeping the storage |
What the library cannot check:
- A
Vecholds a pointer to its elements, so a copy (let w = v) shares them with the original, and pushing to both makes them overwrite each other. Keep oneVecand pass a pointer to it, as the functions take it. - Memory from an arena must not be used after
arena_free, and aVecmust not outlive its arena.
#![freestanding]import io
fn _start() -> i64 { io.puts("hello\n") io.print_int(-42) return 0}| item | description |
|---|---|
const STDOUT = 1, const STDERR = 2 |
file descriptors |
fn write(fd: i64, buf: *u8, len: i64) -> i64 |
the write syscall: bytes written, or a negative error |
fn strlen(s: *u8) -> i64 |
the length of a NUL-terminated string |
fn puts(s: *u8) |
writes a NUL-terminated string, such as a literal, to stdout (no newline added) |
fn print_int(n: i64) |
writes n in decimal and a newline to stdout |
fn exit(code: i64) |
ends the program with exit status code |
Stackful coroutines for native programs (examples/coroutines.jh), with the C
library’s makecontext and swapcontext doing the switching. A coroutine runs a function on a stack of its own;
yield hands a value back to whoever resumed it and waits, and resume
continues it until its next yield.
#![native]import coroimport libc
fn squares(c: *coro.Coro) { let i = 1 while i <= 3 { coro.yield(c, i * i) i = i + 1 }}
fn main() -> i64 { let c = coro.new(squares, 64 * 1024) // a 64 KiB stack from malloc while coro.resume(&c) { libc.printf("%ld\n", c.value) // 1, 4, 9 } coro.free(&c) return 0}| item | description |
|---|---|
struct Coro |
a coroutine; its public fields are value: i64, the last yielded value (also a way to pass one in), and data: *u8, for the body’s own state; the others are private to the library |
fn new(body: fn(*Coro), size: i64) -> Coro |
a coroutine that will run body on a stack of size bytes; it starts on the first resume, and is done from the start if no stack could be had |
fn resume(c: *Coro) -> bool |
runs c until it yields (true: c.value is the value) or its body returns (false); resuming a finished coroutine does nothing |
fn yield(c: *Coro, v: i64) |
called by the body: hands v to the resumer, and returns when resumed |
fn free(c: *Coro) |
frees the stack and the saved contexts; the coroutine must not be resumed afterwards |
- A coroutine can resume others, so they compose into pipelines.
- Coroutines are cooperative and run on one thread. A started
Coromust not move, since its body holds a pointer to it. - Each coroutine has a C
ucontext_tfor itself and one for its resumer.resumeandyieldswap between them withswapcontext; the firstresumesets up the coroutine’s withmakecontext, whoseuc_linkmakes the resumer go on when the body returns. jihoo treats aucontext_tas bytes (8 KiB, more than glibc’s 968 on x86_64 and 4560 on aarch64), apart from the fields at its start, which are at the same offsets on both. makecontextandswapcontextare in glibc but not in every C library (musl has neither).
Locks for threads that share memory (examples/threads.jh), built on the
atomic builtins and the Linux kernel’s
futexes: a thread that has to wait sleeps in the kernel instead of spinning,
and a lock nobody waits for is taken and released without a system call. The
module uses no libc, so freestanding programs can import it as well as native
ones; native programs start threads with thread.
#![native]import sync
struct Account { lock: sync.Mutex balance: i64}
fn deposit(a: *Account, n: i64) { sync.lock(&a.lock) a.balance = a.balance + n sync.unlock(&a.lock)}| item | description |
|---|---|
struct Mutex |
a lock that one thread holds at a time; its fields are private |
fn mutex_new() -> Mutex |
a new, unlocked Mutex |
fn lock(m: *Mutex) |
takes the lock, waiting while another thread holds it; a thread that takes a lock it holds already waits forever |
fn try_lock(m: *Mutex) -> bool |
takes the lock if it is free, and says whether it did |
fn unlock(m: *Mutex) |
releases the lock, waking a thread that waits for it; the program stops if the Mutex is not locked |
struct Cond |
a condition variable; its fields are private |
fn cond_new() -> Cond |
a new Cond |
fn wait(c: *Cond, m: *Mutex) |
releases m (which the thread holds), sleeps until a signal or broadcast, and takes m again; it can also return without one, so wait in a loop that checks the condition |
fn signal(c: *Cond) |
wakes one thread waiting on c |
fn broadcast(c: *Cond) |
wakes every thread waiting on c |
struct Group |
a count of unfinished work, like Go’s sync.WaitGroup; its fields are private |
fn group_new() -> Group |
a new Group with a count of zero |
fn group_add(g: *Group, n: i64) |
adds n (which may be negative) to the count; at zero, group_wait returns. The program stops if the count leaves 0 to 2^31 - 1 |
fn group_done(g: *Group) |
group_add(g, -1): one piece of work is finished |
fn group_wait(g: *Group) |
waits until the count is zero |
- A Mutex, Cond or Group is shared by pointer and must stay where it is while threads use it: a copy is an unrelated second one.
- The Mutex is Ulrich Drepper’s from “Futexes Are Tricky”: its state is 0
(free), 1 (held) or 2 (held, and a thread may be sleeping on it), and
unlockmakes afutexcall only in state 2. A Cond is a sequence number that every signal changes, so a waiter that has released the Mutex does not miss a signal. All futex calls are private to the process. - A Mutex is not fair: a thread that unlocks and locks again at once may get the lock again before one that was waiting.
thread
Section titled “thread”Threads for native programs, on the C library’s POSIX threads: functions that
run at the same time, on every core of the machine. Coordinate them with the
atomics or sync (Threads and atomics).
go f(x) starts a thread with less ceremony, but one that cannot be joined;
this library’s threads give back their result, and report failing to start.
#![native]import thread
fn double(arg: *u8) -> i64 { return *(arg as *i64) * 2}
fn main() -> i64 { let x = 21 match thread.spawn(double, &x as *u8) { Some(t) => return thread.join(&t) // exit status 42 None => return 1 }}| item | description |
|---|---|
struct Thread |
a started thread; its fields are private |
fn spawn(body: fn(*u8) -> i64, arg: *u8) -> Option(Thread) |
starts a thread that calls body(arg); None if the system has no thread or memory to spare |
fn join(t: *Thread) -> i64 |
waits for t to finish and gives what body returned; joining the same Thread twice stops the program (a copy of one does not know the other was joined, so join only one) |
fn cpus() -> i64 |
the number of cores online, at least 1 |
fn yield() |
lets other threads run before this one goes on |
fn sleep_ms(ms: i64) |
sleeps for ms milliseconds |
fn now_ns() -> i64, fn now_ms() -> i64 |
the time on a clock that only goes forward (CLOCK_MONOTONIC), in nanoseconds or milliseconds: subtract two readings to time something |
- Every thread must be joined once:
spawntakes a little memory frommallocfor the thread’s start, whichjoinfrees. - What
argpoints to must outlive the thread, for instance by joining the thread before the function holding it returns. - A thread that traps stops the whole program, as it would in C.
mainreturning ends the program, even while threads are still running.
channel
Section titled “channel”Channels between threads (examples/pipeline.jh): what hosted programs’
chan is for tasks, for native threads
started with thread. Values arrive in the order they were sent,
and close tells receivers that no more will come, as in Go.
#![native]import channelimport thread
fn squares(arg: *u8) -> i64 { let c = *(arg as *channel.Chan(i64)) let i = 1 while i <= 3 { channel.send(i64, c, i * i) i = i + 1 } channel.close(i64, c) return 0}
fn main() -> i64 { match channel.new(i64, 0) { Some(c) => { let t = thread.spawn(squares, &c as *u8) let sum = 0 let more = true while more { match channel.recv(i64, c) { Some(x) => sum = sum + x // 1, 4, 9 None => more = false // closed, and nothing left } } match t { Some(h) => thread.join(&h) None => {} } channel.free(i64, c) return sum // exit status 14 } None => return 1 }}| item | description |
|---|---|
struct Chan(T: type) |
a channel of T values; a handle, so copies of it are the same channel; its fields are private |
fn new(comptime T: type, cap: i64) -> Option(Chan(T)) |
a channel that buffers up to cap values; with cap 0 none, so that each sender waits for a receiver; None if there is no memory. A negative cap stops the program |
fn send(comptime T: type, c: Chan(T), v: T) |
sends v, waiting while the channel is full (unbuffered: until a receiver took it); sending on a closed channel stops the program |
fn recv(comptime T: type, c: Chan(T)) -> Option(T) |
the next value, waiting until there is one; None once the channel is closed and empty |
fn close(comptime T: type, c: Chan(T)) |
no more values will be sent; closing twice stops the program |
fn count(comptime T: type, c: Chan(T)) -> i64 |
the values sent and not yet received |
fn free(comptime T: type, c: Chan(T)) |
gives the channel’s memory back; nothing may use it afterwards |
- A channel is a ring buffer of bytes in memory from
malloc, guarded by async.Mutex, with onesync.Condfor receivers and one for senders. It holds bytes rather thanTs so that oneSelectcan wait on channels of different types; the functions that takeTare typed doors to it. - Any type can be sent; values are copied in and out, as by assignment.
Select
Section titled “Select”A Select waits on several channels at once, like hosted programs’
select. select is a keyword,
so the functions are wait and poll. Cases are added once and stay, so a
loop can wait on the same cases again and again:
let n: i64 = 0let stop = falselet sel = channel.select_new()let got_n = channel.on_recv(i64, &sel, numbers, &n) // case 0let got_stop = channel.on_recv(bool, &sel, quit, &stop) // case 1let running = truewhile running { let i = channel.wait(&sel) // the case that went ahead if !channel.received(&sel) { channel.off(&sel, i) // closed: leave it out from now on running = i != got_n } else if i == got_n { total = total + n } else { running = false }}| item | description |
|---|---|
struct Select |
the cases of a select; its fields are private |
fn select_new() -> Select |
a Select without cases |
fn on_recv(comptime T: type, sel: *Select, c: Chan(T), dst: *T) -> i64 |
adds a case that receives from c into *dst, and gives its number (cases count from 0) |
fn on_send(comptime T: type, sel: *Select, c: Chan(T), src: *T) -> i64 |
adds a case that sends *src on c, read when the case goes ahead, and gives its number |
fn wait(sel: *Select) -> i64 |
does the first case that can go ahead, waiting until one can, and gives its number |
fn poll(sel: *Select) -> i64 |
the same without waiting: -1 if no case can go ahead, like a _ arm |
fn wait_ms(sel: *Select, ms: i64) -> i64 |
like wait, but -1 if no case could go ahead within ms milliseconds; each call starts its own clock, as case <-time.After(d) does in a Go loop. Zero or less is poll; with no case on, it only sleeps |
fn received(sel: *Select) -> bool |
whether the receive that wait or poll just did got a value (false: its channel was closed and empty, and *dst is unchanged) |
fn off(sel: *Select, i: i64) |
leaves case i out from now on, as Go programs do by setting a channel to nil |
const MAX_CASES = 16 |
the most cases one Select has |
- A receive case can go ahead when its channel has a value, or is closed and
empty. A send case can go ahead when its channel has room; on an
unbuffered channel, when a receiver is waiting (in
recv, or in awaitwith a receive case on it), and then it does not wait for the receiver to take the value. - If several cases can go ahead, the first added wins, as with hosted
select. - What
dstandsrcpoint to must stay valid while the Select is used. - Misuse stops the program:
waitwith no case on (it would wait forever), more thanMAX_CASEScases,offwith a number that is not a case, and a send case on a closed channel. - While it waits, a Select links a node per case into its channels and
sleeps on a futex word of its own, which a channel bumps whenever a value
comes or goes or it closes; it then looks at its cases again.
wait_msgives the futex only the time left until its deadline, onthread.now_ns’s clock, so wakeups that change nothing do not make it wait longer.
A timeout in a loop, such as a heartbeat while waiting for work:
let i = channel.wait_ms(&sel, 1000)while i == -1 { libc.printf("still waiting\n") // once a second i = channel.wait_ms(&sel, 1000)}