\large \vspace{10pt} and Other Stories \
\small \vspace{60pt}
by Gary Fredericks
at Clojure/conj 2018
\huge
@gfredericks_
- DRW
- etc.
\huge Trying to understand the relationship between Clojure’s bytecode and the dynamic runtime
\huge
- It’s interesting to know how things work
- It’s probably useful for faster debugging (stack traces might make more sense!)
\huge Yes
- Clojure code can be compiled and loaded in lots of ways
- There’s a parallel in-memory representation of things in the code
- Not even a clear line between compile-time and runtime
\huge
- Lots of things are missing
- Some things are intentional lies
- Some things are accidental lies
\huge
require
require
, but moresocompile
require
aftercompile
:reload
, etc.
\frame{\subsectionpage}
src/my/ns.clj
(ns my.ns
(:require
[my.other-ns :as other]))
(defn assoc-foo
[m]
(assoc m :foo other/foo))
\vspace{10pt}
src/my/other_ns.clj
(ns my.other-ns)
(def foo "foo")
\huge
$ clj
Clojure 1.10.0-RC2
user=>
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-1.pdf}
user=> (require 'my.ns)
(ns my.ns
(:require
[my.other-ns :as other]))
;; mildly edited for readability
(do
(in-ns 'my.ns)
(with-loading-context
(refer 'clojure.core)
(require '[my.other-ns :as other]))
(if (.equals 'my.ns 'clojure.core)
nil
(do
(dosync
(commute @#'*loaded-libs* conj 'my.ns))
nil)))
;; mildly edited for readability
(do
;; ___________
(in-ns 'my.ns)
;; ^^^^^^^^^^^
(with-loading-context
(refer 'clojure.core)
(require '[my.other-ns :as other]))
(if (.equals 'my.ns 'clojure.core)
nil
(do
(dosync
(commute @#'*loaded-libs* conj 'my.ns))
nil)))
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-2b.pdf}
;; mildly edited for readability
(do
(in-ns 'my.ns)
(with-loading-context
;; __________________
(refer 'clojure.core)
;; ^^^^^^^^^^^^^^^^^^
(require '[my.other-ns :as other]))
(if (.equals 'my.ns 'clojure.core)
nil
(do
(dosync
(commute @#'*loaded-libs* conj 'my.ns))
nil)))
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-2.pdf}
;; mildly edited for readability
(do
(in-ns 'my.ns)
(with-loading-context
(refer 'clojure.core)
;; ________________________________
(require '[my.other-ns :as other]))
;; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(if (.equals 'my.ns 'clojure.core)
nil
(do
(dosync
(commute @#'*loaded-libs* conj 'my.ns))
nil)))
(ns my.other-ns)
;; mildly edited for readability
(do
;; _____________________
(in-ns 'my.other-ns)
(with-loading-context
(refer 'clojure.core))
;; ^^^^^^^^^^^^^^^^^^^^^
(if (.equals 'my.other-ns 'clojure.core)
nil
(do
(dosync
(commute @#'*loaded-libs* conj 'my.other-ns))
nil)))
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-3.pdf}
;; mildly edited for readability
(do
(in-ns 'my.other-ns)
(with-loading-context
(refer 'clojure.core))
;; ______________________________________________
(if (.equals 'my.other-ns 'clojure.core)
nil
(do
(dosync
(commute @#'*loaded-libs* conj 'my.other-ns))
nil)))
;; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(ns my.other-ns)
(def foo "foo")
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-4.pdf}
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-5.pdf}
;; mildly edited for readability
(do
(in-ns 'my.ns)
(with-loading-context
(refer 'clojure.core)
;; ________________________________
(require '[my.other-ns :as other]))
;; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
(if (.equals 'my.ns 'clojure.core)
nil
(do
(dosync
(commute @#'*loaded-libs* conj 'my.ns))
nil)))
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-6.pdf}
(ns my.ns
(:require
[my.other-ns :as other]))
(defn assoc-foo
[m]
(assoc m :foo other/foo))
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-7.pdf}
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-8.pdf}
All top-level forms in clojure code are for side effects, often in the namespace graph
\frame{\subsectionpage}
\huge
$ clj
Clojure 1.10.0-RC2
user=> (require 'my.ns)
\hspace*{-23pt} \includegraphics[scale=0.23]{images/API-normal.pdf}
\hspace*{-23pt} \includegraphics[scale=0.23]{images/API-repl.pdf}
public final class user$eval1
extends clojure.lang.AFunction {
// #'clojure.core/require
public static final clojure.lang.Var const__0;
// 'my.ns
public static final clojure.lang.AFn const__1;
// const__0 = RT.var("clojure.core","require");
// const__1 = Symbol.intern(null,"my.ns");
public static {};
// super()
public user$eval1();
// const__0.getRawRoot().invoke(const__1);
public static java.lang.Object invokeStatic();
// call .invokeStatic()
public java.lang.Object invoke();
}
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-1.pdf}
\small
package clojure.lang;
import java.net.URLClassLoader;
// ...etc.
public class DynamicClassLoader
extends URLClassLoader{
static ConcurrentHashMap<String, Reference<Class>>
classCache =
new ConcurrentHashMap<String, Reference<Class>>();
// ... lots of overridden methods
}
\hspace*{-23pt} \includegraphics[scale=0.23]{images/API-require.pdf}
(ns my.ns
(:require
[my.other-ns :as other]))
(defn assoc-foo
[m]
(assoc m :foo other/foo))
\small
;; mildly edited for readability
(do
(in-ns 'my.ns)
((fn loading__6621__auto__ []
(clojure.lang.Var/pushThreadBindings
{clojure.lang.Compiler/LOADER
(.getClassLoader
(.getClass loading__6621__auto__))})
(try
(refer 'clojure.core)
(finally
(clojure.lang.Var/popThreadBindings)))))
(if (.equals 'my.ns 'clojure.core)
nil
(do
(clojure.lang.LockingTransaction/runInTransaction
(fn [] (commute @#'*loaded-libs* conj 'my.ns)))
nil)))
\large
public class my.ns$eval154
extends clojure.lang.AFunction {
// ...
public static Object invokeStatic(){
if(!clojureCoreSym.equals(myNsSym)){
LockingTransaction.runInTransaction(
new my.ns$eval154$fn__155();
);
}
}
}
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-1.pdf}
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-2.pdf}
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-3.pdf}
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-4.pdf}
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-5.pdf}
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-6.pdf}
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-7.pdf}
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-8.pdf}
- Each function literals is compiled to a dedicated class that can be instantiated into functions
- At the repl and during normal code loading, top-level forms are
- compiled to bytecode for a 0-arg function
- loaded via the
DynamicClassLoader
- instantiated and
.invoke()
‘d, likely performing side effects on the namespace graph or returning a value to the repl
\frame{\subsectionpage}
\LARGE
$ clj
Clojure 1.10.0-RC2
user=> (compile 'my.ns)
Syntax error macroexpanding
clojure.core/ns at (ns.clj:1:1).
No such file or directory
\LARGE
$ mkdir classes
$ clj
Clojure 1.10.0-RC2
user=> (compile 'my.ns)
my.ns
$ find classes/ -type f
classes/clojure/core/specs/alpha$fn__222.class
classes/clojure/core/specs/alpha$fn__235.class
classes/clojure/core/specs/alpha$fn__285$fn__289.class
classes/clojure/core/specs/alpha__init.class
classes/clojure/core/specs/alpha$fn__249$fn__255.class
classes/clojure/core/specs/alpha$fn__180.class
classes/clojure/core/specs/alpha$fn__237$fn__241.class
classes/clojure/core/specs/alpha$fn__249$fn__253.class
classes/clojure/core/specs/alpha$fn__212.class
classes/clojure/core/specs/alpha$fn__197.class
... 47 more ...
$ find classes/ -type f | grep my
classes/my/ns$assoc_foo.class
classes/my/ns$fn__304.class
classes/my/ns$loading__6706__auto____298.class
classes/my/ns__init.class
classes/my/other_ns$fn__302.class
classes/my/other_ns$loading__6706__auto____300.class
classes/my/other_ns__init.class
\hspace*{-23pt} \includegraphics[scale=0.23]{images/API-compile.pdf}
$ find classes/ -type f | grep my
classes/my/ns$assoc_foo.class
classes/my/ns$fn__304.class
classes/my/ns$loading__6706__auto____298.class
classes/my/ns__init.class
classes/my/other_ns$fn__302.class
classes/my/other_ns$loading__6706__auto____300.class
classes/my/other_ns__init.class
(in-ns 'my.ns)
- call ns-helper-fn-1
- pass ns-helper-fn-2 to that transaction locking thing
- instantiate
my.ns$assoc_foo
- call
.setMeta
on#'assoc-foo
- call
.bindRoot
on#'assoc-foo
public class my.ns__init
- static fields
- 2 vars (
#'in-ns
,#'assoc-foo
), - 3 constants (metadata, ~’my.ns~, ~’clojure.core~)
- 2 vars (
- static init
- calls
__init0()
- calls
load()
- calls
__init0
- initializes the five static fields
load
- call
(in-ns 'my.ns)
- initialize
my.ns$loading__6706__auto____298
and.invoke()
- check
(= 'clojure.core 'my.ns)
- call
LockingTransaction.runInTransaction(new my.ns$fn__304())
- set metadata on
#'assoc-foo
- initialize
my.ns$assoc_foo
, set root binding of#'assoc-foo
- call
- static fields
- (compile 'my.ns)
- Compiler.compile
- Compiler.compile1('(ns my.ns ...))
- writes two helper classes
- (require 'my.other-ns)
- Compiler.compile
- Compiler.compile1('(ns my.other-ns ...))
- writes two helper classes
- Compiler.compile1('(def foo "foo"))
- writes init class for my.other-ns
- Compiler.compile1('(defn assoc-foo ...))
- writes assoc_foo class
- writes init class for my.ns
compile
- operates on namespaces
- evals all of the code into the current JVM
- writes
.class
files for all classes, including an*__init
class that performs all the top-level side effects
\frame{\subsectionpage}
\huge
$ clj
Clojure 1.10.0-RC2
user=> (require 'my.ns)
nil
\hspace*{-23pt} \includegraphics[scale=0.23]{images/API-require-compiled.pdf}
my.ns__init<clinit>
- looks up vars and constants
- calls
(in-ns)
- calls
(require 'my.other-ns)
my.other_ns__init<clinit>
- looks up vars and constants
- calls
(in-ns)
- sets meta and root of
#'foo
- initializes
my.ns$assoc_foo
- sets meta and root of
#'assoc-foo
require
-ing a namespace when there’s a class file available results in loading the*__init
class, which triggers all the top-level side effects that mutate the namespace graph
\frame{\subsectionpage}
user=> (require 'my.ns)
nil
user=> (in-ns 'my.other-ns)
#object[clojure.lang.Namespace 0x4ee37ca3 "my.other-ns"]
my.other-ns=> (def foo "bar")
#'my.other-ns/foo
\hspace*{-23pt} \includegraphics[scale=0.35]{images/RT-inkscape-9.pdf}
my.other-ns=> (my.ns/assoc-foo {})
{:foo "bar"}
src/my/ns.clj
(ns my.ns
(:require
[my.other-ns :as other]))
(defn assoc-foo
[m]
(assoc m :foo other/foo
:another "map-entry"))
\hspace*{-23pt} \includegraphics[scale=0.23]{images/API-require.pdf}
(in-ns)
(already exists)(require 'my.other-ns)
(NOOP)- Compile the function, load into DynamicClassLoader as
my.ns$assoc_foo
- Compiler looks up
#'assoc-foo
(already exists) (.setMeta #'assoc-foo ...)
(no problem)(.bindRoot #'assoc-foo (new my.ns$assoc_foo))
perfect!
my.other-ns=> (my.ns/assoc-foo {})
{:foo "bar" :another "map-entry"}
- the repl and
(require :reload)
do largely the same thing:- Use
Compile.eval
- creates classes
- loads them into the dynamic classloader
- invokes them, so that they perform modifications to the namespace graph, existing namespaces and vars
- Use
\frame{\sectionpage}
- Weird things can happen with name collisions
- The unit of compilation isn’t quite a form – files have scope for certain dynamic vars, which the repl can’t honor
- There’s a namespace graph!
ns
,def
, and other top level “commands” mutate the namespace graph- Functions are compiled to classes in memory or in the file system
- in AOT, files can be compiled to
*__init
classes that perform the same mutations as evaling the file