LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Chicken Scheme: A Practical Guide for Systems Engineers

schemelispchicken-schemefunctional-programmingprogramming-languagessystems-programmingguiiupfficompilers

Scheme is a language that has been quietly doing serious work for decades. It is the language SICP was written in. Naughty Dog used a Racket-derived Scheme as the scripting engine for Uncharted and The Last of Us. GNU Guile is the official extension language of the GNU project. And CHICKEN Scheme — the subject of this guide — compiles your Scheme to portable C, producing standalone native binaries that run on Linux, macOS, Windows, ARM, MIPS, SPARC64, and anything else with a C toolchain.

CHICKEN is not an academic curiosity. It has a substantial egg (package) ecosystem, a clean FFI that makes C library bindings feel native, green-thread concurrency, full POSIX integration, and one of the most elegant garbage collection architectures in any compiled language implementation. The current stable release is CHICKEN 5.4.0, released July 2024.

This guide is aimed at engineers who already know at least one systems language and want a thorough map of the territory: Scheme fundamentals, CHICKEN internals, the egg ecosystem, FFI mechanics, GUI programming with IUP, and where CHICKEN sits among its Scheme peers.


Scheme Fundamentals

What Scheme Is, and What It Is Not

Scheme is a dialect of Lisp. It is deliberately small — the R7RS standard document runs to 88 pages; the Common Lisp HyperSpec is over 1,000. That minimalism is the point. Scheme gives you a powerful, consistent core and gets out of the way. There are no ten ways to do loops; the language gives you tail calls and higher-order functions and trusts you to build what you need.

The most recent standard is R7RS (Revised 7 Report on the Algorithmic Language Scheme), finalized in 2013. It defines “R7RS small” as the portable core and “R7RS large” as a collection of optional libraries via SRFIs (Scheme Requests for Implementation). CHICKEN 5 targets R7RS small, with R7RS large libraries available as eggs.

Scheme vs Common Lisp: The Key Differences

These are cousins that diverged decades ago and serve different philosophies.

Lisp-1 vs Lisp-2. The most fundamental difference. Common Lisp maintains separate namespaces for functions and variables — a name can simultaneously refer to a function and a variable. Scheme uses a single unified namespace. In Scheme, define defines everything. Functions are just values bound to names. This forces you to use funcall in CL but means Scheme code is considerably cleaner when passing functions as arguments.

