Status: Superseded by ADR-284 — its D1-D3 are D19-D21 there Date: September 19, 2026 Target Version: Nikaia 0.0.9 Related: ADR-125 D6 (the blocking and async forms), D2 (a callback across the boundary), D3 (the caller owns the memory); ADR-112 (cleanup under a deadline); ADR-102 (a function-typed parameter)
ADR-125 D6 exports a function that may pause twice: blocking,
and _async with a completion callback on a library thread. Two things a
host wants of an asynchronous call were left open. Cancelling it: a user
closed the window, a request timed out, the host is shutting down — and the
work is still running on the library’s executor with nothing the host can say
to it. Streaming: a function that produces many results over time — lines
of a file, rows of a query, frames of a decoder — has no shape in D2’s table,
because a list crosses only as a whole and a Seq does not cross at all.
Both have a native answer inside the language already. A task that loses a
select race is cancelled at its next pause point and its cleanup
still runs (Part I 6.4). And a function that wants to hand out results one
at a time takes a function parameter (ADR-102), which D2
already carries across the boundary as a C function pointer and a context.
This record says how each is reached from C, and adds no construct.
<package>_cancel cancels the task behind it.int hello_fetch_async(const char* url, size_t url_len,
uint8_t* out, size_t cap, size_t* written,
void (*done)(int status, void* ctx), void* ctx,
hello_op** op);
int hello_cancel(hello_op* op);
void hello_op_free(hello_op* op);
op is the last parameter of every _async form. A caller that will never
cancel passes NULL and nothing is allocated. A caller that may cancel
receives a ticket, and hello_cancel(op) asks the task behind it to stop:
the task is cancelled at its next pause point, exactly as one that lost a
select race, and its cleanup blocks run — adopted by the library’s
runtime and bounded by cleanup-deadline, as ADR-112 says for a
program. A sync body has no pause point and is not cancelled; the ticket
exists, cancel returns OK, and done reports what the body did.
done is called exactly once, whatever happens: with the status the work
produced, or with HELLO_E_CANCELLED (-7, the seventh negative code of
ADR-125 D4) when the cancellation arrived first. cancel after
done has run is OK and does nothing; cancel twice is OK. The ticket is
freed by the caller through hello_op_free, after done — the caller owns
its memory (ADR-125 D3), and a ticket freed before done is an
E_ARGUMENT on the free, with the ticket left alone, because the library
still needs it.
The blocking form has no cancellation: the caller’s thread is inside the call, and there is nobody to ask. A caller that wants both a result on its own thread and a way out uses the async form and waits on a condition of its own, which is what a C programmer does already.
bool is the caller’s stop.pub extern "C" fn lines(path: &str, each: fn(&str) -> bool sync) throws { … }
is exported as
int hello_lines(const char* path, size_t path_len,
bool (*each)(const uint8_t* line, size_t line_len, void* ctx),
void* ctx);
The library calls each once per item, on the calling thread (blocking form)
or on a library thread (_async form). The item is a view that lives for the
callback, as ADR-125 D3 says of every callback argument. When
each returns false the function returns OK at once and produces nothing
more — the ordinary meaning of the bool inside the language, where the
body writes if !each(line) { return }. A callback that returns nothing
streams without a stop.
Nothing new crosses the boundary: D2’s callback row carries it. What this
record decides is that this is the streaming shape, and that a function
which returns a Vec of text, a Vec of handles or a Seq is refused at the
declaration with the message write it as a function that takes
fn(item) -> bool sync, so that a library author is told the idiom rather
than left to invent one.
The library produces the next item after the callback has returned, never before. So a slow consumer slows the producer, a buffer never grows behind the host’s back, and the library holds at most one item in flight per call. A host that wants a queue builds one in its callback, in memory it owns.
cleanup runs when it does.bool on the callback row, and one refusal message.hello_fetch_async with a deadline);
the caller has a clock and cancel.select is the one
thing that cancels a task today, and a cancel word for the language is a
question of its own.Nothing. The order of work, after ADR-125 §5’s async form:
cancel, op_free and E_CANCELLED, with the exactly-once
done;bool callback row in the wrapper, and the refusal message for a
returned list or Seq;#110 carries it.