Brool brool (n.) : a low roar; a deep murmur or humming

Snippet: Automatic Proxy Creation in Clojure

 |  snippet macro clojure coding proxy

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.

Discussion

Comments are moderated whenever I remember that I have a blog.

Duncan | 2009-08-23 16:14:51
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
Reply
tim | 2009-08-23 20:10:38
@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.
Reply
Jetpack | 2010-01-04 15:14:02
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?
Reply
tim | 2010-01-15 16:28:27
@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.
Reply
Jetpack | 2010-03-09 13:11:15
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".
Reply
Jetpack | 2010-03-09 13:24:55
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.
Reply
Add a comment