Skip to content

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">

See also: ffi-fn, ffi-close


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.

See also: ffi-open, ffi-close


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.

kaappi> (define lib (ffi-open "libm.dylib"))  ; macOS — "libm.so.6" on Linux
kaappi> (ffi-close lib)

See also: ffi-open, ffi-fn


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.

kaappi> (processor-count)
;=> 8

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