Kaappi Extensions¶
Kaappi-specific features not part of R7RS or standard SRFIs.
FFI (Foreign Function Interface)¶
ffi-open¶
Syntax: (ffi-open path)
Opens a shared library (.so on Linux, .dylib on macOS) and returns a
library handle. The path is a string specifying the library file.
The handle is used with ffi-fn to bind C functions and must be closed
with ffi-close when no longer needed.
kaappi> (define lib (ffi-open "libm.dylib")) ; macOS — "libm.so.6" on Linux
kaappi> lib
;=> #<ffi-library "libm.dylib">
ffi-fn¶
Syntax: (ffi-fn lib "c-name" '(param-types ...) 'return-type)
Binds a C function from the shared library lib and returns a callable
Scheme procedure. The c-name is a string naming the C function.
Parameter and return types are specified as symbols: int, long,
double, float, string, pointer, void, bool, uint8, int8,
int16, int32, int64, uint16, uint32, uint64, size_t, char.
kaappi> (define lib (ffi-open "libm.dylib")) ; macOS — "libm.so.6" on Linux
kaappi> (define c-sqrt (ffi-fn lib "sqrt" '(double) 'double))
kaappi> (c-sqrt 16.0)
;=> 4.0
kaappi> (define c-pow (ffi-fn lib "pow" '(double double) 'double))
kaappi> (c-pow 2.0 10.0)
;=> 1024.0
Note
Type annotations must match the C function signature exactly. Passing a mismatched type causes undefined behavior.
ffi-close¶
Syntax: (ffi-close lib)
Closes the shared library handle lib, releasing the associated
resources. Any procedures previously bound with ffi-fn from this
library become invalid and must not be called after closing.
ffi-callback¶
Syntax: (ffi-callback proc '(param-types ...) 'return-type)
Wraps proc as a C function pointer, for C APIs that take one (a
comparator for qsort, an event handler, ...). Pass the returned
callback object where the C function expects the function pointer — an
ffi-fn parameter declared pointer. When C invokes it, proc is
called with the marshaled arguments; pointer arguments arrive as
numbers (raw addresses).
Only a fixed set of signatures is supported: (pointer pointer) -> int
(the qsort-comparator shape), (pointer pointer) -> void,
(pointer) -> int, (pointer) -> void, (int pointer) -> int,
(int) -> void, and () -> void. Anything else raises an "unsupported
callback signature" error. At most 32 callbacks can be live at once —
release them with ffi-callback-release.
An error raised inside proc is captured and re-raised on the Scheme
side when the enclosing C call returns, so guard around the FFI call
works as expected. Unavailable under --sandbox and on WebAssembly.
;; C (in your own shared library):
;; int apply_pp(int (*f)(void *, void *), long a, long b)
;; { return f((void *)a, (void *)b); }
(define apply-pp (ffi-fn mylib "apply_pp" '(pointer long long) 'int))
(define cb (ffi-callback (lambda (a b) (- a b)) '(pointer pointer) 'int))
(apply-pp cb 10 3) ;; returns 7
(ffi-callback-release cb)
See also: ffi-callback-release,
ffi-fn,
C Extensions guide
ffi-callback-release¶
Syntax: (ffi-callback-release callback)
Releases the slot held by callback (created with ffi-callback).
Callback slots are a fixed resource — at most 32 live at once — so
release a callback as soon as no C library holds its function pointer
anymore. After release, C code must not invoke the pointer again.
See also: ffi-callback
fd->port¶
Syntax: (fd->port fd)
Wraps fd — a raw OS file descriptor obtained through the FFI, such as
a socket — as a bidirectional binary port. Reads and writes on the port
go through the same non-blocking, reactor-integrated path as Kaappi's
file ports: an operation that would block suspends the calling fiber
instead of the OS thread, and the descriptor is switched to non-blocking
mode the first time it is touched under the fiber scheduler. Writes are
buffered; use flush-output-port to force them out.
The port takes ownership of the descriptor: close-port closes the
fd, wakes any fiber parked on it, and unregisters it from the reactor.
Don't also close the fd through your own FFI path — the number could be
recycled onto an unrelated port.
The standard streams (fd 0, 1, 2) are rejected, since their blocking
semantics are relied on elsewhere. Not available under --sandbox
(where the whole (kaappi ffi) library is blocked) or on WebAssembly.
This is the bridge that gives FFI socket libraries fiber-friendly I/O with no C changes — kaappi-net wraps every connected socket exactly like this:
(define (socket-port fd) (fd->port fd))
(define conn (socket-port (c-accept listener))) ; fd from an FFI call
(spawn (lambda ()
(let ((request (read-bytevector 4096 conn))) ; parks this fiber only
(write-bytevector (handle request) conn)
(flush-output-port conn)
(close-port conn)))) ; closes the fd too
See also: ffi-fn, spawn,
flush-output-port,
Concurrency guide
Green Threads (Fibers)¶
spawn¶
Syntax: (spawn thunk)
Creates and immediately starts a new fiber that executes thunk (a
procedure of zero arguments). Fibers are cooperatively scheduled green
threads that run on the VM's event loop. Returns a fiber object that can
be passed to fiber-join to retrieve the result.
kaappi> (define f (spawn (lambda () (* 6 7))))
kaappi> (fiber-join f)
;=> 42
kaappi> (define f2 (spawn (lambda ()
(display "fiber running\n")
'done)))
fiber running
kaappi> (fiber-join f2)
;=> done
See also: fiber-join, yield,
thread-start! (OS thread equivalent)
yield¶
Syntax: (yield)
Voluntarily gives up the current fiber's time slice, allowing the
scheduler to run other ready fibers. Returns an unspecified value.
Since fibers are cooperatively scheduled, long-running computations
should call yield periodically to avoid starving other fibers.
kaappi> (spawn (lambda ()
(let loop ((i 0))
(when (< i 5)
(display i) (display " ")
(yield)
(loop (+ i 1))))))
;=> #<fiber>
kaappi> 0 1 2 3 4
See also: spawn,
thread-yield! (OS thread equivalent)
fiber?¶
Syntax: (fiber? obj)
Returns #t if obj is a fiber, #f otherwise.
kaappi> (fiber? (spawn (lambda () 42)))
;=> #t
kaappi> (fiber? (current-thread))
;=> #t
kaappi> (fiber? 'not-a-fiber)
;=> #f
See also: spawn
fiber-join¶
Syntax: (fiber-join fiber)
Blocks until fiber completes and returns its result value. If the
fiber raised an exception, fiber-join re-raises it in the calling
context.
kaappi> (define f (spawn (lambda () (+ 10 20))))
kaappi> (fiber-join f)
;=> 30
kaappi> (define f2 (spawn (lambda ()
(let loop ((i 0) (acc 0))
(if (= i 100)
acc
(begin (yield) (loop (+ i 1) (+ acc i))))))))
kaappi> (fiber-join f2)
;=> 4950
See also: spawn,
thread-join! (OS thread equivalent)
Channels¶
make-channel¶
Syntax: (make-channel) | (make-channel capacity)
Creates a new channel for passing values between fibers — and, when
handed to an OS thread through its thread-start! thunk, between
threads (see the Concurrency
guide). Values are
delivered first-in, first-out, without shared mutable state.
With no argument the channel is unbounded: channel-send queues the
value and returns immediately. With capacity (a non-negative exact
integer), at most capacity values may be queued: a send on a full
channel blocks until a receive frees a slot, so a bounded channel
applies backpressure to a producer that outpaces its consumer.
A capacity of 0 creates a rendezvous channel (a synchronous,
unbuffered channel, like Go's make(chan T)): channel-send completes
only when a receiver takes the value, and channel-receive completes
only when a sender provides one — whichever side arrives first waits
for the other. A completed send therefore guarantees a receiver was
committed to take the value, which makes rendezvous channels a
synchronization point as well as a data conduit. Timeouts on either
operation remain the escape hatch when no counterpart ever arrives, and
an unpaired send or receive that can never be matched raises the usual
deadlock error rather than hanging.
kaappi> (define rv (make-channel 0))
kaappi> (spawn (lambda () (channel-send rv 'x))) ; parks until a receiver
kaappi> (channel-receive rv)
;=> x
kaappi> (define ch (make-channel)) ; unbounded
kaappi> (define bd (make-channel 8)) ; at most 8 queued values
kaappi> ch
;=> #<channel>
See also: channel-send,
channel-receive, channel-close!,
channel?
channel-send¶
Syntax: (channel-send channel value) | (channel-send channel value timeout) | (channel-send channel value timeout timeout-val)
Sends value on channel. On an unbounded channel (the default) the
value is queued and the send returns immediately; on a bounded channel a
send blocks while the channel is full, resuming when a receive frees a
slot; on a rendezvous channel (capacity 0) a send blocks until a
receiver takes the value. Returns an unspecified value.
If timeout is given (a time object or number of seconds) and the send cannot complete within that time, returns timeout-val if supplied, or raises an error otherwise.
Sending on a closed channel raises an error, as does sending an
eof-object — at the receiving end it would be indistinguishable from the
end-of-stream marker channel-close! produces. A send that can never
complete raises a deadlock error instead of hanging.
kaappi> (define ch (make-channel))
kaappi> (channel-send ch 42) ; unbounded: returns immediately
kaappi> (channel-receive ch)
;=> 42
kaappi> (define bd (make-channel 1))
kaappi> (channel-send bd 'a)
kaappi> (channel-send bd 'b 0.1 'full) ; full for 100 ms -> timeout value
;=> full
See also: channel-receive,
channel-close!, make-channel
channel-receive¶
Syntax: (channel-receive channel) | (channel-receive channel timeout) | (channel-receive channel timeout timeout-val)
Receives the oldest queued value from channel. If the channel is empty, the receiving fiber blocks until a sender provides a value. If the channel has been closed and all queued values have been received, returns an eof-object.
If timeout is given (a time object or number of seconds) and no value arrives within that time, returns timeout-val if supplied, or raises an error otherwise. A receive that can never be satisfied raises a deadlock error instead of hanging.
kaappi> (define ch (make-channel))
kaappi> (spawn (lambda ()
(channel-send ch 'hello)
(channel-send ch 'world)))
;=> #<fiber>
kaappi> (channel-receive ch)
;=> hello
kaappi> (channel-receive ch)
;=> world
kaappi> (channel-receive ch 0.1 'nothing) ; empty for 100 ms -> timeout value
;=> nothing
See also: channel-send,
channel-close!, make-channel
channel-close!¶
Syntax: (channel-close! channel)
Closes channel, signalling end-of-stream: subsequent sends raise an
error, values already queued are still delivered, and once the channel
is drained every channel-receive returns an eof-object. Receivers
already blocked on an empty channel are woken and get the eof-object
immediately. Closing an already-closed channel has no effect. Returns
an unspecified value.
Closing replaces ad-hoc sentinel values as the way a producer tells consumers no more data is coming:
kaappi> (define ch (make-channel))
kaappi> (spawn (lambda ()
(for-each (lambda (x) (channel-send ch x)) '(1 2 3))
(channel-close! ch)))
;=> #<fiber>
kaappi> (let loop ((total 0))
(let ((v (channel-receive ch)))
(if (eof-object? v) total (loop (+ total v)))))
;=> 6
See also: channel-closed?,
channel-send, channel-receive
channel-closed?¶
Syntax: (channel-closed? channel)
Returns #t if channel has been closed with channel-close!, #f
otherwise. A closed channel may still hold queued values —
channel-closed? answering #t does not mean the stream has been fully
consumed; only a channel-receive returning an eof-object does.
kaappi> (define ch (make-channel))
kaappi> (channel-closed? ch)
;=> #f
kaappi> (channel-send ch 1)
kaappi> (channel-close! ch)
kaappi> (channel-closed? ch)
;=> #t
kaappi> (channel-receive ch) ; queued value still delivered
;=> 1
kaappi> (eof-object? (channel-receive ch))
;=> #t
See also: channel-close!, channel?
channel?¶
Syntax: (channel? obj)
Returns #t if obj is a channel, #f otherwise.
kaappi> (channel? (make-channel))
;=> #t
kaappi> (channel? (make-mutex))
;=> #f
kaappi> (channel? 'not-a-channel)
;=> #f
See also: make-channel
Parallel Pools¶
(kaappi parallel) — worker pools and parallel map/for-each over
(srfi 18) threads (fiber workers under --sandbox and WASM, where real
threads are unavailable). See the Concurrency
guide
for a walkthrough and the cross-thread-copy semantics shared with raw
thread-start!/thread-join!.
make-pool¶
Syntax: (make-pool n)
Creates a pool of n workers (a positive exact integer), each pulling
tasks from a shared queue until the pool is shut down. Workers are real
OS threads when available, or fiber workers on the calling thread's
scheduler under --sandbox and in the WebAssembly build.
kaappi> (import (kaappi parallel))
kaappi> (define pool (make-pool 4))
kaappi> (pool-shutdown! pool)
See also: pool-submit, pool-shutdown!,
processor-count
pool-submit¶
Syntax: (pool-submit pool thunk)
Submits thunk (a procedure of zero arguments) to pool and returns a
reply channel immediately — it does not wait for the task to run. Pass the
reply channel to task-wait to block for the result. Raises if pool has
already been shut down. Like thread-start!, thunk and its eventual
result cross to and from the worker by copy.
kaappi> (import (kaappi parallel))
kaappi> (define pool (make-pool 2))
kaappi> (define reply (pool-submit pool (lambda () (* 6 7))))
kaappi> (task-wait reply)
;=> 42
kaappi> (pool-shutdown! pool)
See also: task-wait, make-pool
task-wait¶
Syntax: (task-wait reply)
Blocks on the reply channel returned by pool-submit until the
corresponding task completes, then returns its result. If the task's
thunk raised an exception, task-wait re-raises it in the calling
context — the same contract as fiber-join/thread-join!.
kaappi> (import (kaappi parallel))
kaappi> (define pool (make-pool 2))
kaappi> (define reply (pool-submit pool (lambda () (error "boom"))))
kaappi> (guard (e (#t (display "task failed\n"))) (task-wait reply))
task failed
kaappi> (pool-shutdown! pool)
See also: pool-submit
pool-shutdown!¶
Syntax: (pool-shutdown! pool)
Closes pool's task queue and waits for every worker to finish. Tasks
already submitted (including one racing this call) all run to
completion; pool-submit after shutdown raises.
kaappi> (import (kaappi parallel))
kaappi> (define pool (make-pool 2))
kaappi> (pool-shutdown! pool)
kaappi> (pool-submit pool (lambda () 1))
;=> error: send on closed channel
See also: make-pool, pool-submit
parallel-map¶
Syntax: (parallel-map proc list)
Applies proc to each element of list, one task per element, using a
private pool sized (processor-count) that's torn down before returning.
Results are returned in the original list order regardless of completion
order. An exception from any element propagates out of parallel-map.
kaappi> (import (kaappi parallel))
kaappi> (parallel-map (lambda (n) (* n n)) '(1 2 3 4 5))
;=> (1 4 9 16 25)
See also: parallel-for-each, make-pool
parallel-for-each¶
Syntax: (parallel-for-each proc list)
Like parallel-map, but for side effects: applies proc to each element
of list using a private pool, waits for every call to finish, and
returns an unspecified value. Each call runs on a worker with its own
heap, and a channel captured by proc's closure does not cross the
worker boundary (it raises "channel belongs to another thread"). Use
parallel-for-each for effects that stand alone on each worker —
writing files, network requests, logging — and reach for
parallel-map or
pool-submit/task-wait when you need
values back.
kaappi> (import (kaappi parallel))
kaappi> (parallel-for-each
(lambda (n)
(call-with-output-file
(string-append "out-" (number->string n) ".txt")
(lambda (p) (write (* n n) p))))
'(1 2 3))
kaappi> (call-with-input-file "out-2.txt" read)
;=> 4
See also: parallel-map
Cheap per-element work: chunk manually instead
parallel-map/parallel-for-each submit one task per list element, so
every element pays a pool-submit/task-wait round trip plus the copy
across the worker boundary. That is the right shape when each call does
real work; for very cheap per-element operations the bookkeeping
dominates the computation. For those, use
make-pool/pool-submit/task-wait directly with one task per
processor, each covering a slice of the input with an ordinary
sequential loop — see the Parallel Prime
Search
example.
processor-count¶
Syntax: (processor-count)
Returns the number of logical processors available, or 1 under
--sandbox or in the WebAssembly build — matching the worker count a pool
degrades to in those environments.
See also: make-pool
Diagnostics¶
(kaappi diagnostics) — programmatic access to the stable KP codes
that Kaappi stamps on the errors it raises. Every code is documented in
the Diagnostic Reference; codes never change
meaning between releases, so they are safe to dispatch on where message
text is not.
error-object-code¶
Syntax: (error-object-code obj)
Returns the stable diagnostic code stamped on obj as an interned
symbol (for example KP3004 for a division-by-zero error), or #f if
there is none. Because the symbol is interned, codes compare with eq?.
Unlike error-object-message and error-object-irritants, this is a
total accessor that never raises: it returns #f both for values that
are not error objects and for error objects carrying no code — such as
those from error, since the KP namespace is reserved to the
implementation. That makes it safe as the first dispatch check inside a
guard, where raise may have delivered any value at all.
kaappi> (import (kaappi diagnostics))
kaappi> (guard (e (#t (error-object-code e))) (/ 1 0))
;=> KP3004
kaappi> (guard (e (#t (error-object-code e))) (error "boom"))
;=> #f
kaappi> (error-object-code 42)
;=> #f
kaappi> (define (safe-div a b)
(guard (e ((eq? (error-object-code e) 'KP3004) 'undefined))
(/ a b)))
kaappi> (safe-div 1 0)
;=> undefined
See also: error-object-message,
error-object-irritants,
Diagnostic Reference