Driving plot from Common Lisp with SBCL

Driving plot from Common Lisp with SBCL

🌐 Auf Deutsch lesen
📅 2026-10-18 ✍️ Andreas Wittmann 👁️ ... lisp sbcl graphics

The plot article ends with a short mention that a Common Lisp client for plot_server.cpp's socket protocol already exists, without covering it — that was deliberately left for its own write-up. This is that write-up: loading the client into SBCL, connecting to the server, and working through a handful of examples.

What's SBCL-specific here

The client (package.lisp, plot.lisp, plot.asd — all linked from the plot article) is written against SBCL's own implementation-specific packages, not portable Common Lisp: sb-bsd-sockets for the Unix domain socket, sb-thread:make-mutex to serialize concurrent calls onto one connection, and sb-ext:run-program/sb-ext:string-to-octets for starting the server and encoding UTF-8 text. Porting it to another implementation would mean swapping those for usocket, bordeaux-threads, and uiop:run-program respectively — none of that is done here, so this article is specifically about SBCL.

Prerequisites

SBCL itself:

apt install sbcl

The native plot-server binary, built from the same directory (see the plot article's section on it):

make plot-server

plot:open starts this server for you automatically if none is running yet — but it does so with sb-ext:run-program ... :search t, which looks up plot-server on $PATH. Make sure the directory containing the freshly built binary is on PATH, or copy/symlink it somewhere that already is.

Loading the system

ASDF needs to know where to find the plot system. The quickest way for a one-off session is pushing the directory onto asdf:*central-registry* directly:

(require :asdf)
(push #P"/path/to/posts/plot/common-lisp/plot/" asdf:*central-registry*)
(asdf:load-system "plot")

For something more permanent, symlink (or place) the plot/ directory under ~/.local/share/common-lisp/source/, which is on ASDF's default source-registry search path — then plain (asdf:load-system "plot") finds it without the push step.

Either way, loading it gives you a plot package with (among others) plot:open, plot:clear, plot:color, plot:pixel, plot:line, plot:rectangle, plot:filled-rectangle, plot:triangle, plot:cross, plot:text, plot:present, plot:auto-present, plot:mode, plot:close, and the plot:with-frame macro — a direct mapping onto the socket protocol commands documented in the plot article.

A first session

(plot:open)                     ; connects, or starts plot-server if needed

(plot:clear 255 255 255)        ; white background
(plot:color 200 0 0)
(plot:line -50 0 50 0)

(plot:color 0 150 0)
(plot:rectangle -30 -30 30 30)

(plot:color 0 0 200 128)        ; the optional 4th argument is alpha
(plot:filled-rectangle 10 10 40 40)

(plot:cross)                    ; draws the x/y axes

(plot:color 0 0 0)
(plot:triangle 0 -40 -20 -10 20 -10)

(plot:text 5 5 "Hällo, λ!")     ; non-ASCII text works -- it's UTF-8 under the hood

(plot:present)                  ; nothing appears in the window until this
(plot:close)

plot:open with no arguments connects on the default socket path, at the default 800×600 size — the same defaults plot-server itself uses. Pass (plot:open width height path) to override any of them.

Every one of those calls round-trips over the socket and blocks for an OK/ERR reply (see the protocol table in the plot article for exactly which text command each one sends); an ERR reply, or any lower-level socket error, signals a Lisp error rather than failing silently.

with-frame and auto-present

Typing (plot:present) after every single shape gets tedious outside a quick REPL experiment. Two different conveniences cover two different situations.

plot:with-frame wraps a body of drawing calls and presents once at the end — good for a self-contained snapshot:

(plot:with-frame (:clear t)
  (plot:color 128 0 128)
  (plot:line -10 -10 10 10))

plot:auto-present instead flips the server's behavior for every following command until you turn it back off — good for interactively poking at the canvas from the REPL and seeing each shape land immediately:

(plot:auto-present t)
(plot:pixel 1 1)                ; shows up right away
(plot:auto-present nil)         ; back to manual plot:present

A simple animation

Nothing here is specific to Lisp — it's the same idea as driving the socket from any other language, just with Lisp's dotimes instead of a for loop. A rectangle sweeping left to right, redrawing the whole frame each step:

(plot:open)
(plot:mode 1)          ; PLOT_MODE_CROSS: origin at the center
(plot:auto-present t)

(dotimes (i 20)
  (plot:clear 240 240 240)
  (plot:color 200 0 0)
  (plot:filled-rectangle (- (* i 5) 100) -5
                          (+ (- (* i 5) 100) 10) 5))

(plot:close)

With auto-present enabled, every filled-rectangle call in the loop flushes to the window on its own — no explicit present needed inside the loop body.

Conclusion

The Lisp side of this adds nothing to the protocol itself — every call here is a thin wrapper that formats one line of text, writes it to the socket, and waits for OK. What it buys is a REPL: connect once, draw a shape, see it immediately, change a number, draw it again, without recompiling or restarting anything. That immediacy is the actual point of driving plot from Lisp rather than C++ — the protocol in the plot article was designed for exactly this kind of external, possibly long-lived client.