Skip to content

Instantly share code, notes, and snippets.

@pfeodrippe
Last active August 4, 2025 22:59
Show Gist options
  • Save pfeodrippe/1b8501b4069ef3252acfe9517991ae16 to your computer and use it in GitHub Desktop.
Save pfeodrippe/1b8501b4069ef3252acfe9517991ae16 to your computer and use it in GitHub Desktop.
jextract + clojure + raylib
pom.xml
pom.xml.asc
*.class
/classes/
/target/
/checkouts/
.lein-deps-sum
.lein-repl-history
.lein-plugins/
.lein-failures
.nrepl-port
.lsp
.clj-kondo/.cache
.cpcache
results
.sounds
.DS_Store
*dylib
do_not_commit_me

Only tested with OSX

  • Use Java 22
  • Install raylib to your system
    • OSX, brew install raylib
  • Add jextract-raylib.sh to bin
    • Change it as needed
  • Add vybe_raylib.h to bin
  • Add build.clj to the root
  • Add the other clojure files to their appropriate places
  • Run bash bin/jextract-raylib.sh
    • Ignore the warnings
  • Run clj -T:build compile-app
  • Run clj -M:dev -m vybe.raylib to start nREPL
  • Connect to the REPL at localhost:7888
{:deps {camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"}
;; NREPL
nrepl/nrepl {:mvn/version "1.1.1"}
cider/cider-nrepl {:mvn/version "0.45.0"}}
:aliases
{ ;; Run with clj -T:build function-in-build
:build {:deps {io.github.clojure/tools.build {:git/tag "v0.9.4" :git/sha "76b78fe"}}
:ns-default build}
:dev {:jvm-opts ["--enable-preview"
"--enable-native-access=ALL-UNNAMED"
"-Djdk.attach.allowAttachSelf"
"-XstartOnFirstThread"
"-XX:+UnlockDiagnosticVMOptions"
"-XX:+DebugNonSafepoints"
"-XX:+CreateCoredumpOnCrash"
;; For brew raylib.
"-Djava.library.path=/opt/homebrew/Cellar/raylib/5.0/lib/"]
:extra-deps {io.github.clojure/tools.build {:git/tag "v0.9.4" :git/sha "76b78fe"}}}}
:paths ["src" "resources" "target/classes"]}
(ns vybe.raylib.impl
(:require
[clojure.string :as str]
[camel-snake-kebab.core :as csk])
(:import
(org.raylib raylib_h Color)
(java.lang.foreign Arena MemorySegment MemoryLayout ValueLayout)
(jdk.internal.foreign.layout ValueLayouts)))
(defonce *state
(atom {:buf-general []
:buf1 []
:buf2 []
:front-buf? true}))
(defn add-command
([cmd]
(add-command cmd {}))
([cmd {:keys [general]}]
(locking *state
(swap! *state (fn [state]
(if general
(-> state
(update :buf-general conj cmd))
(-> state
(update :buf1 conj cmd)
(update :buf2 conj cmd))))))))
(def ^:private declared-methods
(concat (:declaredMethods (bean org.raylib.raylib_h))
(:declaredMethods (bean org.raylib.raylib_h_1))))
(defn- ->type
[v]
(let [n (.name v)
type-name (when (.isPresent n)
(.get n))]
(cond
(and (= (type v)
jdk.internal.foreign.layout.ValueLayouts$OfAddressImpl)
(not type-name))
:pointer
type-name
(keyword "raylib" type-name)
:else
(case (symbol (.getName (class v)))
jdk.internal.foreign.layout.ValueLayouts$OfDoubleImpl
:double
jdk.internal.foreign.layout.ValueLayouts$OfLongImpl
:long
jdk.internal.foreign.layout.ValueLayouts$OfFloatImpl
:float
jdk.internal.foreign.layout.ValueLayouts$OfIntImpl
:int
jdk.internal.foreign.layout.ValueLayouts$OfShortImpl
:short
jdk.internal.foreign.layout.ValueLayouts$OfCharImpl
:char
jdk.internal.foreign.layout.ValueLayouts$OfByteImpl
:byte
jdk.internal.foreign.layout.ValueLayouts$OfBooleanImpl
:boolean))))
(defn layout?
[t]
(and t
(= (namespace t)
"raylib")))
(defn address?
[t]
(or (layout? t)
(= t :pointer)))
(defn raylib-methods
[]
(->> declared-methods
(filter #(str/includes? (.getName %) "$descriptor"))
#_(filter #(= (.getName %) "GetMonitorName$descriptor"))
#_(take 10)
(mapv (fn [method]
(let [desc (.invoke method nil (into-array Object []))
args (.argumentLayouts desc)
ret' (.returnLayout desc)
ret (when (.isPresent ret')
(->type (.get ret')))
desc-name ((comp :name bean) method)
main-name (str/replace desc-name #"\$descriptor" "")
main-method (->> declared-methods
(filter (comp #(= main-name
(.getName %))))
first)]
#_(def -ddd
[[:desc desc]
[:args args]
[:ret ret]
[:desc-name desc-name]
[:main-name main-name]
[:main-method main-method]])
(when-not main-method
(throw (ex-info "Method for desc does not exist"
{:desc desc
:desc-name desc-name})))
(let [args (mapv (fn [v param]
{:name (.getName param)
:clj-type (if (= v :panama/allocator)
v
(->type v))})
args
;; If return is a layout, the method
;; receives an allocator (e.g. Arena) as
;; the first arg.
(if (layout? ret)
(rest (.getParameters main-method))
(.getParameters main-method)))]
(vector main-name
{:args args
:ret ret
:has-arena? (or (layout? ret)
(some (comp address? :clj-type)
args))
:main-thread? (nil? ret)})))))))
#_ (def methods-to-intern (raylib-methods))
(defonce default-arena
(Arena/ofAuto))
(defmacro t
"Runs command (delayed) in the main thread.
Useful for REPL testing as it will block and return
the result from the command."
[& body]
`(let [*res# (promise)]
(add-command
(fn []
(let [v# ~@body]
(deliver *res# v#)
v#))
{:general true})
(let [res# (deref *res# 500 ::error)]
(when (= res# ::error)
(throw (ex-info "Error while running command"
{:form (quote ~&form)
:form-meta ~(meta &form)})))
res#)))
(defn first-thread?
[]
(= (.getId (Thread/currentThread)) 1))
(defn try-string
[s]
(if (string? s)
(.allocateFrom default-arena s)
s))
(def any-thread-methods
#{"WindowShouldClose"
"GetMonitorName"})
(defmacro -intern-methods
[init size]
`(do ~(->> (raylib-methods)
(drop init)
(take size)
(mapv (fn [[n {:keys [args ret has-arena? main-thread?]}]]
(let [ray-args (mapv (fn [{:keys [name clj-type]}]
(if (= clj-type :pointer)
``(try-string ~~(symbol name))
(symbol name)))
args)]
(try
`(defmacro ~(csk/->kebab-case-symbol (str n (if main-thread?
"!"
"")))
{:arglists (list
(quote
~(mapv (fn [{:keys [name clj-type]}]
[(symbol name) clj-type])
args)))
:doc ~(format "Returns %s." (or ret "void"))}
;; Fn args.
~(mapv (comp symbol :name) args)
;; Fn body.
~(cond
;; Functions that start with `Is` and other
;; prefixes can be safely run outside the main
;; thread.
(or (str/starts-with? n "Is")
(contains? any-thread-methods n))
``(~(symbol "org.raylib.raylib_h" ~n)
~@~(vec
(concat
(when (and has-arena? (layout? ret))
[``default-arena])
ray-args)))
(or (not main-thread?)
(and main-thread?
(str/includes? n "Window")))
``(if (first-thread?)
(~(symbol "org.raylib.raylib_h" ~n)
~@~(vec
(concat
(when (and has-arena? (layout? ret))
[``default-arena])
ray-args)))
(t (~(symbol "org.raylib.raylib_h" ~n)
~@~(vec
(concat
(when (and has-arena? (layout? ret))
[``default-arena])
ray-args)))))
:else
;; Main thread.
``(if (first-thread?)
(~(symbol "org.raylib.raylib_h" ~n)
~@~(vec
(concat
(when (layout? ret)
[''default-arena])
ray-args)))
(add-command
(with-meta
(fn ~'~'--internal-fn
([]
~~(if has-arena?
``(~'~'--internal-fn default-arena)
``(~'~'--internal-fn nil)))
([~'~'arena]
;; (org.raylib.raylib_h/WHATEVER [allocator?] and some args)
(~(symbol "org.raylib.raylib_h" ~n)
~@~(vec
(concat
(when (layout? ret)
[''arena])
ray-args)))))
{:form (quote ~~'&form)})))))
(catch Error _e
nil))))))))
(def intern-methods
(memoize
(fn []
(mapv (fn [n]
;; We use `eval` to avoid macroexpansion of
;; all the methods, which would give us a
;; "method too large" error.
(eval `(-intern-methods ~(* n 100) 100)))
(range (inc (int (/ (count (raylib-methods))
100))))))))
#_(intern-methods)
#_(macroexpand-1 '(-intern-methods 300 10))
#_(meta #'draw-text!)
#_(macroexpand-1 '(load-model "OOOB"))
#_(macroexpand-1 '(update-camera! 1 2))
#_(macroexpand-1 '(get-monitor-name 0))
(comment
())
#!/bin/bash
# You have to be using at least Java 22
# Run this script from the root folder (not from inside `bin`)
~/Downloads/jextract-22/bin/jextract -l raylib \
--output src-java \
--header-class-name raylib_h \
--use-system-load-library \
-I /opt/homebrew/Cellar/raylib/5.0/include \
-t org.raylib bin/vybe_raylib.h
(ns vybe.raylib
"Raylib stuff.
Java bindings
https://github.com/electronstudio/jaylib
Clojure bindings
https://github.com/kiranshila/raylib-clj
Cheatsheet
https://www.raylib.com/cheatsheet/cheatsheet.html"
(:require
[cider.nrepl :refer [cider-nrepl-handler]]
[nrepl.server :refer [start-server]]
[vybe.raylib :as vr]
[vybe.raylib.impl :as vr.impl])
(:import
(org.raylib raylib_h Color Vector3 Vector2 Camera Camera2D
Texture Rectangle)
(java.lang.foreign Arena ValueLayout)))
;; Intern macro methods.
(vr.impl/intern-methods)
;; Start server as we need to be on the main thread, see
;; https://medium.com/@kadirmalak/interactive-opengl-development-with-clojure-and-lwjgl-2066e9e48b52
(defonce server (start-server :port 7888 :handler cider-nrepl-handler))
;; ------ Helpers
(defmacro with-arena
[arena-sym & body]
`(with-open [~arena-sym (Arena/ofConfined)]
~@body))
(defmacro t
"Runs command (delayed) in the main thread.
Useful for REPL testing as it will block and return
the result from the command."
[& body]
`(vr.impl/t ~@ body))
;; ------ Raylib types
(defn color
[c-val]
(let [[r g b a] (mapv unchecked-byte c-val)]
(doto (Color/allocate vr.impl/default-arena)
(Color/r r)
(Color/g g)
(Color/b b)
(Color/a a))))
(defn float*
[v]
(.allocateFrom vr.impl/default-arena ValueLayout/JAVA_FLOAT (float v)))
(defn vec2
([]
(vec2 [0 0]))
([[x y]]
(doto (Vector2/allocate vr.impl/default-arena)
(Vector2/x x)
(Vector2/y y))))
(defn vec3
([]
(vec3 [0 0 0]))
([[x y z]]
(doto (Vector3/allocate vr.impl/default-arena)
(Vector3/x x)
(Vector3/y y)
(Vector3/z z))))
(defn rectangle
[x y width height]
(doto (Rectangle/allocate vr.impl/default-arena)
(Rectangle/x x)
(Rectangle/y y)
(Rectangle/width width)
(Rectangle/height height)))
(defn camera-2d
[{:keys [offset target rotation zoom]
:or {rotation 0
zoom 1}}]
(doto (Camera2D/allocate vr.impl/default-arena)
(Camera2D/offset offset)
(Camera2D/target target)
(Camera2D/rotation rotation)
(Camera2D/zoom zoom)))
(defn camera-3d
[{:keys [position target up fovy projection]
:or {fovy 45
projection (raylib_h/CAMERA_PERSPECTIVE)}}]
(doto (Camera/allocate vr.impl/default-arena)
(Camera/position position)
(Camera/target target)
(Camera/up up)
(Camera/fovy fovy)
(Camera/projection projection)))
;; ------- Misc
(defn- run-buf-general-cmds
[]
(let [{:keys [buf-general]} @vr.impl/*state]
(locking vr.impl/*state
(try
(run! (fn [cmd] (cmd)) buf-general)
(catch Exception e
(println e)
(vr/draw-text! "!! ERROR !!" 200 200 20
(vr/color [255 0 0 255])))
(finally
(swap! vr.impl/*state (fn [state]
(-> state
(assoc :buf-general [])))))))))
(defn- run-buf-1-2-cmds
[]
(let [{:keys [buf1 buf2 front-buf?]} @vr.impl/*state
buf (if front-buf? buf1 buf2)]
(locking vr.impl/*state
(try
(run! (fn [cmd]
(try
(cmd)
(catch Exception e
(println {:form (:form (meta cmd))
:exception e})
(vr/draw-text! "!! ERROR !!" 200 200 20
(vr/color [255 0 0 255])))))
(concat buf))
(finally
(swap! vr.impl/*state (fn [state]
(-> state
(assoc (if front-buf? :buf1 :buf2) [])
(update :front-buf? not)))))))))
(defonce draw
(fn []))
(defn- main-loop
[]
(run-buf-general-cmds)
(when (vr/is-window-ready)
;; TODO Let the user control begin/end of drawing for `draw`.
(raylib_h/BeginDrawing)
(try
(draw)
(catch Error e
(println e)))
(run-buf-1-2-cmds)
(raylib_h/EndDrawing)))
(defn -main
[]
(while (empty? (:buf-general @vr.impl/*state))
(Thread/sleep 30))
(while true
(main-loop)))
;; jextract, https://jdk.java.net/jextract/
;; sudo xattr -r -d com.apple.quarantine ~/Downloads/jextract-22
;; ... refs
;; calling jextract, https://docs.oracle.com/en/java/javase/21/core/call-native-functions-jextract.html#GUID-480A7E64-531A-4C88-800F-810FF87F24A1
;; jdk22, https://jdk.java.net/22/
;; sdk install java jdk-22 /Users/pfeodrippe/Downloads/jdk-22.jdk/Contents/Home
;; ... panama docs
;; javadocs, https://cr.openjdk.org/~mcimadamore/jdk/FFM_22_PR/javadoc/java.base/java/lang/foreign/Arena.html
;; native memory, https://community.sap.com/t5/technology-blogs-by-sap/from-c-to-java-code-using-panama/ba-p/13578395
;; memory access, https://github.com/openjdk/panama-foreign/blob/foreign-memaccess%2Babi/doc/panama_memaccess.md
;; struct layout, https://www.baeldung.com/java-project-panama#2-foreign-memory-manipulation
(comment
;; The macros allow you to run these commands without being in the main thread,
;; things will be sent there (and in the best case, they will run directly in
;; the main thread) \o
(vr/init-window! 600 600 "Opa")
(vr/set-target-fps! 120)
(vr/set-window-position! 1000 200)
(vr/clear-background! (vr/color [10 100 200 255]))
(vr/draw-rectangle! 30 50 100 200 (vr/color [255 100 10 255]))
;; Or run things directly in the main thread by altering the `vr/draw` var.
(def my-cam
(camera-3d {:position (vec3 [2 3 2])
:target (vec3 [0 1 0])
:up (vec3 [0 1 0])}))
(defn my-draw
[]
(-> (Camera/position my-cam)
(Vector3/y (* 3 (Math/sin (vr/get-time)))))
(vr/update-camera! my-cam (raylib_h/CAMERA_ORBITAL))
(vr/clear-background! (vr/color [255 255 0 255]))
(vr/begin-mode-3-d! my-cam)
(vr/draw-cube! (vr/vec3 [0 0 0]) 1 1 2 (vr/color [255 0 255 255]))
#_(vr/draw-model! model (vec3 [0 0 0]) 1 (color [255 255 255 255]))
(vr/draw-grid! 10 1.0)
(vr/end-mode-3-d!)
#_(vr/clear-background! (vr/color [255 255 255 255]))
(vr/draw-text! "Eis" 100 100 20
(vr/color [255 129 0 255]))
(vr/draw-fps! 20 20))
(alter-var-root #'draw (constantly #'my-draw))
())
#include <raylib.h>
#include <rlgl.h>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment