メリークリスマス!本記事は Emacs Advent Calendar 2019 の25日目の記事です。
まずはこちらをご覧ください。
java コマンドと同様、Emacs でも "Hello, World!" を出力していますね。 HelloWorld.java を書き換えてコンパイルしたあとも、java コマンドの結果と同じ文字列を出力しています。
これはどういうことかというと、 純度 100% Emacs Lisp で .class
ファイルを解析・実行しています。
つまり Emacs が JVM となった瞬間です。おめでとうございます 🎉
当然ながら JVM 全てをカバーできているわけではなく、基本的な部分だけを実装してあります。 今後も開発は続くかもしれないし、続かないかもしれません。
本記事の概要
- 書いてあること
- Emacs Lisp でそれっぽく動くところまでの話
- 書いていないこと
- JVM とは何か
- .class ファイルフォーマットの詳細な解説
開発に至った経緯
Twitter を眺めていたある日、下記スライドが目に止まりました。
いわゆる JVM 言語と呼ばれている JRuby や Kotlin とかと気持ち的には同じものかな? みたいなふわっとした認識でした。 その後、他の言語でも同じことやってるものが無いか軽くしらべてみたら、いろいろありました (参考資料) 。
🤔「なるほどね」
😳「ところで Emacs でもできるのかな!?」
JVM 言語における Lisp 実装といえば Clojure を思いつきますが、その流れで Emacs Lisp で探してみました。 ぱっと見つからなかったのでおそらく無いのでしょう。よし、ならば作ってみよう!
という気持ちが沸きあがり、勢いで着手しました。去年と似たような気持ち です。
開発のおはなし
JVM 実装の流れは先人達の資料に書かれているので、ここでは
- Emacs Lisp ではどうやって実現したか
- 苦労したところ
あたりをささっと記していきます。
1. .class ファイルの解析
これができなければ先に進めません。早速 .class ファイルのフォーマットを確認しました。
Chapter 4. The class File Format
例えば1つの .class ファイル全体のフォーマットはこう定義されています。
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
via 4.1. The ClassFile Structure - Chapter 4. The class File Format
Emacs で決められたフォーマットを持つバイナリファイルを解析する時によく使われるライブラリとして bindat
が挙げられます(要出展)。
そこで、今回 ClassFile に対応する bindat spec を下記のように定義してみました。
;; bindat の spec で指定する u8 は 1byte を表しており ;; JVM の spec に記載されている u1 と同じ (defconst jvm--classfile-spec-classfile '( (magic u32) (minor-version u16) (major-version u16) (constant-pool-count u16) (constant-pool repeat (eval (- last 1)) (struct jvm--classfile-spec-cp-info)) (access-flags u16) (this-class u16) (super-class u16) (interfaces-count u16) (interfaces repeat (interfaces-count) u16) (fields-count u16) (fields repeat (fields-count) (struct jvm--classfile-spec-field-info)) (methods-count u16) (methods repeat (methods-count) (struct jvm--classfile-spec-method-info)) (attributes-count u16) (attributes repeat (attributes-count) (struct jvm--classfile-spec-attribute-info)) ))
定義した bindat spec で実際に .class ファイルを解析してみましょう。
(let* ((data (with-temp-buffer (insert-file-contents-literally "./examples/HelloWorld.class") (buffer-substring-no-properties (point-min) (point-max)))) (decoded (bindat-unpack jvm--classfile-spec-classfile (encode-coding-string data 'raw-text)))) (bindat-get-field decoded 'magic)) ;; => #xcafebabe
うまくいきました。
1-1. 苦労したところ
ClassFile は一部可変領域があります。それが ConstantPool です。(ConstantPool が何か、という説明は今回は割愛します)
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; ← こいつ!!! u2 access_flags; attribute_info attributes[attributes_count]; (省略)
一見、 constant_pool_count
の数だけ cp_info
なるものを用意すれば良いのじゃろ? と思わせてきますが、実はこの cp_info
が厄介で
cp_info { u1 tag; u1 info[]; }
via https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4
この info[]
というメンバ、サイズが指定されていません。何かというと tag の値によって領域が変わる というものです。
例えば CONSTANT_Class_inf
だとこうなり:
CONSTANT_Class_info { u1 tag; u2 name_index; }
CONSTANT_Fieldref_info
だとこうなります:
CONSTANT_Fieldref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
このようなパターンを bindat spec にどうやって落とせば良いか。今回は下記のように定義しました。
(defconst jvm--classfile-spec-cp-info '((tag u8) (union (tag) (7 ;; CONSTANT_Class_info (name-index u16)) (9 ;; CONSTANT_Fieldref_info (class-index u16) (name-and-type-index u16)) ;; 省略 ;; 全体はこちら https://github.com/gongo/emacs-jvm/blob/ea239628d77a893ba28a950c6dc304b2ea5cd5d8/jvm-classfile.el#L13-L53
union キーワードを使い 引数に指定した値によって、その後の spec を切り替える という方法で実現できました。
2. 命令を実装していく
なんやかんやあり、なんとか .class ファイルの解析が完了しました。 あとは解析結果から main 関数の処理 を抽出し、それを1つずつ順番に実行していくだけです。
その実行していく命令も、当然実装していかなければなりません。やりましょう!
Chapter 6. The Java Virtual Machine Instruction Set
まずは、どの命令の実装が必要となるかを調べる必要があります。これは javap -v
あたりで確認できます。
$ javap -v HelloWorld (省略) public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello, World! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return (省略)
javap -v
の出力から
- 0: getstatic
- 3: ldc
- 5: invokevirtual
- 8: return
これらが必要であることがわかります。あとはこれらを仕様書どおりに実装していくだけです。
https://github.com/gongo/emacs-jvm/blob/ea239628d77a893ba28a950c6dc304b2ea5cd5d8/jvm-instruction.el
2-1. 苦労したところ
HelloWorld 系でよく目にする System.out.println
は標準パッケージであり、.class ファイルが無い(どっかで探せばあるのか?)ため、どのように実装すれば良いでしょうか。
他の言語で実装している先人を参考にした結果、「とりあえずそれっぽいものを用意する」のが主流らしいので
emacs-jvm でもそのようにしてはいるのですが、正直うまく書けませんでした。。。
納得いかないので、いつか綺麗に書きたいです。こういう奴って Emacs Lisp だとどう表現するのがすっきりするのだろうか。
3. おまけ
3-1. なぜ ring を使っているのか
operand-stack は文字どおり stack なので、当初は pop push できる list で実装していました。 しかし各 instruction 内で pop push した結果が、呼び出し元 (今回でいえば jvm--classfile-run-method) に反映されない的なよくある話になり
「dynamic binding とかで参照できるようにしてやろうぜ」 「それ用の struct を定義して渡してあげればいいんじゃね?」
とかいろいろあったのですが、面倒くさくなって今回は ring にしました。動きもそれっぽいのでひとまずはこれで。
ここもいつかスッキリしたいポイント。
3-2. Signature の解析
Signatures encode declarations written in the Java programming language that use types outside the type system of the Java Virtual Machine. They support reflection and debugging, as well as compilation when only class files are available.
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.9.1
例えば main(String[] args)
の Signture は ([Ljava/lang/String;)V
になります。
なぜこいつの解析が必要になるかというと、こいつに記述されている引数の型や数を確認しなければならないのです。(引数の数だけ stack から pop しないといけない、とかそういう実装がある)。
今回は php-java の実装 を参考に、無理矢理 Emacs Lisp に落としました。(感謝。。。)
まとめ
余談
それっぽく動きはしました。しかしまだまだ足りない機能があることは当然ながら、実装そのものもうまくいかずに結構やっつけコードになってしまったので悔しい限りです。さすがに全ての JVM エミュレートは無理なのですが、いつか「お、結構いけるな」ぐらいのレベルまで再現できるようになるといいな。
参考資料
- きっかけ
- https://speakerdeck.com/memory1994/php-de-jvm-woshi-zhuang-site-hello-world-wochu-li-surumade
- きっかけとしてだけではなく、下記の仕様書を見てもよくわからなかった部分を補完してくれたのでとても助かりました
- 仕様書
- 参考実装