More on Clojure Compilation
A few days ago I wrote about how you can compile Clojure code into jar files. Well, I’d like to take a closer look at how functions are exported and how they look in Java code.
Let’s take a look at this simple “Hello, World!” program.
(ns hello-world.core)
(defn hello [subject]
(println (str "Hello, " subject "!")))
(defn -main []
(hello "World"))
Running this program will print “Hello, World!” out to the console. Now I want to be able to do a few things with the jar that this code generates:
- Invoke main
- Invoke hello from a ‘core’ object
- Invoke hello statically
Setting up Main
Assuming project.clj
has already been set up, the only thing left to do
is add :gen-class
to the file’s namespace.
(ns hello-world.core
(:gen-class))
; ...
After compilation, we can see some Java class files have been generated in
/target/classes/hello_world
–one of these will be named core.class.
If we open this up, we see some Java code has been generated with our main method.
package hello_world;
// ...
public class core {
// ...
public static void main(String[] var0) {
// Implementation ...
}
}
Setting up Non-Static Hello
Back in our Clojure core.clj file, we’ll need to add a new method to export
in our Java class and update :gen-class
with a :methods
option.
(ns hello-world.core
(:gen-class
:methods [[hello [String] void]]))
; ...
(defn hello [subject]
(println (str "Hello, " subject "!")))
(defn -hello [_ subject] (hello subject))
; ...
Now what does that :methods
setting mean? :methods
is an array of methods
that are to be exported from the module upon compilation. Here, we’re exporting
a method named hello
that accepts a Java String
and returns void
.
Because we named our method hello
in :methods
, we need a corresponding
Clojure function that starts with a dash: -hello
.
While it may seem redundant to define the function twice, this is necessary in order for the function to be added to the jar.
You may have noticed that -hello
takes two parameters, where the first is
ignored. Why is that? Let’s look at the core.class file.
package hello_world;
// ...
public class core {
private static final Var hello__var = Var.internPrivate("hello-world.core", "-hello");
// ...
public void hello(String var1) {
Var var10000 = hello__var;
Object var2 = var10000.isBound() ? var10000.get() : null;
if (var2 != null) {
((IFn)var2).invoke(this, var1);
} else {
throw new UnsupportedOperationException("hello (hello-world.core/-hello not defined?)");
}
}
// ...
}
In the autogenerated class, we have a reference to the Clojure -hello
function
and ((IFn)var2).invoke(this, var1);
within the Java hello
method for that function.
Essentially what this is doing is executing the -hello
function with
a this
parameter as the first parameter and the and the actual parameter
that was passed to the hello
Java function will come after that.
This is very similar to how the first parameter would work in a deftype
function, referencing the object.
Setting up Static Hello
There are only a couple differences between compiling a static and non-static Clojure function. Here’s how you might set up a static hello function:
(ns hello-world.core
(:gen-class
:methods [[hello [String] void]
^{:static true} [static_hello [String] void]]))
(defn hello [subject]
(println (str "Hello, " subject "!")))
(def -static_hello hello)
; ...
Since we already have a hello function declared, we must name the static
version something different: static_hello
. In our :methods
declaration,
we indicate that the function is static with ^{:static true}
.
When we create our -static_hello
function, we can just set it to our
existing hello
function. This means that -static_hello
only has one
parameter, as opposed to our -hello
function. This is because static methods
don’t require an object reference parameter.
If we look back at core.class, we can see exactly how this function is declared
as well as the call to invoke
, which only takes one parameter (without the this
reference).
package hello_world;
// ...
public class core {
// ...
public static void static_hello(String var0) {
Var var10000 = static_hello__var;
Object var1 = var10000.isBound() ? var10000.get() : null;
if (var1 != null) {
((IFn)var1).invoke(var0);
} else {
throw new UnsupportedOperationException("static_hello (hello-world.core/-static_hello not defined?)");
}
}
// ...
}
Now we’re ready to compile. Running lein uberjar
should create a jar
file with main
, hello
, and static_hello
functions!