Skip to content

clojure-goes-fast/clj-java-decompiler

Repository files navigation

clj-java-decompiler CircleCI

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-2024 Alexander Yakushev