Adding a new FFI binding
The defcfn pattern
Every raylib binding is a defcfn form. Here are three real ones from src/raylib/core/window.clj lines 1-21:
(ns raylib.core.window
(:require
[raylib.core]
[raylib.internals :as ri]
[coffi.mem :as mem]
[coffi.ffi :refer [defcfn]]))
(defcfn init-window!
"Initialize window and OpenGL context"
{:arglists '([width height title])}
"InitWindow"
[::mem/int ::mem/int ::mem/c-string] ::mem/void)
(defcfn window-should-close?
"Check if KEY_ESCAPE pressed or Close icon pressed"
"WindowShouldClose"
[] ::ri/bool)
(defcfn close-window!
"Close window and unload OpenGL context"
"CloseWindow" [] ::mem/void)
defcfn (from coffi.ffi) always has the same four-part shape:
- Docstring — describes what the C function does. Copy it from
raylib.h's comment on the same line as the function signature. {:arglists '(...)}— optional. Only needed when the function takes arguments, sincedefcfn's own parameter vector is a list of types, not names —:arglistsis what makes(doc init-window!)and editor autocomplete show meaningful argument names instead of[arg0 arg1 arg2].window-should-close?andclose-window!both take no arguments, so they skip it.- C function name string — the exact symbol raylib exports (e.g.
"InitWindow"), used to look up the function in the shared library. [param-types] return-type— the parameter types vector followed by the return type, both from the type-mapping table below.
Every binding namespace requires raylib.core first (see the :require above) — that namespace is what loads the native libraylib shared library, and a defcfn can't resolve its C symbol until the library is loaded.
C -> coffi type mapping
| C Type | Coffi Type |
|---|---|
int | ::mem/int |
float | ::mem/float |
double | ::mem/double |
bool | ::ri/bool (returns 0/1) |
unsigned char | ::ri/ubyte |
const char* | ::mem/c-string |
void | ::mem/void |
Color | ::rs/color |
Vector2 | ::rs/vector-2 |
Vector3 | ::rs/vector-3 |
Rectangle | ::rs/rectangle |
Camera2D | ::rc2d/camera-2d |
Camera3D | ::rc3d/camera3d |
| Pointer (in/out param) | ::mem/pointer |
::ri/bool and ::ri/ubyte are not built-in coffi types — coffi only ships primitive types like ::mem/int and ::mem/byte out of the box. They're custom types this project defines in src/raylib/internals.clj:
(ns raylib.internals
(:require [coffi.mem :as mem]))
;; ubyte
(defmethod mem/primitive-type ::ubyte
[_type]
::mem/byte)
(defmethod mem/serialize* ::ubyte
[obj _type _scope]
(unchecked-byte obj))
(defmethod mem/deserialize* ::ubyte
[obj _type]
(Byte/toUnsignedLong obj))
;; bool
(defmethod mem/primitive-type ::bool
[_type]
::mem/byte)
(defmethod mem/serialize* ::bool
[obj _type _scope]
(byte (if obj 1 0)))
(defmethod mem/deserialize* ::bool
[obj _type]
(not (zero? obj)))
Raylib's C bool and unsigned char both travel over the FFI boundary as a single byte — that's just how they're laid out in memory. coffi's mem/primitive-type multimethod tells coffi which real primitive (::mem/byte) to use on the wire for a custom type. mem/serialize* and mem/deserialize* then teach coffi how to box and unbox that raw byte into something Clojure-friendly: ::ubyte deserializes to an unsigned Byte/toUnsignedLong value (so a byte like -1 reads back as 255, not -1), and ::bool serializes a Clojure truthy/falsy value to 1/0 and deserializes 0/non-0 back to false/true. Without these three multimethod overrides, ::ri/bool and ::ri/ubyte wouldn't exist as usable coffi types at all.
Struct definitions with defalias
C structs are defined with defalias in src/raylib/structs.clj. The full list at time of writing (grep -n "defalias" src/raylib/structs.clj): Color, Vector2, Vector3, Vector4, Texture, RenderTexture, Rectangle.
(defalias ::color
[::mem/struct
[[:r ::ri/ubyte]
[:g ::ri/ubyte]
[:b ::ri/ubyte]
[:a ::ri/ubyte]]])
(defalias ::vector-2
[::mem/struct
[[:x ::mem/float]
[:y ::mem/float]]])
Once defined, a struct behaves as a plain Clojure map: {:x 100.0 :y 200.0} for a ::vector-2, {:r 255 :g 0 :b 0 :a 255} for a ::color.
Field order must match the C struct layout exactly. coffi lays out the native memory segment for a ::mem/struct field-by-field, in the order you list them — it has no way to know raylib's real field order except from what you tell it. If raylib.h declares Color as r, g, b, a and you write the defalias fields in a different order, every read and write against that struct silently misaligns.
Pointer in/out parameters
Some raylib functions take a struct pointer and mutate it in place rather than returning a new struct — UpdateCamera(Camera3D *camera, int mode) is one. The worked example for this pattern is update-camera in src/raylib/core/camera3d.clj:
;; Camera update function
(defcfn update-camera!
"Update camera position for selected mode"
{:arglists '([camera mode])}
"UpdateCamera"
[::mem/pointer ::mem/int] ::mem/void)
;; Helper function that updates camera and returns the new state
(defn update-camera
"Update camera position for selected mode. Returns updated camera map.
mode: CAMERA_FREE, CAMERA_ORBITAL, CAMERA_FIRST_PERSON, CAMERA_THIRD_PERSON"
[camera mode]
(let [arena (mem/confined-arena)
seg (mem/alloc-instance ::camera3d arena)]
(mem/serialize-into camera ::camera3d seg arena)
(update-camera! seg mode)
(mem/deserialize-from seg ::camera3d)))
The raw defcfn (update-camera!) takes ::mem/pointer — coffi can't serialize a Clojure map directly as a pointer argument, so the wrapper function (update-camera) does the pointer dance by hand:
mem/confined-arena— creates an arena that owns the lifetime of any native memory allocated from it, scoped to this block.mem/alloc-instance— allocates a native memory segment inside that arena, sized and laid out for the::camera3dstruct.mem/serialize-into— writes the Clojurecameramap's fields into that segment, following the struct's field layout.update-camera!— the rawdefcfncall passes the segment as the pointer argument; raylib'sUpdateCameramutates the segment's bytes in place.mem/deserialize-from— reads the (now-mutated) segment back out into a fresh Clojure map, which becomes the wrapper's return value.
See Coffi & Panama Internals for what a confined arena actually is and when you need one at all.
Worked example: adding a new raylib function end-to-end
DrawRectangleGradientV is a real raylib C function that isn't bound anywhere in this repo yet — confirmed with:
grep -rn "DrawRectangleGradientV" src/raylib/
which returns nothing. It's a good pick for a worked example: a plain draw call with two Color arguments and no pointer trickery.
- Find the C signature in
raylib.h:void DrawRectangleGradientV(int posX, int posY, int width, int height, Color color1, Color color2); - Pick the target namespace. This is a shape-drawing call, so it belongs in
src/raylib/shapes/basic.clj(confirmed to exist) alongside its siblingdraw-rectangle!, which already uses the sameint int int int Colorargument shape. - Write the
defcfnform, following the type-mapping table above:(defcfn draw-rectangle-gradient-v! "Draw a vertical-gradient-filled rectangle" {:arglists '([pos-x pos-y width height color-1 color-2])} "DrawRectangleGradientV" [::mem/int ::mem/int ::mem/int ::mem/int ::rs/color ::rs/color] ::mem/void) - Verify it. Start any example (
bb basic-shapes, which already connects an embedded nREPL on port 7888), connect your editor, andclj-nrepl-evala call like(raylib.shapes.basic/draw-rectangle-gradient-v! 100 100 200 100 colors/red colors/blue)inside the running game's draw loop to confirm it renders instead of throwing.
See also
coffi-panama-internals.md— what happens underneathdefcfnwhen Clojure calls the resulting function