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.
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
gen_class_demo.clj
proxy_demo.clj
[1] https://github.com/robovm/robovm/blob/master/objc/src/main/java/org/robovm/objc/ObjCObject.java
Refreshing patch so it applies to master, no changes, keeping attribution.
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?
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 [javafx.scene.control.ListCell] []))
CompilerException java.lang.ExceptionInInitializerError, compilingC:\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
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 ...
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:
3. Local bindings and function args need to be "gensym-ed" ...#.
4. Implisit this needs to be accessed as ~'this
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:
Could this be done with a macro instead? E.g. proxyfn