1
2
3
4
5
6
7
;; In Scheme — map is just a value in scope like any other
(define (double x) (* x 2))
(map double '(1 2 3 4))  ; => (2 4 6 8)

;; Higher-order functions are natural — no funcall needed
(define (apply-twice f x) (f (f x)))
(apply-twice double 3)  ; => 12

Mandatory tail call optimization. Common Lisp does not require TCO — it is implementation-defined. Scheme mandates it. Every conforming Scheme implementation must eliminate tail calls, making recursion as stack-safe as iteration. This is not a convenience; it changes how you write programs.

Hygienic macros. Common Lisp macros are textual — they operate on raw s-expressions and require gensym tricks to avoid variable capture. Scheme provides hygienic macros via syntax-rules (pattern-based, declarative) and syntax-case (procedural, more powerful). Hygienic macros respect lexical scope by construction.

Philosophy. CL is a batteries-included, multi-paradigm industrial language with CLOS, the condition system, restarts, and decades of accumulated libraries. Scheme is minimal by design — fewer primitives, more power per primitive, extensible through macros and higher-order functions.

Tail Call Optimization in Depth

TCO is not just about recursion. It is about the entire semantics of function calls in tail position. A call is in tail position when its result is directly returned — no further computation wraps it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
;; This is NOT a tail call — the result of (fact (- n 1)) is multiplied by n
(define (fact n)
  (if (= n 0)
      1
      (* n (fact (- n 1)))))  ; not tail position

;; This IS a tail call — accumulator pattern
(define (fact n)
  (define (loop n acc)
    (if (= n 0)
        acc
        (loop (- n 1) (* n acc))))  ; tail position: result returned directly
  (loop n 1))

;; Mutual recursion is also fully TCO in Scheme
(define (even? n) (if (= n 0) #t  (odd?  (- n 1))))
(define (odd?  n) (if (= n 0) #f  (even? (- n 1))))
(even? 1000000)  ; => #t, no stack overflow

In CHICKEN specifically, TCO is implemented through the CPS transformation — tail calls compile to direct C function calls with no stack growth, equivalent to a goto.

Proper Tail Calls in Practice

Named let is idiomatic Scheme iteration — it creates a local recursive function and immediately calls it, and the recursive call is in tail position:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
;; Named let: the idiomatic Scheme loop
(define (sum-list lst)
  (let loop ((remaining lst) (acc 0))
    (if (null? remaining)
        acc
        (loop (cdr remaining) (+ acc (car remaining))))))

(sum-list '(1 2 3 4 5))  ; => 15

;; Building a result — still tail-recursive via accumulator
(define (reverse-list lst)
  (let loop ((remaining lst) (acc '()))
    (if (null? remaining)
        acc
        (loop (cdr remaining) (cons (car remaining) acc)))))

The do form provides imperative-style iteration when that reads more clearly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
;; do: (do ((var init step) ...) (test result) body ...)
(define (iota n)
  (do ((i 0 (+ i 1))
       (acc '() (cons i acc)))
      ((= i n) (reverse acc))))

(iota 5)  ; => (0 1 2 3 4)

;; Processing a vector
(define (vector-sum v)
  (do ((i 0 (+ i 1))
       (sum 0 (+ sum (vector-ref v i))))
      ((= i (vector-length v)) sum)))

Continuations and call/cc

A continuation is the “rest of the computation” — everything that will happen after the current expression returns. call-with-current-continuation (aliased as call/cc) captures this as a first-class procedure. Calling that procedure abandons whatever is currently happening and resumes from the captured point.

The most common use is escape continuations — early exit from nested loops or recursive searches:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
;; Early exit: product short-circuits on zero
(define (product lst)
  (call/cc
    (lambda (return)
      (let loop ((lst lst))
        (cond
          ((null? lst) 1)
          ((= (car lst) 0) (return 0))  ; escape immediately
          (else (* (car lst) (loop (cdr lst)))))))))

(product '(1 2 3 4 5))   ; => 120
(product '(1 2 0 4 5))   ; => 0 (short-circuits at zero)

;; Non-local exit from nested search
(define (find-in-tree pred tree)
  (call/cc
    (lambda (found)
      (let search ((node tree))
        (cond
          ((null? node) #f)
          ((not (pair? node))
           (when (pred node) (found node)))
          (else
           (search (car node))
           (search (cdr node)))))
      #f)))

Continuations can also be used to implement coroutines, generators, and cooperative threading — CHICKEN’s green thread system (srfi-18 egg) is built on continuations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
;; Simple generator using continuations
(define (make-range-generator start end)
  (define return #f)
  (define resume #f)
  (lambda ()
    (call/cc
      (lambda (k)
        (set! return k)
        (if resume
            (resume #f)
            (let loop ((i start))
              (when (< i end)
                (call/cc
                  (lambda (k)
                    (set! resume k)
                    (return i)))
                (loop (+ i 1))))
            (return 'done))))))

Hygienic Macros

syntax-rules defines pattern-matching macros that are hygienic by construction — introduced bindings cannot accidentally capture variables from the call site.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
;; while loop — not in R7RS, but trivial to add
(define-syntax while
  (syntax-rules ()
    ((_ condition body ...)
     (let loop ()
       (when condition
         body ...
         (loop))))))

;; swap! — exchanges two variable bindings
(define-syntax swap!
  (syntax-rules ()
    ((_ a b)
     (let ((tmp a))
       (set! a b)
       (set! b tmp)))))

;; Hygienic: 'tmp' here cannot clash with a 'tmp' at the call site
(let ((tmp 1) (other 2))
  (swap! tmp other)
  (list tmp other))  ; => (2 1) — correct, not confused by outer 'tmp'

;; my-and with short-circuit evaluation
(define-syntax my-and
  (syntax-rules ()
    ((_) #t)
    ((_ e) e)
    ((_ e1 e2 ...)
     (if e1 (my-and e2 ...) #f))))

;; my-or
(define-syntax my-or
  (syntax-rules ()
    ((_) #f)
    ((_ e) e)
    ((_ e1 e2 ...)
     (let ((t e1))
       (if t t (my-or e2 ...))))))

syntax-case (available in CHICKEN via (chicken syntax)) gives you the full power of procedural macro writing, including the ability to inspect sub-expressions, generate identifiers with datum->syntax, and write macros that compose correctly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
;; syntax-case: more power when patterns are not enough
(define-syntax my-define-record
  (lambda (stx)
    (syntax-case stx ()
      ((_ name (field ...))
       (with-syntax
         ((constructor (datum->syntax #'name
                         (string->symbol
                           (string-append "make-"
                             (symbol->string (syntax->datum #'name))))))
          (predicate (datum->syntax #'name
                       (string->symbol
                         (string-append (symbol->string (syntax->datum #'name))
                                        "?")))))
         #'(define-record-type name
             (constructor field ...)
             predicate
             (field field #f) ...))))))

CHICKEN Scheme Internals

The Cheney-on-the-MTA Architecture

CHICKEN’s garbage collector is described in Henry Baker’s 1994 paper “CONS Should Not CONS Its Arguments, Part II: Cheney on the M.T.A.” — the M.T.A. refers both to the New York subway system and to “making the allocation fast.” Understanding it explains most of CHICKEN’s behavior and performance characteristics.

The key insight: after CPS transformation, no Scheme function ever returns to its caller in the conventional sense. Functions call continuations, which call continuations, which call continuations. Since no function returns, the C stack never unwinds. Baker’s observation: if the stack never unwinds, we can use the C stack as the allocation arena for newly created objects.

Step 1: CPS transformation. The compiler rewrites every Scheme function into continuation-passing style. Every function takes an extra argument k (the continuation). Instead of return value, it calls k(value). The stack grows continuously as CPS calls chain together.

Step 2: Stack as nursery. New objects — pairs, strings, closures, vectors — are allocated directly on the C stack using alloca(). Allocation is a single stack pointer bump. This is extremely fast and has excellent cache locality.

Step 3: Minor GC via longjmp. Each allocation checks whether the stack has grown past a threshold (typically around 256KB). When it has, a minor GC fires:

  1. The GC identifies live objects by starting from the current function’s arguments and continuation (the “tip of the iceberg”).
  2. It copies all live objects from the stack into the heap using Cheney’s two-pointer breadth-first copying algorithm.
  3. It calls longjmp() back to the top-level trampoline, resetting the stack pointer. The stack is now empty again; all live data is in the heap.
  4. Execution resumes from where it left off — the continuation knows where to go.

Step 4: Major GC. The heap is divided into two equal regions: fromspace and tospace. When the heap’s fromspace fills up during minor GC:

  1. All live fromspace objects are copied to tospace (again using Cheney’s algorithm).
  2. Fromspace and tospace swap roles.
  3. If insufficient garbage was collected, the heap grows and the process repeats.

The result is a two-generation collector where generation 0 lives on the C stack and generation 1 is the heap. Short-lived objects (most objects in a functional program) never reach the heap at all — they are allocated on the stack and discarded when the stack is reclaimed via longjmp.

Write barriers. Because mutation can create references from old generation objects to young ones, CHICKEN maintains a write barrier. When a heap object is mutated to point to a stack object, that reference is added to a remembered set and treated as a GC root.

This architecture has concrete implications:

  • Allocation is very fast (stack pointer bump)
  • Minor GC is triggered predictably (stack threshold)
  • The pause for a minor GC is proportional to live data, not heap size
  • Long-lived mutation-heavy programs with large heaps incur higher major GC cost

Compilation Pipeline

CHICKEN’s compilation pipeline has five distinct phases:

  1. Canonicalization: Macro expansion, alpha-conversion (unique naming to track scoping), and normalization of toplevel defines to assignments.

  2. CPS conversion: The entire program is transformed into continuation-passing style. Every expression gets an explicit continuation parameter. This is what makes tail calls free — in CPS, a tail call is just a function call.

  3. Optimization passes (five rounds):

    • Eliminate double negations and other algebraic redundancies
    • Contract procedures called only once (inline them at the call site)
    • Eliminate unnecessary local variables
    • Lower builtins to primitive operations
    • Final contraction pass
  4. Closure conversion: Free variables in lambda expressions are captured into explicit closure records. The stack-allocated closure is what gets promoted to the heap during minor GC.

  5. C code generation: The CPS closures become C functions. The compiler emits stack overflow checks, allocation code using C_alloc, continuation invocations, and trampoline infrastructure.

The generated C is not meant to be read by humans, but the structure is regular and predictable. Tight inner loops often compile down to a handful of C statements. CHICKEN also emits type annotations that GCC or clang can use for further optimization.


csi vs csc: The Two CHICKEN Entry Points

csi — The Interpreter

csi is CHICKEN’s read-eval-print loop. It interprets Scheme directly without going through C, which makes it slower than compiled code but useful for exploration, testing, and scripting.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Start the REPL
csi

# Run a script file
csi -ss myscript.scm arg1 arg2

# Evaluate an expression and exit
csi -e '(display (+ 1 2)) (newline)'

# Load a file and drop into REPL
csi -l mylib.scm

# Quiet mode (suppress banner)
csi -q

Inside the REPL, comma-prefixed commands provide introspection:

#;1> ,?
; available commands:
; ,d EXPR       -- describe a value
; ,p EXPR       -- pretty-print
; ,x EXPR       -- expand macros and pretty-print
; ,m MODULE     -- switch to module context
; ,l FILE       -- load a file
; ,t EXPR       -- time an expression
; ,c            -- show call trace from last error
; ,q            -- quit

The REPL supports result history with #0 (last result), #1 (second-to-last), etc. Adding the breadline or linenoise egg provides tab completion and persistent history.

csc — The Compiler

csc is the compiler driver. It invokes the Scheme-to-C translator, then hands the generated C to your system’s C compiler (GCC or clang), then links the result.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Compile to a native binary
csc -o myprogram myprogram.scm

# Compile a shared library (for use as a Chicken extension)
csc -shared -o mylib.so mylib.scm

# Emit the intermediate C without compiling
csc -to-stdout myprogram.scm > myprogram.c

# Compile with optimization
csc -O3 -o myprogram myprogram.scm

# Full unsafe, maximum speed (remove all runtime checks)
csc -O5 -unsafe -o myprogram myprogram.scm

# Static binary (bundle all egg dependencies)
csc -static -o myprogram myprogram.scm

# Link against a C library
csc -o myprogram myprogram.scm -L /usr/lib -l sqlite3

Optimization levels:

Level Effect
-O0 No optimization
-O1 Optimize small non-allocating leaf procedures
-O2 General inlining of small known procedures (default)
-O3 Inline top-level procedures, type specialization
-O4 O3 + unsafe mode (removes runtime checks)
-O5 O4 + disable interrupts, disable trace, clustering

Optimization declarations can be placed in source files for fine-grained control:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
;; Inline this specific procedure everywhere it's called
(declare (inline fast-math-op))

;; Treat all standard bindings as immutable — enables more aggressive optimization
(declare (standard-bindings))

;; Assume globals are never redefined — allows inlining across modules
(declare (block))

;; Tell the type system the signature of a procedure
(: process-items (list -> fixnum))

;; Use fixnum arithmetic in this module (no bignum overhead)
(declare (fixnum-arithmetic))

Profiling workflow:

1
2
3
4
5
6
7
8
# Compile with profiling
csc -profile -o myprogram myprogram.scm

# Run the program — generates PROFILE.pid
./myprogram

# View profile data
chicken-profile PROFILE.*

The Module System

CHICKEN’s module system is syntactical rather than environmental — it maps identifier names to bindings without creating separate runtime environments. This is a meaningful design choice: modules are resolved at compile time, not runtime.

Basic Module Syntax

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
;; Define a module that exports two procedures
(module string-utils
  (string-trim-all string-join-with)

  (import scheme (chicken base) (chicken string))

  (define (string-trim-all s)
    (string-trim-right (string-trim s)))

  (define (string-join-with sep lst)
    (if (null? lst)
        ""
        (let loop ((rest (cdr lst))
                   (acc  (car lst)))
          (if (null? rest)
              acc
              (loop (cdr rest)
                    (string-append acc sep (car rest))))))))

;; Use it
(import string-utils)
(string-trim-all "  hello world  ")  ; => "hello world"
(string-join-with ", " '("a" "b" "c"))  ; => "a, b, c"

Import Specifiers

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
;; Import only specific identifiers
(import (only string-utils string-trim-all))

;; Import everything except certain identifiers
(import (except string-utils string-join-with))

;; Import with a namespace prefix
(import (prefix string-utils su:))
(su:string-trim-all "  hi  ")

;; Rename on import
(import (rename string-utils (string-trim-all trim-all)))

Modules Across Files

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
;; mylib.scm
(module mylib (exported-proc)
  (import scheme (chicken base))

  (define (helper x) (* x 2))
  (define (exported-proc x) (helper (+ x 1))))

;; Compile and generate the import library
;; csc -emit-import-library mylib -c mylib.scm

;; main.scm
(import mylib)
(display (exported-proc 5))  ; => 12

R7RS define-library

For portability with other R7RS implementations, CHICKEN supports define-library via the r7rs egg:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
;; r7rs-style library definition
(define-library (myproject utils)
  (export string-trim-all)
  (import (scheme base) (scheme write))
  (begin
    (define (string-trim-all s)
      ;; implementation
      s)))

;; R7RS import
(import (myproject utils))

The Egg System

Eggs are CHICKEN’s extension packages. The repository at eggs.call-cc.org hosts over 400 eggs, installable with chicken-install.

chicken-install Basics

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Install an egg
chicken-install json

# Install multiple eggs
chicken-install srfi-1 srfi-69 sqlite3

# Install to a local directory (no root needed)
chicken-install -prefix ~/.chicken json

# Install with custom C flags (for eggs with C dependencies)
CSC_OPTIONS='-I/opt/homebrew/include -L/opt/homebrew/lib' chicken-install sqlite3

# List installed eggs
chicken-status

# Uninstall
chicken-uninstall json

# Install from a local .egg file
chicken-install ./my-egg/

Essential Eggs by Category

Data and serialization:

  • json — JSON parsing and generation
  • cjson — C-based JSON for performance-sensitive code
  • msgpack — MessagePack binary serialization
  • csv — CSV parsing

HTTP and networking:

  • http-client — High-level HTTP client with SSL support
  • intarweb — Low-level HTTP library (request/response objects)
  • spiffy — Embedded HTTP server
  • uri-common — URI parsing and construction

Databases:

  • sqlite3 — Full SQLite 3 bindings
  • postgresql — libpq bindings for PostgreSQL
  • redis — Redis client
  • lmdb — Lightning Memory-Mapped Database bindings

Parsing:

  • comparse — Combinator parsers
  • packrat — Packrat (PEG) parsing with memoization
  • lalr — LALR(1) parser generator

Testing:

  • test — Lightweight test framework (assert-based)
  • srfi-64 — Standard R7RS test suite API
  • cluckcheck — QuickCheck-style property-based testing

Concurrency:

  • srfi-18 — Green threads (continuations-based)
  • mailbox — Thread-safe message queues

Cryptography:

  • tweetnacl — NaCl/libsodium-compatible modern crypto
  • openssl — SSL/TLS via OpenSSL
  • sha2, md5 — Hash functions
  • hmac — Message authentication

Systems:

  • posix-utils — Extended POSIX wrappers
  • scsh-process — Shell-style process management
  • args — Command-line argument parsing

Data structures:

  • srfi-1 — Comprehensive list operations
  • srfi-69 — Hash tables
  • rb-tree — Red-black trees
  • kd-tree — k-d trees for spatial indexing

Tooling:

  • lsp-server — Language Server Protocol (editor integration)
  • chicken-doc — Documentation browsing in the REPL

Creating an Egg

An egg is a directory containing at least your source file and an .egg descriptor:

my-egg/
├── my-egg.scm
└── my-egg.egg
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
;; my-egg.egg
((author "Your Name")
 (synopsis "Does something useful")
 (license "BSD")
 (version "1.0.0")
 (dependencies srfi-1)
 (components
  (extension my-egg
   (source "my-egg.scm")
   (component-dependencies srfi-1))))
1
2
3
4
5
6
;; my-egg.scm
(module my-egg (frobnicate)
  (import scheme (chicken base) srfi-1)

  (define (frobnicate items)
    (filter odd? (map (lambda (x) (* x 3)) items))))
1
2
cd my-egg
chicken-install .

SRFI Support

CHICKEN ships with a large set of SRFIs built in, and many more are available as eggs.

Built-in SRFIs (available without installing anything): SRFI-0 (feature testing), SRFI-4 (homogeneous numeric vectors), SRFI-8 (receive), SRFI-9 (define-record-type), SRFI-16 (case-lambda), SRFI-17 (generalized set!), SRFI-23 (error), SRFI-26 (cut/cute), SRFI-28 (basic string format), SRFI-39 (parameter objects), SRFI-46 (syntax-rules extensions), SRFI-62 (S-expression comments), SRFI-98 (environment variables).

Key eggs to install for day-to-day work:

  • srfi-1 — List operations: fold, filter, iota, append-map, take, drop, every, any
  • srfi-13 — String library: string-contains, string-prefix?, string-split
  • srfi-14 — Character sets
  • srfi-18 — Multithreading (green threads)
  • srfi-19 — Date and time
  • srfi-69 — Hash tables
  • srfi-133 — Vector library
  • srfi-158 — Generators and accumulators

The Foreign Function Interface

CHICKEN’s FFI is one of its strengths. You can call any C function, access C structs, and call Scheme from C, all without writing a separate binding layer in pure C.

Basic foreign-lambda

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(import (chicken foreign))

;; Bind a C function by name
;; (foreign-lambda return-type "c-name" arg-type ...)
(define strlen
  (foreign-lambda int "strlen" c-string))

(strlen "hello")  ; => 5

;; Link against libm and call sqrt
(define c-sqrt
  (foreign-lambda double "sqrt" double))

(c-sqrt 2.0)  ; => 1.4142135623730951

foreign-lambda*: Inline C Code

When you want to write a small C snippet directly in your Scheme file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
;; Inline C body — use C_return() to return a value
(define my-clamp
  (foreign-lambda* double ((double val) (double lo) (double hi))
    "C_return(val < lo ? lo : (val > hi ? hi : val));"))

(my-clamp 5.0 0.0 3.0)  ; => 3.0

;; Integer bit-length — faster than computing it in Scheme
(define integer-length
  (foreign-lambda* unsigned-int ((unsigned-int x))
    "unsigned int n = 0;
     while (x) { n++; x >>= 1; }
     C_return(n);"))

Custom Foreign Types

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
;; Define a foreign type alias with conversion procedures
;; (define-foreign-type name base-type [to-c from-c])
(define-foreign-type milliseconds
  unsigned-long
  (lambda (ms) (inexact->exact (floor ms)))  ; Scheme -> C
  (lambda (ms) (exact->inexact ms)))          ; C -> Scheme

;; Now you can use 'milliseconds' in any foreign-lambda
(define sleep-ms
  (foreign-lambda* void ((milliseconds ms))
    "#include <time.h>
     struct timespec ts;
     ts.tv_sec  = ms / 1000;
     ts.tv_nsec = (ms % 1000) * 1000000;
     nanosleep(&ts, NULL);"))

Accessing C Structs

Use the foreigners egg for idiomatic struct bindings:

1
chicken-install foreigners
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
(import (chicken foreign) foreigners)

;; Assume this C struct:
;; typedef struct { double x; double y; double z; } vec3_t;

(define-foreign-record-type (vec3 "vec3_t")
  (double x vec3-x set-vec3-x!)
  (double y vec3-y set-vec3-y!)
  (double z vec3-z set-vec3-z!))

;; Allocate and use
(define v
  (let-location ((vec vec3))
    (set-vec3-x! vec 1.0)
    (set-vec3-y! vec 2.0)
    (set-vec3-z! vec 3.0)
    vec))

;; Access a C global variable
(define-foreign-variable errno int "errno")

Callbacks: Calling Scheme from C

When C code needs to call back into Scheme, use foreign-safe-lambda and define-external:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
(import (chicken foreign))

;; Define a Scheme procedure callable from C
(define-external (my-callback (c-string s)) int
  (string-length s))

;; Passing Scheme closures into C code
(define iterate-with-callback
  (foreign-safe-lambda* void ((scheme-object receiver))
    "const char *items[] = {\"alpha\", \"beta\", \"gamma\", NULL};
     C_word str, *a;
     for (int i = 0; items[i]; i++) {
       int len = strlen(items[i]);
       a   = C_alloc(C_SIZEOF_STRING(len));
       str = C_string(&a, len, items[i]);
       C_save(str);
       C_callback(receiver, 1);
     }"))

;; Call it — the lambda is invoked once per item
(iterate-with-callback (lambda (item) (display item) (newline)))

A Complete FFI Example: Wrapping libcurl

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
(import (chicken foreign) (chicken memory))

;; CURL handle type
(define-foreign-type curl-handle (c-pointer "void"))

;; CURLOPT enum values (from curl.h)
(define CURLOPT_URL        10002)
(define CURLOPT_FOLLOWLOC  52)
(define CURLOPT_WRITEDATA  10001)

(define curl-easy-init
  (foreign-lambda curl-handle "curl_easy_init"))

(define curl-easy-cleanup
  (foreign-lambda void "curl_easy_cleanup" curl-handle))

(define curl-easy-setopt/string
  (foreign-lambda int "curl_easy_setopt" curl-handle int c-string))

(define curl-easy-setopt/long
  (foreign-lambda int "curl_easy_setopt" curl-handle int long))

(define curl-easy-perform
  (foreign-lambda int "curl_easy_perform" curl-handle))

(define (http-get url)
  (let ((curl (curl-easy-init)))
    (unless curl (error "curl_easy_init failed"))
    (curl-easy-setopt/string curl CURLOPT_URL url)
    (curl-easy-setopt/long  curl CURLOPT_FOLLOWLOC 1)
    (let ((result (curl-easy-perform curl)))
      (curl-easy-cleanup curl)
      result)))

Practical Scheme Patterns

let, let*, letrec, letrec*

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
;; let — all bindings evaluated in parallel (order undefined)
(let ((x 1) (y 2))
  (+ x y))  ; => 3

;; let* — sequential, each binding can reference previous ones
(let* ((x 1)
       (y (+ x 1))    ; can reference x
       (z (+ y 1)))   ; can reference y
  (list x y z))  ; => (1 2 3)

;; letrec — mutual recursion among bindings
(letrec ((even? (lambda (n) (if (= n 0) #t (odd?  (- n 1)))))
         (odd?  (lambda (n) (if (= n 0) #f (even? (- n 1))))))
  (even? 100))  ; => #t

;; Internal define — equivalent to letrec*, preferred in practice
(define (process data)
  (define (validate x) (and (number? x) (positive? x)))
  (define (transform x) (* x x))
  (filter validate (map transform data)))

define-record-type (SRFI-9 / R7RS)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
;; (define-record-type <name>
;;   (constructor field ...)
;;   predicate?
;;   (field accessor [modifier]) ...)

(define-record-type <point>
  (make-point x y)
  point?
  (x point-x)
  (y point-y))

(define p (make-point 3 4))
(point? p)     ; => #t
(point-x p)    ; => 3

;; With mutable fields
(define-record-type <counter>
  (make-counter value)
  counter?
  (value counter-value set-counter-value!))

(define c (make-counter 0))
(set-counter-value! c (+ (counter-value c) 1))
(counter-value c)  ; => 1

;; More complex record representing a connection pool slot
(define-record-type <conn-slot>
  (make-conn-slot host port timeout state)
  conn-slot?
  (host    conn-slot-host)
  (port    conn-slot-port)
  (timeout conn-slot-timeout conn-slot-set-timeout!)
  (state   conn-slot-state   conn-slot-set-state!))

Higher-Order Functions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
;; The workhorse trio
(map    (lambda (x) (* x x)) '(1 2 3 4 5))  ; => (1 4 9 16 25)
(filter odd? '(1 2 3 4 5))                   ; => (1 3 5)
(fold   + 0  '(1 2 3 4 5))                   ; => 15  (srfi-1)

;; Composing transformations without intermediate lists
(define (process-numbers lst)
  (fold + 0
        (map (lambda (x) (* x x))
             (filter odd? lst))))

(process-numbers '(1 2 3 4 5 6 7))  ; => 84  (1+9+25+49)

;; Currying with cut (srfi-26, built-in)
(map (cut * <> 2) '(1 2 3))      ; => (2 4 6)
(filter (cut > <> 3) '(1 2 3 4 5))  ; => (4 5)

;; fold-right preserves list order
(fold-right cons '() '(1 2 3))   ; => (1 2 3)  (identity on lists)

;; append-map (flat-map)
(append-map (lambda (x) (list x (* x 2))) '(1 2 3))
; => (1 2 2 4 3 6)

;; Closures as objects
(define (make-adder n)
  (lambda (x) (+ x n)))

(define add5 (make-adder 5))
(add5 10)  ; => 15
(map add5 '(1 2 3))  ; => (6 7 8)

;; Memoization
(define (memoize f)
  (let ((cache (make-hash-table)))
    (lambda args
      (or (hash-table-ref/default cache args #f)
          (let ((result (apply f args)))
            (hash-table-set! cache args result)
            result)))))

(define fib
  (memoize
    (lambda (n)
      (if (< n 2) n
          (+ (fib (- n 1)) (fib (- n 2)))))))

(fib 40)  ; => 102334155, fast

Error Handling

CHICKEN uses condition-case (built-in) and the with-exception-handler / guard forms from R7RS:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
;; condition-case — CHICKEN-specific, powerful
(condition-case (risky-operation)
  ((exn i/o)  (print "I/O error: " (get-condition-property exn 'exn 'message)))
  ((exn)      (print "Unknown error: " (get-condition-property exn 'exn 'message))))

;; guard — R7RS standard
(guard (exn
        ((string? (condition/report-string exn))
         (display "caught: ")
         (display (condition/report-string exn))
         (newline)))
  (error "something went wrong" 42))

;; Dynamic-wind: cleanup that always runs
(define (with-open-file filename thunk)
  (let ((port (open-input-file filename)))
    (dynamic-wind
      (lambda () #f)               ; before
      (lambda () (thunk port))     ; during
      (lambda () (close-port port)))))  ; after — even if thunk raises

IUP: Cross-Platform GUI with Native Controls

What IUP Is

IUP is a cross-platform GUI toolkit developed at PUC-Rio (Pontifícia Universidade Católica do Rio de Janeiro), the same institution behind the Lua programming language. IUP’s core design principle: use the platform’s native controls rather than drawing its own. On Windows, IUP uses Win32 controls. On Linux, it uses GTK+. On older Unix systems, Motif.

This means IUP applications look and behave like native applications. A button on Windows uses a Windows button; on Linux it is a GTK button. There is no custom widget renderer. The tradeoff is that the API surface is somewhat constrained to what the platforms share.

IUP’s C API is small and consistent. Attributes (visual properties and state) are strings set and read by name. Callbacks (event handlers) are function pointers registered by attribute name. This uniformity makes it easy to bind to other languages, which is exactly how the CHICKEN iup egg works.

Platform support: Windows (Win32), Linux (GTK+), macOS (via GTK+ on X11 — native Cocoa support is partial), and older Unix (Motif).

Installing the iup Egg

The iup egg requires libiup to be installed as a system library before the egg can be installed.

1
2
3
4
5
6
7
8
# Ubuntu/Debian
sudo apt-get install libiup libiup-dev

# macOS with Homebrew
brew install iup

# Then install the egg
chicken-install iup

The egg provides several modules:

  • iup-base — Core: object creation, attributes, callbacks, layout, dialogs, event loop
  • iup-controls — Standard widgets (buttons, text boxes, lists, etc.)
  • iup-dialogs — System dialogs (file picker, color picker, font picker)
  • iup-glcanvas — OpenGL canvas
  • iup-pplot — 2D plotting

IUP Core Concepts

Everything is a handle. Every widget — button, label, dialog, container — is an IUP handle. You create handles with constructor procedures, set attributes on them, register callbacks, assemble them into a hierarchy, and show a dialog.

Attributes are strings. IUP uses a string-based attribute system. Attribute names are usually Scheme keywords in the CHICKEN binding. Values are auto-converted from Scheme types where it makes sense.

1
2
3
4
5
6
7
8
;; Read an attribute
(attribute widget title:)          ; => "Submit"

;; Set an attribute
(attribute-set! widget title: "Cancel")

;; Generalized set! syntax also works
(set! (attribute widget expand:) 'Yes)

Callbacks return control symbols. Most callbacks return one of:

  • 'default — normal continuation, keep the dialog open
  • 'close — close the containing dialog
  • 'ignore — suppress default action (used for key events, etc.)

Hello World

1
2
3
4
5
6
(import iup)

;; The minimal IUP program
(show (dialog (label "Hello, World!")))
(main-loop)
(exit 0)

A Complete Example: Form with Validation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
(import iup)

(define name-field
  (textbox
    expand: 'Horizontal
    tip: "Enter your name"))

(define result-label
  (label ""
    expand: 'Yes))

(define (on-submit self)
  (let ((name (attribute name-field value:)))
    (if (string=? name "")
        (attribute-set! result-label title: "Name cannot be empty")
        (attribute-set! result-label title:
                        (string-append "Hello, " name "!"))))
  'default)

(define submit-btn
  (button
    title: "Submit"
    action: on-submit))

(define (on-cancel self)
  'close)

(define cancel-btn
  (button
    title: "Cancel"
    action: on-cancel))

(define button-row
  (hbox
    alignment: 'ACENTER
    gap: 10
    (fill)       ; pushes buttons to the right
    submit-btn
    cancel-btn))

(define form
  (vbox
    gap: 10
    margin: '15x15
    (label "Enter your name:")
    name-field
    result-label
    button-row))

(define dlg
  (dialog
    title: "Greeting"
    form))

(show dlg)
(main-loop)
(destroy! dlg)
(exit 0)

Layouts: vbox, hbox, fill

IUP uses a box model — no absolute coordinates. Containers arrange their children.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
;; vbox: vertical stack
(vbox
  gap: 5              ; pixels between children
  margin: '10x10      ; internal padding (width x height)
  alignment: 'ALEFT   ; ALEFT, ACENTER, ARIGHT
  (label "First")
  (label "Second")
  (label "Third"))

;; hbox: horizontal row
(hbox
  gap: 5
  alignment: 'ACENTER  ; ATOP, ACENTER, ABOTTOM
  (button title: "A")
  (button title: "B")
  (fill)               ; elastic spacer — absorbs remaining space
  (button title: "Z"))

;; Nested layouts for complex UIs
(vbox
  (hbox
    (label "Name:")
    (textbox expand: 'Horizontal))
  (hbox
    (label "Email:")
    (textbox expand: 'Horizontal))
  (hbox
    (fill)
    (button title: "OK")
    (button title: "Cancel")))

Timers and Idle Callbacks

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
;; Timer: fires every N milliseconds
(define timer
  (timer
    time: 1000          ; 1 second
    action-cb: (lambda (self)
                 (display "tick\n")
                 'default)))

;; Start the timer
(attribute-set! timer run: 'Yes)

;; Stop it
(attribute-set! timer run: 'No)

;; Idle: called when no events are pending
(set! (callback iup-global idle-action:)
      (lambda ()
        ;; do background work here
        'default))

Canvas for Custom Drawing

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
(import iup iup-controls)

(define canvas
  (canvas
    expand: 'Yes
    rastersize: '400x300
    action: (lambda (self x y)
              ;; x, y is the canvas area
              ;; Use CD (Canvas Draw) library for actual drawing
              'default)))

;; With the cd egg installed, you can draw:
;; (cd:canvas-activate canvas)
;; (cd:foreground (cd:encode-color 255 0 0))
;; (cd:line 0 0 400 300)

File Dialog and Message Boxes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(import iup iup-dialogs)

;; File open dialog
(define (pick-file)
  (let ((dlg (file-dialog
               dialogtype: 'Open
               title: "Open File"
               filter: "*.scm"
               filterinfo: "Scheme files")))
    (popup dlg 0 0)
    (let ((status (attribute dlg status:))
          (value  (attribute dlg value:)))
      (destroy! dlg)
      (if (equal? status "0")
          value
          #f))))

;; Message box
(message "Error" "File not found!")

;; Yes/No question
(alarm "Confirm" "Delete this file?" "Yes" "No" #f)
;; => 1 for Yes, 2 for No

IUP vs Other Scheme GUI Options

Option Approach Maintenance Platform
iup egg Native controls via IUP Active, Thomas Chust Win/Linux/Mac(GTK)
chicken-tk Tcl/Tk via FFI Maintained Win/Linux/Mac
GLFW3 egg OpenGL window management Available Win/Linux/Mac
GTK bindings Direct GTK+ Fragmented Linux-primary

IUP’s advantage for CHICKEN is the tight integration, the clean Scheme API (keywords as attributes, symbols as callback return values), and the native control appearance. If you need a complex custom-drawn UI, GLFW3 with OpenGL or a web-based approach (spiffy + browser as UI) is more appropriate. For standard desktop utility GUIs — forms, dialogs, settings panels — IUP is the most practical choice.

The iup egg is actively maintained by Thomas Chust at a BSD license. The upstream IUP library from PUC-Rio continues to receive updates, and new backends (including improved macOS Cocoa support via “IUP Next”) are in development.


CHICKEN in the Scheme Ecosystem

The Major Implementations

Chez Scheme (now open source, maintained by Cisco/community) is the performance leader. It has a sophisticated native code compiler, excellent garbage collection, and fast startup. Chez was open-sourced in 2016 and became the backend for Racket (Racket-on-Chez). If raw numeric or functional performance is your primary concern, Chez is likely the fastest option.

Racket (formerly PLT Scheme) is the most complete Scheme platform. It has a macro system powerful enough to implement entire new languages, a large standard library, typed/untyped gradual typing, documentation tools, a package manager, and an IDE (DrRacket). It diverged from standard Scheme enough to be its own language. Racket has been used in game scripting (Naughty Dog) and is the primary academic Scheme platform. If you want a full language workbench, Racket is the answer.

GNU Guile is the official extension language of the GNU Project, embedded in GDB, Lilypond, Gnucash, and GNU Make. Guile’s C API is stable and well-documented, making it the best choice when you need to embed a Scheme interpreter in a C or C++ application. Guile 3.0 added a JIT compiler and improved performance substantially.

MIT Scheme is the oldest actively maintained Scheme, descended from the original MIT Lisp Machine work. It is the reference implementation for SICP, has an excellent debugger, and is well-suited for learning Scheme’s semantics. Performance on production workloads is variable.

Gambit Scheme compiles to C similarly to CHICKEN but uses a different GC strategy. It has excellent performance on numerical code and a clean FFI.

Where CHICKEN Fits

CHICKEN occupies a specific niche that none of the others fill as well:

Systems and CLI programming. csc produces genuine native binaries with no runtime dependency. You can ship a single executable, cross-compile for different architectures, or distribute C source that compiles anywhere. The POSIX API is comprehensive and idiomatic.

Embedding and extension. CHICKEN’s C FFI is among the best in the Scheme world for going in both directions — calling C from Scheme and calling Scheme from C. The foreign-lambda* inline C syntax makes tight C/Scheme integration practical without a separate binding generation step.

Scripts that compile. A common CHICKEN workflow is to develop in csi for fast iteration, then csc for deployment. The same source file works in both contexts.

Modest footprint. CHICKEN itself requires only a C toolchain and GNU Make. The compiled binaries are small. This matters in embedded contexts and constrained environments.

Performance benchmark context. CHICKEN performs well on certain benchmarks but trails Chez and Racket on compute-heavy numeric work. The Cheney-on-the-MTA GC is excellent for allocation-heavy functional code but major GC pauses can be visible in latency-sensitive applications with large heaps. For most systems and infrastructure use cases, CHICKEN’s performance is more than adequate.

Benchmark summary (r7rs-benchmarks on i3-N305, 2024):
Chez Scheme   — consistently fast across all benchmarks
Racket        — fast on control-flow heavy code
Guile 3.0     — strong on string and I/O benchmarks
MIT Scheme    — variable, excellent on some GC benchmarks
CHICKEN 5.4   — competitive on allocation-heavy functional code,
                trails on heavy numeric computation

A Practical CHICKEN Project: HTTP Health Checker

Pulling it together — a small but complete program that demonstrates modules, records, error handling, HTTP, and compilation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
;; health-check.scm
;; Compile with: csc -O2 -o health-check health-check.scm

(import scheme
        (chicken base)
        (chicken process-context)
        (chicken time)
        (chicken format)
        srfi-1
        http-client
        uri-common)

(define-record-type <result>
  (make-result url status-code latency-ms error)
  result?
  (url         result-url)
  (status-code result-status-code)
  (latency-ms  result-latency-ms)
  (error       result-error))

(define (check-url url)
  (let ((start (current-milliseconds)))
    (condition-case
        (let-values (((body status-code headers)
                      (http-client:with-input-from-request
                        url #f read-string)))
          (make-result url status-code
                       (- (current-milliseconds) start)
                       #f))
      ((exn)
       (make-result url #f
                    (- (current-milliseconds) start)
                    (get-condition-property exn 'exn 'message))))))

(define (report result)
  (if (result-error result)
      (format #t "FAIL  ~a  ~a (~dms)~%"
              (result-url result)
              (result-error result)
              (result-latency-ms result))
      (format #t "~a  ~a  ~d (~dms)~%"
              (if (= (result-status-code result) 200) "OK  " "WARN")
              (result-url result)
              (result-status-code result)
              (result-latency-ms result))))

(define (main args)
  (when (null? args)
    (display "Usage: health-check <url> [url ...]\n")
    (exit 1))
  (let* ((results (map check-url args))
         (failed  (filter result-error results))
         (non-200 (filter (lambda (r)
                            (and (not (result-error r))
                                 (not (= (result-status-code r) 200))))
                          results)))
    (for-each report results)
    (newline)
    (format #t "~d checked, ~d failed, ~d non-200~%"
            (length results)
            (length failed)
            (length non-200))
    (exit (if (or (pair? failed) (pair? non-200)) 1 0))))

(main (command-line-arguments))
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Development iteration in csi
csi -ss health-check.scm https://api.example.com https://backup.example.com

# Compile for production
csc -O2 -o health-check health-check.scm

# Cross-compile for Linux ARM (with appropriate cross toolchain)
CSC_OPTIONS='-target arm-linux-gnueabihf' csc -O2 -o health-check-arm health-check.scm

# Produce C source for distribution (recipient needs only a C compiler)
csc -to-stdout health-check.scm > health-check.c
gcc -o health-check health-check.c -lchicken -lm

Getting Started

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Install CHICKEN 5.4.0 from source
wget https://code.call-cc.org/releases/5.4.0/chicken-5.4.0.tar.gz
tar xf chicken-5.4.0.tar.gz
cd chicken-5.4.0
make PLATFORM=linux PREFIX=/usr/local
sudo make install PREFIX=/usr/local

# Or via package manager
# Ubuntu/Debian: sudo apt-get install chicken-bin
# macOS:         brew install chicken
# Arch:          sudo pacman -S chicken

# Verify
csi --version   # CHICKEN 5.4.0
csc --version

# Install frequently-used eggs
chicken-install srfi-1 srfi-18 srfi-69 json http-client sqlite3 test

# Start the REPL
csi
#;1> (import srfi-1)
#;2> (iota 10)
; => (0 1 2 3 4 5 6 7 8 9)
#;3> ,q

Useful references:

  • Manual: https://wiki.call-cc.org/man/5/index
  • API search: https://api.call-cc.org/5/doc/
  • Egg index: https://eggs.call-cc.org/5/
  • Community: #chicken on Libera.Chat IRC

CHICKEN rewards the investment in learning it. The Cheney-on-the-MTA architecture is genuinely novel and worth understanding regardless of what you end up using it for. The FFI is one of the cleanest in any Lisp implementation. The compilation model — iterate in csi, ship with csc — maps naturally onto how engineers actually work. And Scheme itself, once the parentheses stop feeling foreign, is one of the most expressive ways to write programs that work correctly.

Comments