The Weird Java Interop issues thread sent me down a bit of a rabbit hole, figuring out if it would be possible to automatically generate Clojure wrappers for Java classes, which include all possible type information, so you never have to type hint again.
Like I said I got a bit sucked in, the result I present: defwrapper
. It’s likely this will become a library, but I first want to use it a bit more myself to get a feel for it. But I also wanted to share it to see what people think.
The idea is simple. Say I want to call methods from the java Synthesizer interface, then I do this,
(defwrapper javax.midi.Synthesizer "synth-")
Then this generates (among others) functions like
(defn synth-get-default-soundbank
{:arglists '([javax.sound.midi.Synthesizer])}
(^javax.sound.midi.Soundbank [^javax.sound.midi.Synthesizer this13886]
(.getDefaultSoundbank this13886)))
It will handle multiple arities, static methods, method overloads.
And the result (try it, it’s fun )
(defwrapper javax.sound.midi.MidiSystem "midi-sys-")
(defwrapper javax.sound.midi.Synthesizer "synth-")
(defwrapper javax.sound.midi.MidiChannel "chan-")
;;; Play some tunes
(def synth (midi-sys-get-synthesizer))
(synth-open synth)
(def chan (first (synth-get-channels synth)))
;; play some chords
(do
(chan-note-on chan 60 600)
(chan-note-on chan 64 600)
(chan-note-on chan 67 600)
(Thread/sleep 1100)
(chan-note-on chan 60 600)
(chan-note-on chan 64 600)
(chan-note-on chan 67 600)
(chan-note-on chan 71 600))
So is this a good idea? I’m still not sure. It was definitely more challenging than I thought it would be, and a good refresher on the finer points of type hints. (and metadata in macros, oh my). Primitives are a pain, as Clojure only allows hinting long
and double
, and only in some places (in others you need to cast, (long ...)
). There’s no void
(I used Object
instead). There are some cases that are hard to handle, e.g. what if two method with the same arity but different argument types have different return types? (hopefully that’s not too common).
So the result is still pretty rough, it uses boxed primitives in lieue of the actual primitive types in some cases, which works but might introduce extra boxing/unboxing. Varargs still requires creating a Java array and passing that in. Constructors aren’t wrapped yet.
But… I also think it’s pretty cool. It causes issues like the one in the “Weird Java Interop” thread to disappear, I think it can take away some of the sharp edges of dealing with Java interop. Does everyone really need to know that sometimes you need to type hint to the public interface in order for things to work? Does anyone enjoy hunting down reflection warnings because someone wants to use their library on Graal?
I’ll be playing with this a lot more I think. Even when not using it (yet) in production code I would 100% use it today when I quickly want to use a Java library from the REPL.
Some more examples of the generated code:
(defn channel-note-off
{:arglists '([javax.sound.midi.MidiChannel int int]
[javax.sound.midi.MidiChannel int])}
(^java.lang.Object [^javax.sound.midi.MidiChannel this13946 ^java.lang.Integer int13947 ^java.lang.Integer int13948]
(.noteOff this13946 int13947 int13948))
(^java.lang.Object [^javax.sound.midi.MidiChannel this13949 ^java.lang.Integer int13950]
(.noteOff this13949 int13950)))
(defn midi-sys-get-midi-file-format
{:arglists '([javax.sound.midi.MidiSystem java.io.InputStream]
[javax.sound.midi.MidiSystem java.io.File]
[javax.sound.midi.MidiSystem java.net.URL])}
(^javax.sound.midi.MidiFileFormat [G__13976]
(cond
(and (instance? java.io.InputStream G__13976))
(let [G__13976 ^java.io.InputStream G__13976]
(javax.sound.midi.MidiSystem/getMidiFileFormat G__13976))
(and (instance? java.io.File G__13976))
(let [G__13976 ^java.io.File G__13976]
(javax.sound.midi.MidiSystem/getMidiFileFormat G__13976))
(and (instance? java.net.URL G__13976))
(let [G__13976 ^java.net.URL G__13976]
(javax.sound.midi.MidiSystem/getMidiFileFormat G__13976)))))
And the current state of things, do try it out and report back!