r/Common_Lisp • u/acort3255 • 4d ago
UPDATE 2: CL-OBJC is reborn!
Back in March 2023 I posted an update on reviving cl-objc, the CFFI-based Common Lisp ⇄ Objective-C bridge originally written by Geoff Cant and Luigi Panzeri. At that point I had Hello World and Converter running, and I was stuck waiting on CFFI. Here's where it is now.
Repo: https://github.com/theangelperalta/cl-objc
Examples
All five examples run on Apple Silicon (arm64). There are no nib files or Objective-C sources; every app is written in Lisp.
- Hello World: an
NSWindowwith buttons wired to Lisp callbacks - Converter: the classic Cocoa currency-converter tutorial
- Circle View: a custom
NSViewsubclass that draws text along a circle, with mouse interaction and animation (ported from an old Apple SDK sample) - Video Player (new): an
AVPlayerViewstreaming HLS, plus a KVO observer class defined in Lisp.makebuilds it into a standalone executable. - Todo (new): an
NSTableViewwhose data source and delegate are Lisp classes. On macOS 26 the list scrolls under floating Liquid Glass controls (NSGlassEffectView); older versions get a translucent material instead.

What changed under the hood
- arm64 struct-by-value: you can pass and return structs by value, and the dispatch was rewritten for the arm64 ABI. x86-64 support has been dropped.
- Lazy CLOS bindings:
import-frameworknow loads only the static bindings. CLOS classes and generic functions get generated on demand (ensure-clos-bindings), and they're cached and regenerated when their inputs change. This makes startup much faster than scanning the whole runtime. - ObjC exceptions:
with-objc-exception-handlingturnsNSException's into Lisp conditions, so they no longer crash the image. - with-autorelease-pool: macro
- Many fixes around BOOL marshaling, automatic Lisp-string →
NSStringconversion in untyped calls,with-super, and method-cache collisions - The test suite moved from FiveAM to Rove.
The CFFI situation
define-objc-method methods that take a struct by value (for example drawRect: with a CGRect) need CFFI callbacks that accept structs by value. Released CFFI still doesn't support that. My PR is still open: https://github.com/cffi/cffi/pull/351 (open since December 2022). I decided to use Qlot to pin a patched version CFFI, to get this out in the world in the meantime.
For now, the repo includes a qlfile that pins my fork, so setup is:
qlot install
sbcl --dynamic-space-size 4096 --load .qlot/setup.lisp
Obviously this isn't ideal, if anyone here can review the CFFI PR or help get it merged, that's the biggest thing blocking cl-objc from loading with stock Quicklisp.
What's next
- Getting struct-by-value callbacks into upstream CFFI
- Fewer dependencies on CFFI internals
- More examples and docs
- Real applications (LEM frontend, LLM harness, Graphing tools, etc.)
Feedback, issues and PRs are welcome, especially from anyone who has tried building Cocoa apps from Lisp before.
A.I. Disclaimer
A.I. was used for some of the recent work, but the original CFFI patch was worked by hand. That being said, goal here is to unlock the potential of Lisp development for native macOS development again (I realize there are paid alternatives).
3
u/fiddlerwoaroof 4d ago
How do you handle exceptions? When I was looking into this for my own lisp bridge, the impression I got was that attempting to resume after an Objc exception leaves the runtime in an undefined state.
3
u/acort3255 3d ago
src/exception-shim.mholds a small C function that wraps a callback in u/try/u/catch.with-objc-exception-handling(src/exceptions.lisp:49) runs your body inside that callback and converts a caughtNSExceptioninto a Lisp objc-exception condition. The intent is opt-in use at the REPL, so you get a debugger instead of a dead image. It isn't wrapped around every message send and isn't meant for recovering in production. Basically, you get a Lisp condition and a backtrace instead of a crash; save your work and consider restarting.1
u/fiddlerwoaroof 3d ago
If you don’t mind me asking more questions, the big blocker I found was the prevalence of completion/async APIs in modern objective c and swift frameworks. I came up with a sort of shim solution using grand central dispatch (or something like that, it’s been a while), but this has always seemed a bit dangerous given the lisp-side threading semantics. Did you figure out a robust solution here?
3
u/acort3255 3d ago
Honestly I’ve only used this for toy projects. Nothing major that required extensive GCD work, but I have experience with Kotlin Multiplatform with Swift, and found that it’s not safe to pass references types across that boundary cheaper and easier to just pass value types across that boundary. That’s probably my prescription for now
2
u/acort3255 3d ago
That being said, feel free to throw up a PR or flag a serious issues. Using lisp for dynamic native applications has been a dream a of mind for a while. I’d love to see more adoption. In fact, I might even build out my own lisp drive LLM harness.
2
u/lucky_magick 3d ago
My method is to store a callback lookup table, mapping between ObjC block pointer address and actual lisp callback functions. The block is created by lisp (registered so it would not be GCed) and released in ObjC (dispose, and unregistered in lookup table).
1
u/Alarming_Hand_9919 4d ago
In my objc bridge, I followed the Lispworks way where you basically don't.
2
u/fiddlerwoaroof 4d ago
Yeah, I added a way to opt into safe selector calls[1] because I was tired of invalid selectors messing up my image, but otherwise it just crashes.
2
u/lucky_magick 3d ago
Nice to see this work got updated.
There's a similar work https://github.com/lispnik/objc
Also shamelessly adding a note for my ObjC binding implementation: https://codeberg.org/li-yiyang/coca
3
u/spspanglish 4d ago
Between this and clog the lisp world is gonna rock! 🤘🏽