I've been writing ae/eventstore.clj, a Clojure wrapper for the EventStoreDB-Client-Java library. Nicely written, it is still >5k lines of your typical verbose Java and I'm hoping I can do more in <1k lines. In writing this I have stumbled upon an 8-year-old Clojure bug, which itself might stem from a JVM bug from the last millennium.
Finding a Clojure–Java interop bug
I loath private, protected, and their ilk, much preferring to make everything public 99% of the time. Power to the consumer! Here's an (abbreviated) example from the Java library. A protected base class which provided the subclass with a public method.
| |
In Java, this is perfectly legal. In Clojure:
| |
| |
I get exceptions like this all the time working with Clojure–Java interop, either I've:
- called the right method on the wrong class
- called the wrong method on the right class
- called an inaccessible method
Let's find out which methods ReadAllOptions provides:
| |
Look at #19, it has a public method called timeouts, and after checking (.getClass timeout) we can be sure that this call should work.
It turns out that this is a bug that goes back to at least 2013, when it was first tracked in the Clojure Jira (#CLJ–1243) and I suspect it might be related to a 22-year-old JVM bug (#JDK–4283544).
Well, what to do? I don't think there's much use in waiting for this to get fixed. Let's do as hackers do and hack. Given that I'm fairly certain that what I want to do is legal in Java, let's look at the Java interop options of which there are three levels:
- class / member accesses and the dot special form
proxy,gen-class,reify- writing Java
Writing Java
In this rare case, it looks like writing Java is ones only option. But I, like many other Clojure developers, was once a Java developer, and the Java needed is very simple, it is a wrapper for ReadAllOptions, so the offending methods will be called inside our Java code.
| |
I've used a builder pattern here. This is good to call using Clojure's cond-> threading macro. It works by evaluating a predicate, and treating the next as if in a normal thread-first macro. In the following code this makes sure that methods are only called if the argument is non-nil, the builder's default value is used otherwise.
| |
As always when calling Java from Clojure, one must import the class:
| |
But before it can be used, the Java needs to be compiled.
Compiling Java with tools.build
Building this in Leiningen should be really simple. Lein's defproject has the key :java-source-paths which can be compiled using lein javac, but this is usually unnecessary as all the usual Lein tasks will do it for you.
I made the switch to deps.edn last year, unfortunately it won't do compile Java code automatically. But this does provide one with an opportunity to learn how to use tools.build, written by Alex Miller (@puredanger). Asserting that builds are programs, we must do for ourselves what Lein would be doing.
To deps.edn one must add an alias for the task to be performed, add any dependencies needed for that task, and the namespace containing the functions to use. Our tasks will be build, our only dependency tools.build, and our namespace build.
| |
One then creates build.clj in the project's root where we will start defining our build task:
| |
The most important subtask will be compiling our Java code, for which tools.build provides the javac function.
| |
I assume here that the Java code is within java but it might be possible to keep it as a subdirectory or src as one would with clj, cljc, and cljs.
Running clojure -T:build compile will compile the Java source files as can be seen by looking in target/classes. Once target/classes is added to the classpath you will be ready to call the class from Clojure.
| |
Trying gen-class?
In attempting to use gen-class to circumvent this bug, I found an even older Clojure bug! Using the builder pattern, the builder's methods return itself. This allows one to use method chaining in Java, or threading macros in Clojure.
| |
| |
One would write the method like so, so the function makes the change to the internal state, and then returns itself.
| |
If it only were so. Ticket #CLJ-84, compile gen-class fail when class returns self, makes it clear that it is not possible for a gen-class method to return an instance of the same class. Created on 17th June 2009 by Rick Hickey, this ticket is as old as Clojure's ticketing system itself. What happens if we return void instead?
| |
Given that the methods are returning void, we have to use an abomination like this with an implicit do and explicit altering of state:
| |
And AOT compile to make use of gen-class by adding a new subtask to build.clj:
| |
And the best thing? It doesn't even work.
| |
Other uses
I can't imagine that such a setup will be useful too often, but here are a couple that come to mind:
- Java is more performant (~2×?)
- an existing codebase migrating to Clojure (à la strangler architecture)
- providing a reliable Java API
- using tools that require Java magic