Generating classes in Clojure
Posted on 2025-03-01
I rarely use the gen-class
facility in Clojure, but when I do, I usually trip over one issue or another. After finally spending some time to read the documentation and peruse source code, I’ve got a better understanding of how it all fits together.
Write that down, write that down!
When Clojure code is evaluated, the compiler generates JVM bytecode and loads it into memory. For various reasons, such as improved startup time, it can be advantageous to compile namespaces into actual classfiles. These compiled namespaces need to be import
ed instead of require
d.
A namespace is normally compiled into a class ending in __init
. For the simplest namespace:
(ns clj-genclass.core)
We can compile with Leiningen:
lein compile clj-genclass.core
And see the class core__init.class
under target/classes/clj_genclass
.
Lookin’ fresh
The resulting class isn’t very convenient to work with, however. That’s where :gen-class
comes in: it provides a large number of options1 that make the generated class more like the Java library classes we interop with.
To automatically compile classes before starting a Lein REPL, the namespace needs to be added under :aot
in project.clj
. Consider the following:
(ns clj-genclass.MyClass
(:gen-class
:constructors {[String] []}
:init init
:state state
:methods [[print [] void]]))
(defn -init
[txt]
[[] txt])
(defn -print
[this]
(println (.state this)))
:aot [clj-genclass.MyClass]
Running lein repl
, we see that the class is compiled:
$ lein repl
Compiling clj-genclass.MyClass
nREPL server started on port 57734 on host 127.0.0.1 - nrepl://127.0.0.1:57734
And can be immediately imported and used:
clj-genclass.core=> (import 'clj_genclass.MyClass)
clj_genclass.MyClass
clj-genclass.core=> (.print (MyClass. "My own class"))
My own class
Can’t depend on imports
What happens when one namespace import
s the class generated by another?
(ns clj-genclass.A
(:gen-class))
(ns clj-genclass.B
(:import clj_genclass.A)
(:gen-class))
:aot [clj-genclass.B
clj-genclass.A]
It would seem that ordering B
before A
should be fine thanks to namespace dependency resolution. However, compiling results in an error:
$ lein compile
Compiling clj-genclass.B
Syntax error macroexpanding at (B.clj:1:1).
Execution error (ClassNotFoundException) at java.net.URLClassLoader/findClass (URLClassLoader.java:445).
clj_genclass.A
The problem is that :import
does not establish a dependency between namespaces. To do so we need a :require
:
(ns clj-genclass.B
(:require [clj-genclass.A])
(:import clj_genclass.A)
(:gen-class))
One issue with this is that it doesn’t play nice with clj-refactor’s cljr-clean-ns
, which removes unused :require
s.
The function is dead, long live the function
If we update MyClass
’s -print
method, e.g.
(defn -print
[this]
(println "foo" (.state this)))
We can see the changes take place either by compiling the class in the REPL:
clj-genclass.core=> (compile 'clj-genclass.MyClass)
clj_genclass.MyClass
clj-genclass.core=> (import 'clj_genclass.MyClass)
clj_genclass.MyClass
clj-genclass.core=> (.print (MyClass. "My own class"))
foo My own class
Or just evaluating it in CIDER. lein compile
does not work because it only writes to classfiles and doesn’t load anything into memory.
The old switcheroo
Classes in Java are loaded by instances of ClassLoader2. Once an instance of ClassLoader
has loaded a class, it must always return what it already loaded. This means that it cannot reload a class whose definition has changed.
To get around this, Clojure takes advantage of the fact that a class is distinguished not just by its name but also by its classloader. Every time a form is (re)evaluated, a new instance of Clojure’s own DynamicClassLoader is used. We can see this in action:
clj-genclass.core=> (defn foo [] "foo")
#'clj-genclass.core/foo
clj-genclass.core=> (.getClassLoader (.getClass foo))
#object[clojure.lang.DynamicClassLoader 0x41f045a1 "clojure.lang.DynamicClassLoader@41f045a1"]
clj-genclass.core=> (defn foo [] "bar")
#'clj-genclass.core/foo
clj-genclass.core=> (.getClassLoader (.getClass foo))
#object[clojure.lang.DynamicClassLoader 0x32035f8f "clojure.lang.DynamicClassLoader@32035f8f"]
When foo
was redefined, the new version was loaded by a different classloader. All instances of DynamicClassLoader
share a cache so they can all find previously loaded classes. Classes that are no longer used (such as those that have been redefined) are eventually removed.
Footnotes
-
:gen-class
by itself does not compile a class–it only specifies how a class should be compiled. ↩ -
Except for the classes of the Java Runtime itself, which are loaded by a bootstrap classloader written in native code. ↩