Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure can't import JavaFX classes with static initializers

I'm playing around with Clojure (1.6) and JavaFX 8, and right at the start I've run into a problem. For example, this very simple code fails:

(ns xxyyzz.core)

(gen-class :name "xxyyzz.core.App"
           :extends javafx.application.Application
           :prefix "app-")

(defn app-start [app stage]
  (let [button (javafx.scene.control.Button.)]))

(defn launch []
  (javafx.application.Application/launch xxyyzz.core.App (into-array String [])))

(defn -main []
  (launch))

This is the last part of the stack trace that seems relevant:

Caused by: java.lang.ExceptionInInitializerError
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:340)
        at clojure.lang.RT.classForName(RT.java:2070)
        at clojure.lang.Compiler$HostExpr.maybeClass(Compiler.java:969)
        at clojure.lang.Compiler$HostExpr.access$400(Compiler.java:747)
        at clojure.lang.Compiler$NewExpr$Parser.parse(Compiler.java:2494)
        at clojure.lang.Compiler.analyzeSeq(Compiler.java:6560)
        ... 48 more
Caused by: java.lang.IllegalStateException: Toolkit not initialized
        at com.sun.javafx.application.PlatformImpl.runLater(PlatformImpl.java:276)
        at com.sun.javafx.application.PlatformImpl.runLater(PlatformImpl.java:271)
        at com.sun.javafx.application.PlatformImpl.setPlatformUserAgentStylesheet(PlatformImpl.java:562)
        at com.sun.javafx.application.PlatformImpl.setDefaultPlatformUserAgentStylesheet(PlatformImpl.java:524)
        at javafx.scene.control.Control.<clinit>(Control.java:81)
        ... 55 more

I don't speak Java at all, but researching this it seems that the problem lies with Clojure and the way it imports Java classes. If I understand correctly, at import time it runs the class static initializer, and for some JavaFX classes (Button in my case) that crashes.

Guess I have two questions: is my understanding of this error correct? And second, is there a way to work around this problem somehow? I've tried pulling the imports inside functions instead at the (ns) declaration, but it still doesn't work.

If there's no Clojure fix, could this be possibly fixed with some additional Java code?

Any tips and pointers are welcome!

like image 401
klozovin Avatar asked Apr 29 '14 13:04

klozovin


1 Answers

I couldn't find a way to alter Clojure's import behavior, but I did find a couple of hacks to do what I need.

First, JavaFX provides builder classes, so the cleanest way in this particular case would be to use ButtonBuilder to create new Buttons.

Second way would be to write a simple Java class that wraps the Button, and then from Clojure's side import that wrapping class. It's an OK solution when working with smaller number of problematic classes.

Third way would be to import at runtime, something like this (thanks to guys at #clojure for helping out with this):

(defn import-at-runtime [name]
  (.importClass (the-ns *ns*)
                (clojure.lang.RT/classForName name)))

(import-at-runtime "javafx.scene.control.Button")

(let [button (eval `(new ~(symbol "javafx.scene.control.Button") ~"Button Text"))

In the end, this seems like a ugly wart in Clojure's Java interop, it would be great if it could be fixed in the future.


UPDATE: There's also clojure.lang.RT/classForNameNonLoading, but unfortunately, it's not public as of Clojure 1.6. It's easy to re-implement it in Clojure, though:

(fn [^String class-name]
  (Class/forName class-name false (clojure.lang.RT/baseLoader)))

Later, the class can be instantiated with clojure.lang.Reflector/invokeConstructor.

like image 173
klozovin Avatar answered Nov 15 '22 09:11

klozovin