Clojure

Avoid compile-time static initialization of classes when using inheritance

Details

  • Type: Enhancement Enhancement
  • Status: Open Open
  • Priority: Critical Critical
  • Resolution: Unresolved
  • Affects Version/s: Release 1.6, Release 1.7
  • Fix Version/s: None
  • Component/s: None
  • Labels:
  • Patch:
    Code
  • Approval:
    Triaged

Description

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 [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 https://github.com/figly/clojure-static-initialization):

Demo.java
package clojure_static_initialization;

public class Demo {
  static {
    System.out.println("Running static initializers!");
  }
  public Demo () {
  }
}
gen_class_demo.clj
(ns clojure-static-initialization.gen-class-demo
  (:gen-class :extends clojure_static_initialization.Demo))
proxy_demo.clj
(ns clojure-static-initialization.proxy-demo)

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

[1] https://github.com/robovm/robovm/blob/master/objc/src/main/java/org/robovm/objc/ObjCObject.java

Activity

Hide
Alex Miller added a comment -

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

Show
Alex Miller added a comment - No changes from previous, just updated to apply to master as of 1.7.0-RC2.
Hide
Alex Miller added a comment -

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

Show
Alex Miller added a comment - If you had a sketch to test this with proxy and gen-class, that would be helpful.
Hide
Abe Fettig added a comment -

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

Show
Abe Fettig added a comment - Sure, what form would you like for the sketch code? A small standalone project? Unit tests?
Hide
Alex Miller added a comment -

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.

Show
Alex Miller added a comment - 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.
Hide
Abe Fettig added a comment -

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

Show
Abe Fettig added a comment - Sample code added, let me know if I can add anything else!
Hide
Abe Fettig added a comment -

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

Show
Abe Fettig added a comment - Just out of curiosity, what are the odds this could make it into 1.8?
Hide
Alex Miller added a comment -

unknown.

Show
Alex Miller added a comment - unknown.
Hide
Didier A. added a comment - - edited

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.

Show
Didier A. added a comment - - edited 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.
Hide
Michael Blume added a comment -

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

Show
Michael Blume added a comment - Refreshing patch so it applies to master, no changes, keeping attribution.
Hide
Alex Miller added a comment -

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?

Show
Alex Miller added a comment - 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?
Hide
Michael Schwager added a comment -

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 [javafx.scene.control.ListCell] []))

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 [javafx.scene.control.ListCell] []))
#'user/lcp

Show
Michael Schwager added a comment - 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 [javafx.scene.control.ListCell] [])) 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 [javafx.scene.control.ListCell] [])) #'user/lcp
Hide
Terje Dahl added a comment -

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 ...

Show
Terje Dahl added a comment - 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 ...
Hide
Terje Dahl added a comment -

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 ...

3. Local bindings and function args need to be "gensym-ed" ...#.
4. 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)
         ...

5. 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

Show
Terje Dahl added a comment - 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 ...
3. Local bindings and function args need to be "gensym-ed" ...#. 4. 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)
         ...
5. 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

People

Vote (8)
Watch (6)

Dates

  • Created:
    Updated: