clj-java-decompiler
You can read the motivation behind clj-java-decompiler and the usage example in the blog post.
This library is an integrated Clojure-to-Java decompiler usable from the REPL. Under the hood, it uses Procyon to decompile the bytecode generated by Clojure compiler into the equivalent Java source code.
Quick demo:
user> (clj-java-decompiler.core/decompile
(loop [i 100, sum 0]
(if (< i 0)
sum
(recur (unchecked-dec i) (unchecked-add sum i)))))
// Decompiling class: user$fn__13332
import clojure.lang.*;
public final class user$fn__13332 extends AFunction
{
public static Object invokeStatic() {
long i = 100L;
long sum = 0L;
while (i >= 0L) {
final long n = i - 1L;
sum += i;
i = n;
}
return Numbers.num(sum);
}
public Object invoke() {
return invokeStatic();
}
}
Why?
There are several usecases when you may want to use a Java decompiler:
- To get a general understanding how Clojure compiler works: how functions are compiled into classes, how functions are invoked, etc.
- To optimize performance bottlenecks when using low-level constructs like loops, primitive math, and type hints.
- To investigate how Java interop facilities are implemented (
reify
,proxy
,gen-class
).
Usage
Add com.clojure-goes-fast/clj-java-decompiler
to your dependencies:
Then, at the REPL:
user> (require '[clj-java-decompiler.core :refer [decompile]])
nil
user> (decompile (fn [] (println "Hello, decompiler!")))
// Decompiling class: clj_java_decompiler/core$fn__13257
import clojure.lang.*;
public final class core$fn__13257 extends AFunction
{
public static final Var __println;
public static Object invokeStatic() {
return __println.invoke("Hello, decompiler!");
}
public Object invoke() {
return invokeStatic();
}
static {
__println = RT.var("clojure.core", "println");
}
}
You can also disassemble to bytecode, with the output being similar to the one
of javap
.
user> (disassemble (fn [] (println "Hello, decompiler!")))
;;; Redacted
public static java.lang.Object invokeStatic();
Flags: PUBLIC, STATIC
Code:
linenumber 1
0: getstatic clj_java_decompiler/core$fn__17004.const__0:Lclojure/lang/Var;
3: invokevirtual clojure/lang/Var.getRawRoot:()Ljava/lang/Object;
linenumber 1
6: checkcast Lclojure/lang/IFn;
9: getstatic clj_java_decompiler/core$fn__17004.const__1:
Lclojure/lang/Var;
12: invokevirtual clojure/lang/Var.getRawRoot:()Ljava/lang/Object;
linenumber 1
15: checkcast Lclojure/lang/IFn;
18: ldc "Hello, decompiler!"
linenumber 1
20: invokeinterface clojure/lang/IFn.invoke:(Ljava/lang/Object;)Ljava/lang/Object;
linenumber 1
25: invokeinterface clojure/lang/IFn.invoke:(Ljava/lang/Object;)Ljava/lang/Object;
30: areturn
Post-processing and de-cluttering
To make the output clearer, clj-java-decompiler by default disables locals clearing for the code it compiles. You can re-enable it by setting this compiler option to false explicitly, like this:
(binding [*compiler-options* {:disable-locals-clearing false}]
(decompile ...))
You can also change other compiler options (static linking, metadata elision) in the same way.
By default, clj-java-decompiler also performs additional post-processing of the
Procyon output. This includes removing current class name from static
references, and replacing opaque const__
fields with more informative var
names. You can disable this post-processing by executing:
(reset! clj-java-decompier.core/postprocessing-enabled false)
Usage from Emacs
You can use clj-decompiler.el
package (installable from MELPA) to fluidly invoke clj-java-decompiler
right
from your Clojure code buffer. Like with cider-macroexpand
, you place your
cursor at the end of the form you want to decompile and invoke M-x clj-decompiler-decompile
. This will compile the form before the cursor, then
decompile it with clj-java-decompiler
, and present you the Java output in a
separate syntax-highlighted buffer.
clj-decompiler.el
can also automatically inject clj-java-decompiler
dependency at cider-jack-in
time. Check its repository for more details.
How to decompile an already defined function
Short answer: you can't do that. JVM doesn't retain the bytecode for classes it
has already loaded. When the Clojure compiler compiles a piece of Clojure code,
it transforms it into bytecode in memory, then loads it with a classloader, and
discards the bytecode. So, in order to decompile a function, you must pass its
source code to the decompile
macro.
Fortunately, most Clojure libraries are distributed in the source form. If you
use CIDER or any other Clojure IDE, you can jump to the definition of the
function you want to decompile, disable read-only mode (in Emacs, that is done
with C-x C-q), wrap the defn
form with
clj-java-decompiler.core/decompile
and recompile the form (C-c C-c
in Emacs). This becomes much simpler if you use
clj-decompiler.el, you just call
M-x clj-decompiler-decompile
on the function you've jumped to.
If you absolutely need to decompile a loaded function for which the source code is not available, you can consider trying the no.disassemble library. Note that it must be loaded into the JVM at startup time as an agent and can only disassemble functions into bytecode representation (not decompile into Java code).
Another option for when you have no source code but compiled .class
files is
to use one of the available Java
decompilers.
License
clj-java-decompiler is distributed under the Eclipse Public License. See LICENSE.
Copyright 2018-2023 Alexander Yakushev