Java calling Clojure

Posted by Uncle Bob on 08/07/2009

While I think Clojure is an interesting language, in order for it to be of real practical use, I must be able to use it in conjunction with other systems I am working on. Specifically, I’d like to write some FitNesse tools in Clojure; but for that to work, I’ll need to call into my Clojure code from Java.

Today, my son Justin and I managed to do just that by following the instruction in Stuart Halloway’s book, the Clojure api website, and Mark Volkmann’s very useful site.

Be advised, that it takes a bit of fiddling to get this to work. You will have to jockey around with your classpaths and output directories. But it’s not actually that hard to do, and the results can be very rewarding.

We implemented the Bowling game (again), but this time we wrote the unit tests in Java, and had them call into Clojure. From the point of view of the Java tests, it looked just like we were calling into java code. The tests had no idea that this was Clojure.

Here are the tests:

package bowling;  import static org.junit.Assert.assertEquals; import org.junit.Before; import org.junit.Test;  public class BowlingTest {   private Game g;    @Before   public void setup() {     g = new Game();   }    @Test   public void roll() throws Exception {     g.roll(0);   }    @Test   public void gutterGame() throws Exception {     rollMany(20, 0);     assertEquals(0, g.score());   }    private void rollMany(int n, int pins) {     for (int i=0; i< n; i++) {       g.roll(pins);     }   }    @Test   public void allOnes() throws Exception {     rollMany(20, 1);     assertEquals(20, g.score());       }    @Test   public void oneSpare() throws Exception {     g.roll(5);     g.roll(5);     g.roll(3);     rollMany(17, 0);     assertEquals(16, g.score());   }    @Test   public void oneStrike() throws Exception {     g.roll(10);     g.roll(5);     rollMany(17,0);     assertEquals(20, g.score());   }    @Test   public void perfectGame() throws Exception {     rollMany(12, 10);     assertEquals(300, g.score());   }    @Test   public void allSpares() throws Exception {     rollMany(21, 5);     assertEquals(150, g.score());   } }

The clojure code to make them pass looks like this:

(ns bowling.Game   (:gen-class     :state state     :init init     :methods [     [roll [int] void]     [score [] int]     ]))  (defn -init [] [[] (atom [])])  (defn -roll [this pins]   (reset! (.state this) (conj @(.state this) pins)))  (declare score-frames)  (defn -score [this]   (reduce + (take 10 (score-frames [] (conj @(.state this) 0)))))  (defn score-frames [scores [first second third :as rolls]]   (cond     (or (empty? rolls) (nil? second) (nil? third)) scores     (= 10 first) (recur (conj scores (+ 10 second third)) (next rolls))     (= 10 (+ first second)) (recur (conj scores (+ 10 third)) (nnext rolls))     :else     (recur (conj scores (+ first second)) (nnext rolls))))

