Avoid compile-time static initialization of classes when using inheritance

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.

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

Environment

None

Activity

Show:
Michael Blume
March 14, 2017, 4:00 AM

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

Alex Miller
June 27, 2017, 11:22 PM

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?

import
September 25, 2017, 11:46 PM

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

Terje Dahl
September 7, 2018, 8:45 AM

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

Terje Dahl
September 7, 2018, 1:10 PM

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

Assignee

Unassigned

Reporter

Abe Fettig

Labels

Approval

Triaged

Patch

Code

Affects versions

Priority

Critical