My approach to learning new languages is to find an interesting project to extend or build for. This gives me something tangible to work with and helps me stay interested. So, for Clojure I decided to try to write Bukkit plugins. Bukkit is an open-source Minecraft server, written in Java. Being my first Clojure project, I figured that since Clojure is a JVM language and Bukkit is also on the JVM, that it would be really straight forward to just whip up a simple plugin. This probably would have been the case if this weren’t my first project with Clojure. But, since it was first my project, there was actually quite a bit of learning to do before my Clojure plugin would load up into Bukkit and actually do something.
My strategy was to write some Java classes in Clojure and compile them into a plugin jar file. My only option here was to use Clojure’s gen-class
macro, because these classes will need to be named, conform to interfaces, and inherit from sub-classes. There are other ways to create Java classes in Clojure, but gen-class
is the only one that provides all of these features. But, I was surprised to find that these classes of mine were not being created in the jar file when I would run lein jar
. This is because gen-class
requires Ahead of Time Compiling, which goes a step further and turns your Clojure code into Java byte code. So, AOT? What the what?
Luckily, it’s totally possible, and very easy, to use Leiningen to compile my Clojure code into a jar that contains byte code. In regards to Leiningen, this process is called Ahead of Time Compiling. Using AOT in your project’s project.clj file instructs Leiningen and the underlying compiling magic to take it a step farther and produce the byte code for you. In the case of my Bukkit plugin, I needed every namespace to be compiled using AOT, and I achieved this using the following project.clj.
(defproject com.rycole.bukkit.simple "0.1.0-SNAPSHOT"
:repositories [["bukkit.release" "http://repo.bukkit.org/content/repositories/releases"]]
:dependencies [[org.clojure/clojure "1.4.0"]
[org.bukkit/bukkit "1.4.7-R0.1"]]
:filespecs [{:type :path :path "src\\plugin.yml"}]
:aot :all)
The :aot :all
keywords mean that it will apply to all of the namespaces in my project. Now, I could safely move forward knowing that my code will be usable by Bukkit. From where, it’s just a matter of maximizing my usage of Clojure’s Java-interop functionality, and porting the Java examples, from Bukkit’s documentation, over to Clojure.
With all of problem #1 in mind, I started a new Clojure project for my plugin, using Leiningen, by doing lein new com.rycole.bukkit.simple.core
.
Part of why I’m using AOT is because to extend Bukkit’s JavaPlugin
class, which is the basis of a Bukkit plugin, I used Clojure’s gen-class
function. So, the definition of my com.rycole.bukkit.simple.core
namespace looks like this.
(ns com.rycole.bukkit.simple.core
(:require com.rycole.bukkit.simple.listeners)
(:import [com.rycole.bukkit.simple.listeners PlayerLoginListener])
(:gen-class :name com.rycole.bukkit.simple.core.Main
:extends org.bukkit.plugin.java.JavaPlugin))
gen-class
creates a Main
class that extends JavaPlugin
. This occurs at compile-time, into the jar file, and is usable from Java-land. The only remaining thing that this Main
class needs to do is provide event handlers for the plugin enabled and disabled events. Adding functions to our Main
class couldn’t be easier.
(defn -onEnable [this]
(.info (.getLogger this) "PLUGIN ENABLED")
(.registerEvents (.getPluginManager (.getServer this)) (com.rycole.bukkit.simple.listeners.PlayerLoginListener.) this))
(defn -onDisable [this]
(.info (.getLogger this) "PLUGIN DISABLED"))
Now, with what we have here, this plugin can be compiled and loaded up by Bukkit. A plugin.yml
file is required to be in the output jar, and can be easily added in the project.clj
file. Try it out. You should see Bukkit say that it has loaded and enabled your plugin.
Once I had the pluging in this state, I was happy but I wasn’t satisfied. I wanted the pluging to actually do something … anything! So, the easiest thing that I could see to do was to handle player login events. When a player logs in to the server, I wanted my plugin to simply log a message to the Bukkit console.
For a Bukkit plugin to handle events, it has to create Listener
classes that implement functions that accept the appropriate event parameters. Now, I needed to create this class and implement these necessary member functions. Again, gen-class
is the plan of attack, but this time it needs to specify a Java function annotation as well as some hints as to what the variable types of the function arguments are. Once you’ve seen this once, it’s pretty simple.
(ns com.rycole.bukkit.simple.listeners
(:gen-class :name com.rycole.bukkit.simple.listeners.PlayerLoginListener
:implements [org.bukkit.event.Listener]
:methods [[^{org.bukkit.event.EventHandler true} onPlayerLoggedIn [org.bukkit.event.player.PlayerLoginEvent] void]]))
I made a new listeners.clj
file, in which I an generating this class. This time, this class :implements
instead of :extends
. Also, the methods for which I needed to provide some meta data about are explicitly listed in the :methods
parameter. In this case, I was only interested in implementing a single handler function for the PlayerLoginEvent
.
[^{org.bukkit.event.EventHandler true} onPlayerLoggedIn [org.bukkit.event.player.PlayerLoginEvent] void]
The first item in this sequence is the annotation. This annotation is equivalent to @EventHandler
in Java code. The true
just means that it’s desired. Second, the onPlayerLoggedIn
specifies the name of the function. Third, the following sequence specifies the data type of the parameters. This function only has one parameter. Last, the function’s return type is specified as void
.
Now that the function has been all meta-data’ed up, the only thing left to do is to actually implement it.
(defn -onPlayerLoggedIn [this evnt]
(.info (org.bukkit.Bukkit/getLogger) "PLAYER LOGGED IN"))
I don’t actually do anything with the function parameters. I just simply log a message to Bukkit’s console.
With all of this in place, just :require
and :import
your listeners from inside core.clj
and register them so that Bukkit knows about them. Compile the jar using Leiningen by doing lein jar
. Place the jar file into Bukkit’s plugins directory and run Bukkit in such a way that allows you to put Clojure and these plugins on the class path. A Windows batch file for doing this could look like this.
java -cp C:\Users\Ryan\.m2\repository\org\clojure\clojure\1.4.0\clojure-1.4.0.jar;E:\Games\Bukkit\plugins\*;craftbukkit-1.4.7-R0.1.jar org.bukkit.craftbukkit.Main
PAUSE
With any luck, you’re now writing Bukkit plugins in Clojure without any elaborate hacks. The code for my plugin that I talked about in this write up is available on github.
— Ryan Cole