fosskers/raylib

Hand-written bindings to Raylib for Common Lisp.

44

stars

29

commits

Common Lisp

primary language

Apr 28, 2026

updated

common-lisp
raylib

README

#+title: Raylib

This library provides hand-written FFI bindings to [[https://github.com/raysan5/raylib/][Raylib]] for SBCL and ECL.

Writing them by hand and avoiding CFFI has proven to:

- have fewer dependencies
- avoid runtime issues involving ASDF / UIOP
- have much better performance

Keep in mind, however, that since these bindings are hand-written, not all
functions are available. There may also occasionally be drift between the SBCL
and ECL as well.

Games made with this:

- [[https://github.com/fosskers/aero-fighter][Aero Fighter]] 

* Table of Contents :TOC_5_gh:noexport:
- [[#compilation][Compilation]]
  - [[#shared-objects][Shared Objects]]
  - [[#other-dependencies][Other Dependencies]]
  - [[#test-run][Test Run]]
- [[#compiler-notes][Compiler Notes]]
  - [[#sbcl][SBCL]]
    - [[#loading][Loading]]
    - [[#binding-techniques][Binding Techniques]]
      - [[#types-and-construction][Types and Construction]]
      - [[#field-access][Field Access]]
      - [[#booleans][Booleans]]
  - [[#ecl][ECL]]
    - [[#loading-1][Loading]]
    - [[#binding-techniques-1][Binding Techniques]]
      - [[#headers][Headers]]
      - [[#types-and-construction-1][Types and Construction]]
      - [[#freeing-memory][Freeing Memory]]
      - [[#field-access-1][Field Access]]
      - [[#booleans-1][Booleans]]
- [[#depending-on-this][Depending on This]]
  - [[#downstream-makefile][Downstream Makefile]]
  - [[#creating-release-builds][Creating Release Builds]]

* Compilation

** Shared Objects

The Raylib C code has been vendored into this repository. To build it,
as well as the "shim" code necessary to work around Raylib's pattern
of passing all structs by-value, do:

#+begin_example
make
#+end_example

This will produce =liblisp-raylib.so= and =liblisp-raylib-shim.so= in =lib/=.

** Other Dependencies

Luckily there is only one other dependency: =trivial-garbage=. You can fetch it
with [[https://github.com/fosskers/vend][vend]] or similar tools:

#+begin_example
vend get
#+end_example

** Test Run

Once the =raylib= system builds and loads, you can test it with the small game
loop sample at the bottom of the =package.lisp= file. If a window opens and you
see the FPS counter, then it works. Press ESC to close the window.

* Compiler Notes

As mentioned, this library does not use CFFI, a "convenience" library generally
advertised to simplify the process of binding to C libraries. Convenient though
it is, it comes at a cost I deemed unacceptable for game development. Hence it
was necessary to crack open the compiler manuals and write the bindings
separately for each compiler. It's honestly not that much work, especially if
you know you'll only ever bind to a subset of the entire underlying API.

** SBCL

Despite being a Lisp-in-Lisp compiler, its C handling is excellent.

*** Loading

The SBCL variant builds and loads as-is via a usual =asdf:load-system=.

If you alter the bindings during development, it's enough to dynamically call
the =load-shared-objects= function to update what's in your running image.

*** Binding Techniques

**** Types and Construction

Let's observe how the =Vector2= type and its constructor =_MakeVector2= are bound.

"Wait a minute," I hear you thinking, "Raylib is C - it has no special
constructor for =Vector2=." And you'd be right: =_MakeVector2= is a shim function
that heap-allocates a =Vector2= for us and returns the pointer.

#+begin_src c
Vector2 *_MakeVector2(float x, float y) {
  Vector2 *v = malloc(sizeof(Vector2));

  v->x = x;
  v->y = y;

  return v;
}
#+end_src

"Hold on," you pipe up again, "why pointers? Raylib passes everything around
by-value." Right again. Unfortunately, neither SBCL nor ECL support by-value
struct passing at the moment. So instead we do everything with pointers
to the structs we need:

#+begin_src lisp
(define-alien-type nil
    (struct vector2-raw
            (x float)
            (y float)))

(define-alien-routine ("_MakeVector2" make-vector2-raw) (* (struct vector2-raw))
  (x float)
  (y float))
#+end_src

This isn't quite useful, as we can't easily access the inner fields without
arcane calls, nor does the Garbage Collector know what to do with this. We wrap
some more:

#+begin_src lisp
(defstruct (vector2 (:constructor @vector2))
  (pointer nil :type (alien (* (struct vector2-raw
                                       (x single-float :offset 0)
                                       (y single-float :offset 32))))))

(declaim (ftype (function (&key (:x real) (:y real)) vector2) make-vector2))
(defun make-vector2 (&key x y)
  (let* ((ptr (make-vector2-raw (float x) (float y)))
         (v   (@vector2 :pointer ptr)))
    (tg:finalize v (lambda () (free-alien ptr)))))
#+end_src

Three things to note:

1. It is critical for SBCL that the =:offset= values are set correctly within the
   type hint. Otherwise it has to do a lot of guessing at runtime and you'll see
   a big performance hit.
2. We see =trivial-garbage:finalize= in action. This ensures that as our wrapper
   CL struct is getting cleaned up, it will free the underlying C memory.
3. We add a =declaim= mostly for documentation purposes, but also to express for
   convenience that this function can flexibly accept most number types as
   input, enabling:

#+begin_src lisp
(raylib:make-vector2 :x 0 :y 0)  ; No need to pass 0.0
#+end_src

**** Field Access

We use a macro:

#+begin_src lisp
(defmacro vector2-x (v)
  "The X slot of a `Vector2'."
  `(slot (vector2-pointer ,v) 'x))
#+end_src

Since =slot= can be used with =setf= as well, =vector2-x= (etc.) naturally becomes
both a getter and a setter.

Other Raylib functions that require a =Vector2= as input are bound in such a way
that they accept our wrapped =vector2= and internally unwrap it before calling
down into C.
**** Booleans

When interpreting a C bool back into Lisp, SBCL needs to be told exactly how
big, in bits, the underlying number value was. For =stdlib= bools, this is 8 bits:

#+begin_src lisp
(define-alien-routine ("IsGamepadAvailable" is-gamepad-available) (boolean 8)
  (gamepad int))
#+end_src

Otherwise you will get very strange overflowing behaviour, and calls that should
yield =T= will not.

** ECL

ECL is a bit more sensitive than SBCL, but still fully functional if you know
what to be careful of.

*** Loading

The =libffi= system dependency incurs a performance penalty. Further, with future
aims of compiling to WASM, we wish to avoid this dependency altogether. Hence
our ECL-based bindings are entirely "static" and avoid its =:dffi= feature.

This means that during development, we need to load our system in a special way:

#+begin_src lisp
(progn
  (let* ((path (merge-pathnames "lib/" (ext:getcwd)))
         (args (format nil "-Wl,-rpath,~a -L~a" path path)))
    (setf c:*user-linker-flags* args)
    (setf c:*user-linker-libs*  "-llisp-raylib -llisp-raylib-shim"))
  (asdf:load-system :raylib :force t))
#+end_src

This code can be found in the =repl.lisp= file, which you can run to load these
bindings in the expected way. After that, develop as normal. Keep in mind
however that when you compile a new function, do so at the file-level (with =C-c
C-k= or otherwise) at not at the individual function level (=C-c C-c=).

*** Binding Techniques

**** Headers

ECL transforms our bindings directly into C code. If we're calling any external
functions, we need to tell ECL about them. =clines= injects raw C into the
resulting compiled file:

#+begin_src lisp
;; For access to my various `_Foo' functions.
(ffi:clines "#include \"shim.h\"")
;; For access to `free'.
(ffi:clines "#include <stdlib.h>")
#+end_src

**** Types and Construction

As with SBCL, let's look at how we bind to =Vector2=.

#+begin_src lisp
(ffi:def-struct vector2-raw
    (x :float)
  (y :float))

(ffi:def-function ("_MakeVector2" make-vector2-raw)
    ((x :float)
     (y :float))
  :returning (* vector2-raw))
#+end_src

These are actually macros that call down into similar primitives for injecting
raw C right into the file.

#+begin_src lisp
(defstruct (vector2 (:constructor @vector2))
  (pointer nil :type si:foreign-data))

(defun make-vector2 (&key x y)
  (let* ((ptr (make-vector2-raw x y))
         (v   (@vector2 :pointer ptr)))
    (tg:finalize v (lambda () (free! ptr)))))
#+end_src

Somewhat simpler than the SBCL, as we don't need to hand-hold the =:type= hint.
Garbage Collection, however, requires special attention.

**** Freeing Memory

Note the =free!= within the finalizer above.

#+begin_src lisp
;; NOTE: 2025-01-03 This is highly bespoke and comes directly from the maintainer of ECL.
(defun free! (ptr)
  "A custom call to C's `free' that ensures everything is properly reset."
  (ffi:c-inline (ptr) (:object) :void
                "void *ptr = ecl_foreign_data_pointer_safe(#0);
                 #0->foreign.size = 0;
                 #0->foreign.data = NULL;
                 free(ptr);" :one-liner nil))
#+end_src

It's magic but it works. Without this, you will get segfaults.

**** Field Access

#+begin_src lisp
(defmacro vector2-x (v)
  "The X slot of a `Vector2'."
  `(ffi:get-slot-value (vector2-pointer ,v) 'vector2-raw 'x))
#+end_src

As with SBCL, this can be used as both a getter and a setter.

**** Booleans

ECL doesn't seem to interpret C =stblib= bools back into a friendly Lisp type, so
we need to help it:

#+begin_src lisp
(ffi:def-function ("IsGamepadAvailable" is-gamepad-available-raw)
    ((gamepad :int))
  :returning :unsigned-byte)

(defun is-gamepad-available (n)
  (= 1 (is-gamepad-available-raw n)))
#+end_src

* Depending on This
** Downstream Makefile

Your =Makefile= in a project that depends on this could look this:

#+begin_src makefile
PLATFORM ?= PLATFORM_DESKTOP_GLFW

dev: lib/ lib/liblisp-raylib.so lib/liblisp-raylib-shim.so

lib/:
	mkdir lib/

lib/liblisp-raylib.so:
	cd vendored/raylib/ && $(MAKE) PLATFORM=$(PLATFORM)
	cp vendored/raylib/lib/liblisp-raylib.so lib/

lib/liblisp-raylib-shim.so: lib/liblisp-raylib.so
	cp vendored/raylib/lib/liblisp-raylib-shim.so lib/

clean:
	rm -rf lib/
	cd vendored/raylib/ && $(MAKE) clean
#+end_src

This copies the underlying =.so= files into a =lib/= local to your application, so
that when the =raylib= system loads, it will find them where it expects.

** Creating Release Builds

Not written in Markdown, so it's shown here as plain text — view it formatted on GitHub.

Contributors

fosskers

28 commits

fosskers/raylib

Hand-written bindings to Raylib for Common Lisp.

44

stars

29

commits

Common Lisp

primary language

Apr 28, 2026

updated

common-lisp
raylib

README

#+title: Raylib

This library provides hand-written FFI bindings to [[https://github.com/raysan5/raylib/][Raylib]] for SBCL and ECL.

Writing them by hand and avoiding CFFI has proven to:

- have fewer dependencies
- avoid runtime issues involving ASDF / UIOP
- have much better performance

Keep in mind, however, that since these bindings are hand-written, not all
functions are available. There may also occasionally be drift between the SBCL
and ECL as well.

Games made with this:

- [[https://github.com/fosskers/aero-fighter][Aero Fighter]] 

* Table of Contents :TOC_5_gh:noexport:
- [[#compilation][Compilation]]
  - [[#shared-objects][Shared Objects]]
  - [[#other-dependencies][Other Dependencies]]
  - [[#test-run][Test Run]]
- [[#compiler-notes][Compiler Notes]]
  - [[#sbcl][SBCL]]
    - [[#loading][Loading]]
    - [[#binding-techniques][Binding Techniques]]
      - [[#types-and-construction][Types and Construction]]
      - [[#field-access][Field Access]]
      - [[#booleans][Booleans]]
  - [[#ecl][ECL]]
    - [[#loading-1][Loading]]
    - [[#binding-techniques-1][Binding Techniques]]
      - [[#headers][Headers]]
      - [[#types-and-construction-1][Types and Construction]]
      - [[#freeing-memory][Freeing Memory]]
      - [[#field-access-1][Field Access]]
      - [[#booleans-1][Booleans]]
- [[#depending-on-this][Depending on This]]
  - [[#downstream-makefile][Downstream Makefile]]
  - [[#creating-release-builds][Creating Release Builds]]

* Compilation

** Shared Objects

The Raylib C code has been vendored into this repository. To build it,
as well as the "shim" code necessary to work around Raylib's pattern
of passing all structs by-value, do:

#+begin_example
make
#+end_example

This will produce =liblisp-raylib.so= and =liblisp-raylib-shim.so= in =lib/=.

** Other Dependencies

Luckily there is only one other dependency: =trivial-garbage=. You can fetch it
with [[https://github.com/fosskers/vend][vend]] or similar tools:

#+begin_example
vend get
#+end_example

** Test Run

Once the =raylib= system builds and loads, you can test it with the small game
loop sample at the bottom of the =package.lisp= file. If a window opens and you
see the FPS counter, then it works. Press ESC to close the window.

* Compiler Notes

As mentioned, this library does not use CFFI, a "convenience" library generally
advertised to simplify the process of binding to C libraries. Convenient though
it is, it comes at a cost I deemed unacceptable for game development. Hence it
was necessary to crack open the compiler manuals and write the bindings
separately for each compiler. It's honestly not that much work, especially if
you know you'll only ever bind to a subset of the entire underlying API.

** SBCL

Despite being a Lisp-in-Lisp compiler, its C handling is excellent.

*** Loading

The SBCL variant builds and loads as-is via a usual =asdf:load-system=.

If you alter the bindings during development, it's enough to dynamically call
the =load-shared-objects= function to update what's in your running image.

*** Binding Techniques

**** Types and Construction

Let's observe how the =Vector2= type and its constructor =_MakeVector2= are bound.

"Wait a minute," I hear you thinking, "Raylib is C - it has no special
constructor for =Vector2=." And you'd be right: =_MakeVector2= is a shim function
that heap-allocates a =Vector2= for us and returns the pointer.

#+begin_src c
Vector2 *_MakeVector2(float x, float y) {
  Vector2 *v = malloc(sizeof(Vector2));

  v->x = x;
  v->y = y;

  return v;
}
#+end_src

"Hold on," you pipe up again, "why pointers? Raylib passes everything around
by-value." Right again. Unfortunately, neither SBCL nor ECL support by-value
struct passing at the moment. So instead we do everything with pointers
to the structs we need:

#+begin_src lisp
(define-alien-type nil
    (struct vector2-raw
            (x float)
            (y float)))

(define-alien-routine ("_MakeVector2" make-vector2-raw) (* (struct vector2-raw))
  (x float)
  (y float))
#+end_src

This isn't quite useful, as we can't easily access the inner fields without
arcane calls, nor does the Garbage Collector know what to do with this. We wrap
some more:

#+begin_src lisp
(defstruct (vector2 (:constructor @vector2))
  (pointer nil :type (alien (* (struct vector2-raw
                                       (x single-float :offset 0)
                                       (y single-float :offset 32))))))

(declaim (ftype (function (&key (:x real) (:y real)) vector2) make-vector2))
(defun make-vector2 (&key x y)
  (let* ((ptr (make-vector2-raw (float x) (float y)))
         (v   (@vector2 :pointer ptr)))
    (tg:finalize v (lambda () (free-alien ptr)))))
#+end_src

Three things to note:

1. It is critical for SBCL that the =:offset= values are set correctly within the
   type hint. Otherwise it has to do a lot of guessing at runtime and you'll see
   a big performance hit.
2. We see =trivial-garbage:finalize= in action. This ensures that as our wrapper
   CL struct is getting cleaned up, it will free the underlying C memory.
3. We add a =declaim= mostly for documentation purposes, but also to express for
   convenience that this function can flexibly accept most number types as
   input, enabling:

#+begin_src lisp
(raylib:make-vector2 :x 0 :y 0)  ; No need to pass 0.0
#+end_src

**** Field Access

We use a macro:

#+begin_src lisp
(defmacro vector2-x (v)
  "The X slot of a `Vector2'."
  `(slot (vector2-pointer ,v) 'x))
#+end_src

Since =slot= can be used with =setf= as well, =vector2-x= (etc.) naturally becomes
both a getter and a setter.

Other Raylib functions that require a =Vector2= as input are bound in such a way
that they accept our wrapped =vector2= and internally unwrap it before calling
down into C.
**** Booleans

When interpreting a C bool back into Lisp, SBCL needs to be told exactly how
big, in bits, the underlying number value was. For =stdlib= bools, this is 8 bits:

#+begin_src lisp
(define-alien-routine ("IsGamepadAvailable" is-gamepad-available) (boolean 8)
  (gamepad int))
#+end_src

Otherwise you will get very strange overflowing behaviour, and calls that should
yield =T= will not.

** ECL

ECL is a bit more sensitive than SBCL, but still fully functional if you know
what to be careful of.

*** Loading

The =libffi= system dependency incurs a performance penalty. Further, with future
aims of compiling to WASM, we wish to avoid this dependency altogether. Hence
our ECL-based bindings are entirely "static" and avoid its =:dffi= feature.

This means that during development, we need to load our system in a special way:

#+begin_src lisp
(progn
  (let* ((path (merge-pathnames "lib/" (ext:getcwd)))
         (args (format nil "-Wl,-rpath,~a -L~a" path path)))
    (setf c:*user-linker-flags* args)
    (setf c:*user-linker-libs*  "-llisp-raylib -llisp-raylib-shim"))
  (asdf:load-system :raylib :force t))
#+end_src

This code can be found in the =repl.lisp= file, which you can run to load these
bindings in the expected way. After that, develop as normal. Keep in mind
however that when you compile a new function, do so at the file-level (with =C-c
C-k= or otherwise) at not at the individual function level (=C-c C-c=).

*** Binding Techniques

**** Headers

ECL transforms our bindings directly into C code. If we're calling any external
functions, we need to tell ECL about them. =clines= injects raw C into the
resulting compiled file:

#+begin_src lisp
;; For access to my various `_Foo' functions.
(ffi:clines "#include \"shim.h\"")
;; For access to `free'.
(ffi:clines "#include <stdlib.h>")
#+end_src

**** Types and Construction

As with SBCL, let's look at how we bind to =Vector2=.

#+begin_src lisp
(ffi:def-struct vector2-raw
    (x :float)
  (y :float))

(ffi:def-function ("_MakeVector2" make-vector2-raw)
    ((x :float)
     (y :float))
  :returning (* vector2-raw))
#+end_src

These are actually macros that call down into similar primitives for injecting
raw C right into the file.

#+begin_src lisp
(defstruct (vector2 (:constructor @vector2))
  (pointer nil :type si:foreign-data))

(defun make-vector2 (&key x y)
  (let* ((ptr (make-vector2-raw x y))
         (v   (@vector2 :pointer ptr)))
    (tg:finalize v (lambda () (free! ptr)))))
#+end_src

Somewhat simpler than the SBCL, as we don't need to hand-hold the =:type= hint.
Garbage Collection, however, requires special attention.

**** Freeing Memory

Note the =free!= within the finalizer above.

#+begin_src lisp
;; NOTE: 2025-01-03 This is highly bespoke and comes directly from the maintainer of ECL.
(defun free! (ptr)
  "A custom call to C's `free' that ensures everything is properly reset."
  (ffi:c-inline (ptr) (:object) :void
                "void *ptr = ecl_foreign_data_pointer_safe(#0);
                 #0->foreign.size = 0;
                 #0->foreign.data = NULL;
                 free(ptr);" :one-liner nil))
#+end_src

It's magic but it works. Without this, you will get segfaults.

**** Field Access

#+begin_src lisp
(defmacro vector2-x (v)
  "The X slot of a `Vector2'."
  `(ffi:get-slot-value (vector2-pointer ,v) 'vector2-raw 'x))
#+end_src

As with SBCL, this can be used as both a getter and a setter.

**** Booleans

ECL doesn't seem to interpret C =stblib= bools back into a friendly Lisp type, so
we need to help it:

#+begin_src lisp
(ffi:def-function ("IsGamepadAvailable" is-gamepad-available-raw)
    ((gamepad :int))
  :returning :unsigned-byte)

(defun is-gamepad-available (n)
  (= 1 (is-gamepad-available-raw n)))
#+end_src

* Depending on This
** Downstream Makefile

Your =Makefile= in a project that depends on this could look this:

#+begin_src makefile
PLATFORM ?= PLATFORM_DESKTOP_GLFW

dev: lib/ lib/liblisp-raylib.so lib/liblisp-raylib-shim.so

lib/:
	mkdir lib/

lib/liblisp-raylib.so:
	cd vendored/raylib/ && $(MAKE) PLATFORM=$(PLATFORM)
	cp vendored/raylib/lib/liblisp-raylib.so lib/

lib/liblisp-raylib-shim.so: lib/liblisp-raylib.so
	cp vendored/raylib/lib/liblisp-raylib-shim.so lib/

clean:
	rm -rf lib/
	cd vendored/raylib/ && $(MAKE) clean
#+end_src

This copies the underlying =.so= files into a =lib/= local to your application, so
that when the =raylib= system loads, it will find them where it expects.

** Creating Release Builds

Not written in Markdown, so it's shown here as plain text — view it formatted on GitHub.

Contributors

fosskers

28 commits

Languages

Common Lisp

83.7%

C

13.9%

Makefile

2.4%