Driving plot from Common Lisp with SBCL
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.