Share your thoughts in the 2024 State of Clojure Survey!

Welcome! Please see the About page for a little more info on how this works.

+1 vote
in Compiler by

I'm working on a project using Clojure and RoboVM. We use AOT compilation to compile Clojure to JVM classes, and then use RoboVM to compile the JVM classes to native code. In our Clojure code, we call Java APIs provided by RoboVM, which wrap the native iOS APIs.

But we've found an issue with inheritance and class-level static initialization code. Many iOS APIs require inheriting from a base object and then overriding certain methods. Currently, Clojure runs a superclass's static initialization code at compile time, whether using ":gen-class" or "proxy" to create the subclass. However, RoboVM's base "ObjCObject" class (link: 1), which most iOS-specific classes inherit from, requires the iOS runtime to initialize, and throws an error at compile time since the code isn't running on a device.

CLJ-1315 addressed a similar issue by modifying "import" to load classes without running static initialization code. I've written my own patch which extends this behavior to work in ":gen-class" and "proxy" as well. The unit tests pass, and we're using this code successfully in our iOS app.

Patch: clj-1743-2.patch

Here's some sample code that can be used to demonstrate the current behavior (Full demo project at (link: https://github.com/figly/clojure-static-initialization)):

`
package clojure_static_initialization;

public class Demo {
static {

System.out.println("Running static initializers!");

}
public Demo () {
}
}
`

(ns clojure-static-initialization.gen-class-demo (:gen-class :extends clojure_static_initialization.Demo))

`
(ns clojure-static-initialization.proxy-demo)

(defn make-proxy []
(proxy [clojure_static_initialization.Demo] []))
`

(link: 1) https://github.com/robovm/robovm/blob/master/objc/src/main/java/org/robovm/objc/ObjCObject.java

14 Answers

0 votes
by

Comment made by: alexmiller

No changes from previous, just updated to apply to master as of 1.7.0-RC2.

0 votes
by

Comment made by: alexmiller

If you had a sketch to test this with proxy and gen-class, that would be helpful.

0 votes
by

Comment made by: abram

Sure, what form would you like for the sketch code? A small standalone project? Unit tests?

0 votes
by

Comment made by: alexmiller

Just a few lines of Java (a class with static initializer that printed) and Clojure code (for gen-class and proxy extending it) here in the test description that could be used to demonstrate the problem. Should not have any dependency on iOS or other external dependencies.

0 votes
by

Comment made by: abram

Sample code added, let me know if I can add anything else!

0 votes
by

Comment made by: abram

Just out of curiosity, what are the odds this could make it into 1.8?

0 votes
by

Comment made by: alexmiller

unknown.

0 votes
by

Comment made by: notespin

I'm affected by this bug too. A function in a namespace calls a static Java variable which is initialized in place. Another namespace which is genclassed calls that function. Now at compile time, the static java is initialized and it makes building fail, because that static java initialization needs resources which don't exist on the build machine.

0 votes
by

Comment made by: michaelblume

Refreshing patch so it applies to master, no changes, keeping attribution.

0 votes
by

Comment made by: alexmiller

I am confused by the patch making changes in RT.loadClassForName() but the changes in Compiler are calls to RT.classForNameNonLoading()? Is this patch drift or what's up?

0 votes
by

Comment made by: sonicsmooth

Thank for you posting this patch. The issue with static initializers has been making it difficult to do JavaFX development with both AOT and interactive development. I cloned the Clojure 1.9.0-master source today and applied the patch, but the example Clojure project still shows "Running static initializers!" I verified this is the case with an actual use case of mine. The error goes away if I start a JFXPanel first. Is there a workaround as of Sept. 2017, eg another way of defining a proxy or deferring until runtime? Thank you.

$ lein clean;lein repl
Compiling 1 source files to C:\dev\clojure\clojure-static-initialization\target\classes
Compiling clojure-static-initialization.gen-class-demo
Compiling clojure-static-initialization.proxy-demo
Running static initializers!
Clojure 1.9.0-master-SNAPSHOT

user=> (def lcp (proxy (link: javafx.scene.control.ListCell) (link: )))

CompilerException java.lang.ExceptionInInitializerError, compiling:(C:\dev\clojure\clojure-static-initialization\target\f31ee90298a1be447b450330204c3c0806c08b96-init.clj:1:10)

$ lein clean;lein repl
Compiling 1 source files to C:\dev\clojure\clojure-static-initialization\target\classes
Compiling clojure-static-initialization.gen-class-demo
Compiling clojure-static-initialization.proxy-demo
Running static initializers!
Clojure 1.9.0-master-SNAPSHOT

user=> (def jfxpanel (javafx.embed.swing.JFXPanel.))

'user/jfxpanel

user=> (def lcp (proxy (link: javafx.scene.control.ListCell) (link: )))

'user/lcp

0 votes
by

Comment made by: terje@andante.no

I am not so sure that the fix is simply to a matter of swapping calling {{classForNameNonLoading}} a couple of places. {{proxy}} does some pretty sophisticated introspection and class analysis, which touches the static code in many places.

An alternative solution would be to have a proxy function - one which does the same as {{proxy}} but at runtime.
I currently have a workaround which works for the problem of JavaFX's ListCell ...

0 votes
by

Comment made by: terje@andante.no

The workaround: Delaying a macro evaluation from compile-time to run-time.
In this case, I am assuming that you have wrapped your proxy-call in a function and that it would be safe to do it at run-time (because you have init-ed your JavaFX or whatever):
1. Use a "back-tick" to prevent your macro from evaluation at compile-time.
2. Wrap your back-ticked code in an eval:

(defn make-thing [] (eval `(proxy ...

  1. Local bindings and function args need to be "gensym-ed" 1. .
  2. Implisit {{this}} needs to be accessed as ~'this

`
(defn make-thing []
(eval
`(proxy [ListCell][]

 (updateItem [item# empty?#]
   (proxy-super updateItem item# empty?#)            
     (.setText ~'this nil)
     ...

`

  1. args passed to the function need to be dynamically bound outside the eval, and perhaps rebound in a let inside the back-ticked code for accessing on seperate thread:

`
(def ^:dynamic an-arg nil)

(defn make-thing [an-arg]
(binding [*an-arg* an-arg]

(eval
`(let [an-arg# *an-arg*]
  (proxy [ListCell][]
    (updateItem [item# empty?#]
      (proxy-super updateItem item# empty?#)            
       (.setText ~'this nil)
       (println "an-arg:" an-arg#)
       ...

`

Could this be done with a macro instead? E.g. {{proxyfn}}

0 votes
by
Reference: https://clojure.atlassian.net/browse/CLJ-1743 (reported by abram)
...