Aug 21 2009
Snippet: Automatic Proxy Creation in Clojure
The proxy function makes it easy for Clojure to interface with the Java layer, but I was dealing with an interface (the AIM Java API) that had an punitive number of things that needed to be overridden…
public void OnIdleStateChange(AccSession arg0, int arg1) {
}
public void OnInstanceChange(AccSession arg0, AccInstance arg1, AccInstance arg2, AccInstanceProp arg3) {
}
public void OnLookupUsersResult(AccSession arg0, String[] arg1, int arg2, AccResult arg3, AccUser[] arg4) {
}
public void OnSearchDirectoryResult(AccSession arg0, int arg1, AccResult arg2, AccDirEntry arg3) {
}
// ... go on like this for pages
The Java code is here, if you’re interested in the entire set of calls. Now, I didn’t care about most of those events, but I had to override them, since they didn’t have a default implementation. What made this seem painful was that I was really only interested in two of the callbacks. So I started to record an Emacs macro to convert the Java code to the equivalent Clojure proxy statement, and then I realized that I didn’t have to — I was using a Lisp.
(defmacro auto-proxy [interfaces variables & args]
(let [defined (set (map #(str (first %)) args))
names (fn [i] (map #(.getName %) (.getMethods i)))
all-names (into #{} (apply concat (map names (map resolve interfaces))))
undefined (difference all-names defined)
auto-gen (map (fn [x] `(~(symbol x) [& ~'args])) undefined)]
`(proxy ~interfaces ~variables ~@args ~@auto-gen)))
Auto-proxy works just like proxy, but it makes an empty implementation for any call that wasn’t defined. So, suddenly, what would have been a bunch of lines collapsed into just:
(defn create-aim-proxy []
(auto-proxy [com.aol.acc.AccEvents] []
(OnImReceived [session imSession participant im]
(handle-im session imSession participant im))
(OnStateChange [arg0 arg1 arg2]
(handle-state-change arg0 arg1 arg2))))
Macros ftw. The nice thing about Clojure/Lisp is that it makes coding up this kind of reusable framework stuff really easy.
Why do you need to do this, doesn’t the proxy form already do this for you?
“If a method fn is not provided for an interface method, an UnsupportedOperationException will be thrown should it be called.”
http://clojure.org/java_interop#toc25
@Duncan: I didn’t want it to throw an exception, I wanted it to just have an empty default implementation. Since it was being invoked as a callback from the API, I didn’t even have an intervening level in which I could put a try statement.
Tim, this is a very useful macro for me and I bet for plenty of others that want to migrate from Java to Clojure. Have you submitted it to Rich for inclusion in clojure-contrib or core?
@Jetpack: Yah, I’d be interested in that, but last time I looked it wasn’t obvious how to go about submitting stuff. I’ll definitely take a look.
Tim, I’m trying to extend a class as well as implement an interface in the same proxy. Would you have an idea how I could do this with your auto-proxy macro? Simply adding the extended class to the auto-proxy arguments gives me an excpetion: “java.lang.IncompatibleClassChangeError: Implementing class”.
Got the solution from #clojure: the extended class name needs to be put at the front of the interfaces argument of the macro. works like a charm.
Again, in my opinion this macro would be a very useful addition to clojure-contrib or core.