The magic is all up in that (ns bowling.game… block.

When a method is called on the instance, Clojure automatically invokes a function of the same name, but prefixed by a -, so -roll and -score are the implementations of the roll and score methods. Note that each of these functions take a this argument. You can get the state out of this by saying (.state this).

From a source code point of view, that’s about all there is to it. But how do you compile this into a java .class file?

Here’s what we did. We created a new file named compile.clj that looks like this:

(set! *compile-path* "../../classes") (compile 'bowling.Game)

We can run this file from our IDE (IntelliJ using the LAClojure plugin) and it will compile quite nicely. But there are a few things you have to make sure you do.

You will get errors. And the errors aren’t particularly informative. The key to understanding them is to look at the backtrace of the exceptions they throw. Notice, for example, if the function “WriteClassFile” (or something like that) is buried in the backtrace. If so, you are probably having trouble writing your class file.

In the end, we wrote up an ant script that compiled our clojure and our java together. (You have to compile them in the right order! If java calls Clojure, those .class files have to exist before the java will compile!)

The ant script we used was created for us by IntelliJ, and then we modified it to get it to work. I include it here with the caveat that we made it work, but we didn’t make it “right”. You can ignore most of the stuff at the beginning. The interesting part is the Clojure target, and the clean target.

<?xml version="1.0" encoding="UTF-8"?> <project name="bowlingforjunit" default="all">    <property file="bowlingforjunit.properties"/>   <!-- Uncomment the following property if no tests compilation is needed -->   <!--    <property name="skip.tests" value="true"/>    -->    <!-- Compiler options -->    <property name="compiler.debug" value="on"/>   <property name="compiler.generate.no.warnings" value="off"/>   <property name="compiler.args" value=""/>   <property name="compiler.max.memory" value="128m"/>   <patternset id="ignored.files">     <exclude name="**/CVS/**"/>     <exclude name="**/SCCS/**"/>     <exclude name="**/RCS/**"/>     <exclude name="**/rcs/**"/>     <exclude name="**/.DS_Store/**"/>     <exclude name="**/.svn/**"/>     <exclude name="**/.pyc/**"/>     <exclude name="**/.pyo/**"/>     <exclude name="**/*.pyc/**"/>     <exclude name="**/*.pyo/**"/>     <exclude name="**/.git/**"/>     <exclude name="**/.sbas/**"/>     <exclude name="**/.IJI.*/**"/>     <exclude name="**/vssver.scc/**"/>     <exclude name="**/vssver2.scc/**"/>   </patternset>   <patternset id="library.patterns">     <include name="*.zip"/>     <include name="*.war"/>     <include name="*.egg"/>     <include name="*.ear"/>     <include name="*.swc"/>     <include name="*.jar"/>   </patternset>   <patternset id="compiler.resources">     <include name="**/?*.properties"/>     <include name="**/?*.xml"/>     <include name="**/?*.gif"/>     <include name="**/?*.png"/>     <include name="**/?*.jpeg"/>     <include name="**/?*.jpg"/>     <include name="**/?*.html"/>     <include name="**/?*.dtd"/>     <include name="**/?*.tld"/>     <include name="**/?*.ftl"/>   </patternset>    <!-- JDK definitions -->    <property name="jdk.bin.1.6" value="${jdk.home.1.6}/bin"/>   <path id="jdk.classpath.1.6">     <fileset dir="${jdk.home.1.6}">       <include name="lib/deploy.jar"/>       <include name="lib/dt.jar"/>       <include name="lib/javaws.jar"/>       <include name="lib/jce.jar"/>       <include name="lib/management-agent.jar"/>       <include name="lib/plugin.jar"/>       <include name="lib/sa-jdi.jar"/>       <include name="../Classes/charsets.jar"/>       <include name="../Classes/classes.jar"/>       <include name="../Classes/dt.jar"/>       <include name="../Classes/jce.jar"/>       <include name="../Classes/jconsole.jar"/>       <include name="../Classes/jsse.jar"/>       <include name="../Classes/laf.jar"/>       <include name="../Classes/management-agent.jar"/>       <include name="../Classes/ui.jar"/>       <include name="lib/ext/apple_provider.jar"/>       <include name="lib/ext/dnsns.jar"/>       <include name="lib/ext/localedata.jar"/>       <include name="lib/ext/sunjce_provider.jar"/>       <include name="lib/ext/sunpkcs11.jar"/>     </fileset>   </path>    <property name="project.jdk.home" value="${jdk.home.1.6}"/>   <property name="project.jdk.bin" value="${jdk.bin.1.6}"/>   <property name="project.jdk.classpath" value="jdk.classpath.1.6"/>    <!-- Project Libraries -->    <!-- Global Libraries -->    <path id="library.clojure.classpath">     <pathelement location="/Users/unclebob/projects/clojure-build/lib/ant-launcher.jar"/>     <pathelement location="/Users/unclebob/projects/clojure-build/lib/ant.jar"/>     <pathelement location="/Users/unclebob/projects/clojure-build/lib/clojure-contrib.jar"/>     <pathelement location="/Users/unclebob/projects/clojure-build/lib/clojure.jar"/>     <pathelement location="/Users/unclebob/projects/clojure-build/lib/commons-codec-1.3.jar"/>     <pathelement location="/Users/unclebob/projects/clojure-build/lib/commons-fileupload-1.2.1.jar"/>     <pathelement location="/Users/unclebob/projects/clojure-build/lib/commons-io-1.4.jar"/>     <pathelement location="/Users/unclebob/projects/clojure-build/lib/compojure.jar"/>     <pathelement location="/Users/unclebob/projects/clojure-build/lib/hsqldb.jar"/>     <pathelement location="/Users/unclebob/projects/clojure-build/lib/jetty-6.1.14.jar"/>     <pathelement location="/Users/unclebob/projects/clojure-build/lib/jetty-util-6.1.14.jar"/>     <pathelement location="/Users/unclebob/projects/clojure-build/lib/jline-0.9.94.jar"/>     <pathelement location="/Users/unclebob/projects/clojure-build/lib/servlet-api-2.5-6.1.14.jar"/>   </path>    <!-- Modules -->    <!-- Module BowlingForJunit -->    <dirname property="module.bowlingforjunit.basedir" file="${ant.file}"/>    <property name="module.jdk.home.bowlingforjunit" value="${project.jdk.home}"/>   <property name="module.jdk.bin.bowlingforjunit" value="${project.jdk.bin}"/>   <property name="module.jdk.classpath.bowlingforjunit" value="${project.jdk.classpath}"/>    <property name="compiler.args.bowlingforjunit" value="${compiler.args}"/>    <property name="bowlingforjunit.output.dir" value="${module.bowlingforjunit.basedir}/classes"/>   <property name="bowlingforjunit.testoutput.dir" value="${module.bowlingforjunit.basedir}/classes"/>    <path id="bowlingforjunit.module.bootclasspath">     <!-- Paths to be included in compilation bootclasspath -->   </path>    <path id="bowlingforjunit.module.classpath">     <path refid="${module.jdk.classpath.bowlingforjunit}"/>     <pathelement location="/Library/junit4.6/junit-4.6.jar"/>     <path refid="library.clojure.classpath"/>     <pathelement location="${basedir}/classes"/>   </path>    <path id="bowlingforjunit.runtime.module.classpath">     <pathelement location="${bowlingforjunit.output.dir}"/>     <pathelement location="/Library/junit4.6/junit-4.6.jar"/>     <path refid="library.clojure.classpath"/>     <pathelement location="${basedir}/classes"/>   </path>    <patternset id="excluded.from.module.bowlingforjunit">     <patternset refid="ignored.files"/>   </patternset>    <patternset id="excluded.from.compilation.bowlingforjunit">     <patternset refid="excluded.from.module.bowlingforjunit"/>   </patternset>    <path id="bowlingforjunit.module.sourcepath">     <dirset dir="${module.bowlingforjunit.basedir}">       <include name="src"/>     </dirset>   </path>    <target name="compile" depends="clojure, compile.java" description="Compile module BowlingForJunit"/>    <target name="compile.java" description="Compile module BowlingForJunit; production classes">     <mkdir dir="${bowlingforjunit.output.dir}"/>     <javac destdir="${bowlingforjunit.output.dir}" debug="${compiler.debug}" nowarn="${compiler.generate.no.warnings}"             memorymaximumsize="${compiler.max.memory}" fork="true" executable="${module.jdk.bin.bowlingforjunit}/javac">       <compilerarg line="${compiler.args.bowlingforjunit}"/>       <bootclasspath refid="bowlingforjunit.module.bootclasspath"/>       <classpath refid="bowlingforjunit.module.classpath"/>       <src refid="bowlingforjunit.module.sourcepath"/>       <patternset refid="excluded.from.compilation.bowlingforjunit"/>     </javac>      <copy todir="${bowlingforjunit.output.dir}">       <fileset dir="${module.bowlingforjunit.basedir}/src">         <patternset refid="compiler.resources"/>         <type type="file"/>       </fileset>     </copy>   </target>    <target name="clojure">     <java classname="clojure.lang.Compile">       <classpath>         <path location="/Users/unclebob/projects/clojure/BowlingForJunit/classes"/>         <path location="/Users/unclebob/projects/clojure/BowlingForJunit/src"/>                 <path location="/Users/unclebob/projects/clojure/BowlingForJunit/src/bowling"/>         <path location="/Users/unclebob/projects/clojure-build/lib/clojure.jar"/>         <path location="/Users/unclebob/projects/clojure-build/lib/clojure-contrib.jar"/>         </classpath>       <sysproperty key="clojure.compile.path" value="/Users/unclebob/projects/clojure/BowlingForJunit/classes"/>       <sysproperty key="java.awt.headless" value="true"/>       <arg value="bowling.Game"/>     </java>   </target>    <target name="clean" description="cleanup module">     <delete dir="${bowlingforjunit.output.dir}"/>     <delete dir="${bowlingforjunit.testoutput.dir}"/>     <mkdir  dir="${bowlingforjunit.output.dir}"/>   </target>    <target name="all" depends="clean, compile" description="build all"/> </project>

Comments

Leave a response