clojureが*.cljをコンパイルしたときに,どのようなバイトコードが 生成されるのかを調べてみる. まずは,単純な例: (ns example.hello)から始めてみる.

気をつけること

  • コンパイル単位は名前空間
  • 名前空間ごとにクラスローダ"namespace__init.class"が作られる
  • http://clojure.org/compilationに詳細あり
  • コンパイル時は(コンパイル対象のclojureコード)ソースディレクトリをクラスパスに含める
  • 実行時はコンパイルされたクラスファイルの他にclojure.jarをクラスパスに含める

コンパイル

leiningenを使わず,生のclojureでコンパイルしていく. 以下のように3つのクラスファイルが作られる

  • hello__init.class
  • hello$fn__4.class
  • hello$loading__4958__auto__.class
$ mkdir clojure-work ; cd clojure-work
$ wget http://central.maven.org/maven2/org/clojure/clojure/1.6.0/clojure-1.6.0.jar
$ mkdir -p src/example
$ vim src/example/hello.clj
  (ns example.hello)
$ mkdir classes
$ tree
    .
    ├── classes
    ├── clojure-1.6.0.jar
    └── src
        └── example
            └── hello.clj
$ java -cp ./src:./classes:clojure-1.6.0.jar clojure.main
user=> (compile 'example.hello)
$ tree classes
    classes
    └── example
        ├── hello$fn__4.class
        ├── hello__init.class
        └── hello$loading__4958__auto__.class
## もし,hello.cljにmain関数を定義したならば
## hello.classが作られるように:gen-classをつけコンパイルする.
# $ java -cp clojure-1.6.0.jar:./classes example.hello

マクロ展開

名前空間を作る(ns ’example.hello)は,マクロなので展開しておく. まず,macroexpandの結果.

(do (clojure.core/in-ns (quote example.hello))
    (clojure.core/with-loading-context (clojure.core/refer (quote clojure.core)))
    (if (.equals (quote example.hello) (quote clojure.core))
      nil
      (do (clojure.core/dosync
            (clojure.core/commute
              (clojure.core/deref (var clojure.core/*loaded-libs*))
              clojure.core/conj
              (quote example.hello)))
          nil)))

さらに展開すると以下のようになるよう. ただmacroexpandでは展開しきれないなので,参考資料から予想した. 後々詳細を書くが,全体はhello__init.classに書かれている.

(do
  (clojure.core/in-ns (quote example.hello))
  ((fn* loading__4958__auto__
        ([]
         (. clojure.lang.Var (clojure.core/pushThreadBindings {clojure.lang.Compiler/LOADER (. (. loading__4958__auto__ getClass) getClassLoader)}))
         (try
           (clojure.core/refer (quote clojure.core))
           (finally
             (. clojure.lang.Var (clojure.core/popThreadBindings)))))))
  (if (. (quote example.hello) equals (quote clojure.core))
    nil
    (do
      (. clojure.lang.LockingTransaction (clojure.core/runInTransaction (fn*
                                                                          ([]
                                                                           (clojure.core/commute (clojure.core/deref (var clojure.core/*loaded-libs*))
                                                                                                 clojure.core/conj
                                                                                                 (quote example.hello))))))
      nil)))

これ以降は生成されたクラスファイルを見ていく.

hello__init.class

名前空間をロードする場合は,AOTの__init.classファイルを探し,ロードする. static initializerはクラスの読み込み時に走り, 名前空間以下の関数などのロードはこのタイミングで行われる. 以下は,javapでデコンパイルしたものをjava擬似コードで表現したもの. pushNSandLoaderやpopthreadbindingsは*ns*,*fn-loader*,*read-eval*などの clojureのトップレベルの束縛を変更している. (in-ns … )や(with-loading-context … )はIFnを使って呼んでいる. 特に,(with-loading-context … )の方はhello$loading__4958__auto__.classに 実体がある.

package example;
import clojure.lang.*;

public class hello__init {
  public static {};                 // staitc initializer

  public static final Var const__0;
  public static final AFn const__1;
  public static final AFn const__2;

  public static void load();
  public static void __init0();
}

static {
  __init0();
  ClassLoader loader = Class.forName("example.hello__init").getClassLoader();
  clojure.lang.Compiler.pushNSandLoader(loader);
  try {
    load();
  } finally {
    clojure.lang.Var.popThreadBindings();
  }
}

static void __init0() {
  const__0 = (Var)RT.var("clojure.core", "in-ns");
  const__1 = (AFn)Symbol.intern(null, "example.hello");
  const__2 = (AFn)Symbol.intern(null, "clojure.core");
}

public static void load() {
  // (in-ns 'example.hello)
  IFn inNs = (IFn)const__0.getRawRoot();
  inNs.invoke(const__1);

  // (with-loading-context (refer 'clojure.core))
  IFn loading4958auto = (IFn)new example.hello$loading__4958__auto();
  loading4958auto.invoke();

  // (if (.equals 'example.hello 'clojure.core)
  //   nil
  //   (do
  //     (LockingTransaction/runIntransaction (fn* …))
  //     nil))
  Symbol exampleHello = (Symbol)const__1;
  if (exampleHello.equals(const__2)) {
    return null;
  } else {
    Callable callable = (Callable)new example.hello$fn__4();
    LockingTransaction.runInTransaction(callable);
    return null;
  }
}

hello$loading__4958__auto__.class

(with-loading-context (refer 'clojure-core))に対応する. AFunctionクラスは,clojureの1関数に対応するようで,

  • ロード時に,static initializer
  • 関数利用時に,constructor

を使う.

mapUniqueKeysあたりで引数を取ってきているが詳細は未調査. なんとなくそれっぽいことはわかる.

package example;
import clojure.lang.*;

public final class hello$loading__4958__auto__ extends AFunction {
  public static final Var const__0;
  public static final AFn const__1;

  public static {};
  public hello$loading__4958__auto__();

  public Object invoke();
}

public static {
  const__0 = (Var)Rt.var("clojure.core", "refer"); const__1 = (AFn)Symbol.intern(null, "clojure.core");
}

public hello$loading__4958__auto__() {
    super();
}

public Object invoke() {
  // (Var/pushThreadBindings {Compiler/LOADER (.getClassLoader (.getClass loading__4958__auto__))})
  Object[] bindings = new Object[2];
  bindings[0] = Compiler.LOADER;
  bindings[1] = this.getClass().getClassLoader();
  Var.pushThreadBindings((Associative)RT.mapUniqueKeys(bindings));

  try {
    // (refer 'clojure.core)
    IFn refer = (IFn)const__0.getRawRoot();
    return refer.invoke(const__1);
  } finally {
    Var.popThreadBindings();
  }
}

hello$fn__4.class

まず,展開したものを書いておく. (fn* ([] …))あたりに対応している. *loaded-libs*にexample.helloを追加していくのはわかる.

(dosync (commute (deref #'clojure.core/*loaded-libs*)
                 conj
                 'example.hello))

=>

(LockingTransaction/runInTransaction
  (fn* ([] (commute (deref #'clojure.core/*loaded-libs*)
                    conj
                    'example.hello))))
package example;
import clojure.lang.*;

public final class hello$fn__4 extends AFunction {
  public static final Var const__0;
  public static final Var const__1;
  public static final Var const__2;
  public static final Var const__3;
  public static final AFn const__4;

  public static {};

  public hello$fn__4();

  public Object invoke();
}

public static {
  const__0 = (Var)RT.var("clojure.core", "commute");
  const__1 = (Var)RT.var("clojure.core", "deref");
  const__2 = (Var)RT.var("clojure.core", "*loaded-libs*");
  const__3 = (Var)RT.var("clojure.core", "conj");
  const__4 = (AFn)Symbol.intern(null, "example.hello");
}

public hello$fn__4() {
    super();
}

public Object invoke() {
  IFn commute = (IFn)const__0.getRawRoot();
  IFn deref = (IFn)const__1.getRawRoot();
  // (deref #'clojure.core/*loaded-libs*)
  Object loadedLibs = deref.invoke(const__2);
  Object conj = const__3.getRawRoot();
  // (commute loadedLibs conj 'example.hello)
  return commute.invoke(loadedLibs, conj, const__4);
}

参考

How Clojure works: a simple namespace