Thanks Driven Life

日々是感謝

JVM (Java Virtual Machine) on Emacs

メリークリスマス!本記事は Emacs Advent Calendar 2019 の25日目の記事です。

まずはこちらをご覧ください。

f:id:gongoZ:20191225010105g:plain

java コマンドと同様、Emacs でも "Hello, World!" を出力していますね。 HelloWorld.java を書き換えてコンパイルしたあとも、java コマンドの結果と同じ文字列を出力しています。

これはどういうことかというと、 純度 100% Emacs Lisp.class ファイルを解析・実行しています。 つまり EmacsJVM となった瞬間です。おめでとうございます 🎉

当然ながら JVM 全てをカバーできているわけではなく、基本的な部分だけを実装してあります。 今後も開発は続くかもしれないし、続かないかもしれません。


本記事の概要

  • 書いてあること
    • Emacs Lisp でそれっぽく動くところまでの話
  • 書いていないこと
    • JVM とは何か
    • .class ファイルフォーマットの詳細な解説

開発に至った経緯

Twitter を眺めていたある日、下記スライドが目に止まりました。

🤔「ふむふむ、PHPJVMエミュレーターを…」

いわゆる 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 でもそのようにしてはいるのですが、正直うまく書けませんでした。。。

https://github.com/gongo/emacs-jvm/blob/ea239628d77a893ba28a950c6dc304b2ea5cd5d8/jvm-class.el#L25-L33

納得いかないので、いつか綺麗に書きたいです。こういう奴って Emacs Lisp だとどう表現するのがすっきりするのだろうか。

3. おまけ

3-1. なぜ ring を使っているのか

https://github.com/gongo/emacs-jvm/blob/ea239628d77a893ba28a950c6dc304b2ea5cd5d8/jvm-classfile.el#L173-L176

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--util-parse-signature

まとめ

EmacsJVM にもなれる!お疲れ様でした!!

余談

それっぽく動きはしました。しかしまだまだ足りない機能があることは当然ながら、実装そのものもうまくいかずに結構やっつけコードになってしまったので悔しい限りです。さすがに全ての JVM エミュレートは無理なのですが、いつか「お、結構いけるな」ぐらいのレベルまで再現できるようになるといいな。

参考資料

Fit Boxing を始めて5ヶ月が経過しました

先月はこちら: http://gongo.hatenablog.com/entry/2019/09/01/232445

記録

開始 (9月上旬) 先月 今月 増減 (先月/開始)
98.8 93.8 93.6 -0.2 / -5.2 kg

f:id:gongoZ:20191001211653p:plain:w400

  • 一瞬 92.2 まで落ちて「こりゃ 91kg 台もすぐだなー」と思ったけど油断して94後半まで戻った
  • 最終的には先月からのキープまで取り戻せたので一安心

150日という節目なので、体脂肪や筋肉量のグラフも載せてみる。

体脂肪 筋肉量
f:id:gongoZ:20191001211822p:plain:w400 f:id:gongoZ:20191001211831p:plain:w400

毎日の数値にブレはあるものの、推移としては、体脂肪減少、筋肉量微増、といったところか

体調

いたって問題なし

ネクストアクション

  • 今度こそ 91 kg に
  • 10月下旬は Switch に触れない期間が1週間ほどあるので、連続記録は途絶えます

おまけ

転職して2年が経過したので振り返ります

ほぼ2年が経過しました。早い。

初めての転職(& 一人暮らし)だったこともあり、節目であるこの時期に振り返ります。

ざっと振り返り

2017年 (9月〜12月)

  • 入社初日に前CTOの送別会が行われた
  • 入社して1週間後に「そろそろ移転するから」って言われた
  • 入社して1ヶ月後に年末調整機能の開発チームに入る(1回目)
    • 前職では1年に1回しか見ていなかったマルフ*1を毎日見ることに
    • 奥深い…人の手には余る…

2018年 (1月 〜 3月)

2018年 (4月 〜 12月)

  • 年末調整機能の開発チームに入る(2回目)
    • この年から React を導入
    • React わからない… Redux わからない…
  • 気合い入れるためにマルフTシャツ作った
  • つらい戦いが多く、周りに助けられてばかりだった
    • 感謝しかない

2019年 (1月 〜 3月)

2019年 (4月 〜 現在)

  • オフィス移転(2回目)
  • 年末調整機能の開発チームに入る(3回目)
    • この年から TypeScript を導入
    • 型さん…すごい…
  • 住宅ローン控除や団体扱い生命保険に対応
    • 更に奥深い…人の手には余る…
  • :memo: 今年は 10/1 から公開 *4

ざっと振り返って

  • 社内で一番長く年末調整機能に関わってそう
  • まだまだ把握しきれていない書類や手続きが多い
  • とにかく周りに助けられており、日々感謝

まとめ

2年、あっという間でした。

入社した当時、社員は30名ちょい(エンジニアは10名に満たない)だったのが、今では150名あたり(エンジニアは30名ぐらい)となりました。 この2年ですっかり古参の部類に入ってしまいました。とはいえまだまだわからないことも多々あり、 めちゃできるエンジニアもどんどん増えてきているので、みんなにいろいろ教えてもらおうと思います。

会社はこれからもどんどん突っ走っていく様相で、それに振り落されないようにシュッと食らい付いていく気持ちが高まっています。

おまけ(一人暮らしについて)

  • 自炊はしたりしなかったり
  • 東京の食べ歩きブログ、少しずつ溜まってきました
  • 歩くことを強いられがちな土地*5 & Fit Boxing 始めたので、転職前よりは健康かも

merciful-polluter 0.0.5 をリリースしました

概要

magic_quotes_gpc のエミュレートを ON にしている時、$_REQUESTエスケープされていなかった問題が修正されました。

例えば下記のようなサーバーサイドのコードがあるとします。

<?php
$request = new Gongo\MercifulPolluter\Request();
$request->enableMagicQuotesGpc();
$request->pollute();

var_dump($_GET);
var_dump($_POST);
var_dump($_COOKIE);
var_dump($_REQUEST);

次にクライアントとして下記コードを実行してみます。

$ curl 'http://localhost/?hoge=ho"ge' -X POST -d "huga=hu'ga" -b 'piyo=pi\yo'

その結果がこちら!

// $_GET
array(1) {
  ["hoge"]=>
  string(6) "ho\"ge"
}

// $_POST
array(1) {
  ["huga"]=>
  string(6) "hu\'ga"
}

// $_COOKIE
array(1) {
  ["piyo"]=>
  string(6) "pi\\yo"
}

// $_REQUEST
array(3) {
  ["hoge"]=>
  string(6) "ho\"ge" // 修正前は "ho"ge" だった
  ["huga"]=>
  string(6) "hu\'ga" // 修正前は "hu'ga" だった
  ["piyo"]=>
  string(6) "pi\\yo" //  修正前は "pi\yo" だった
}

まとめ

そろそろ enableRegisterGlobals() とか用意してもいいかもしれない( magic_quotes_gpc だけ有効化したい、みたいなケースに対応)

merciful-polluter 0.0.4 をリリースしました

実に5年ぶり & 令和初となるバージョンアップです。

概要

$_GET$_POST で同じキーにそれぞれ違う配列がセットされている時に、オリジナルとは違った結果になる不具合を修正しました。

解説

例えば下記のようなサーバーサイドのコードがあるとします。

<?php

if (version_compare(PHP_VERSION, '5.4.0', '>')) {
    require_once './vendor/autoload.php';

    $request = new Gongo\MercifulPolluter\Request;
    $request->pollute();
}

var_dump($foo);

// php.ini は下記を想定
//
// register_globals = 'On'
// variables_order = 'GP'

上記コードを、例えば PHP 5.3 で動かしてみると:

$ curl 'http://localhost?foo\[bar\]\[a\]=123' -X POST -d "foo[bar][b]=789"
array(1) {
  ["bar"]=>
  array(2) {
    ["a"]=> string(3) "123"
    ["b"]=> string(3) "789"
  }
}

こうなりますが、これを PHP 7.1 with merciful-polluter 0.0.3 で動かしてみると:

$ curl 'http://localhost?foo\[bar\]\[a\]=123' -X POST -d "foo[bar][b]=789"
array(1) {
  ["bar"]=>
  array(1) {
    ["b"]=> string(3) "789"
  }
}

もうお分かりかと思います。

  • エミュレート版 (merciful-polluter) はそのまま上書きしちゃっている
  • オリジナル版は array_merge している

この違いがありました。それを裏付けるコードがこちら

https://github.com/php/php-src/blob/php-5.3.29/main/php_variables.c#L647-L668

正確には「上書きする値と上書き先の値がどちらも Array だった場合 array_merge_recursive (相当の処理)を行う」がオリジナルの挙動でした。

まとめ

令和です。

Fit Boxing を始めて4ヶ月が経過しました

先月はこちら: http://gongo.hatenablog.com/entry/2019/08/02/073526

記録

開始 (8月上旬) 先月 今月 増減 (先月/開始)
98.8 95.4 93.8 -1.6 / -5.0 kg

f:id:gongoZ:20190901232039p:plain

  • 8月中旬あたりで一度増えるも、なんとか巻き返した
  • 下旬には一度 93.2 まで落ちた。92kg台も近い

体調

いたって問題なし

ネクストアクション

あわよくば 91kg 台に突入したい

builderscon tokyo 2019 に参加しました

この季節がやってまいりました。

まずは登壇された皆様、運営の皆様、参加者の皆様、お疲れさまでした & ありがとうございました! 今年も楽しかったです!

そんなわけで、以下、参加したセッションの雑な感想(前夜祭は不参加)

1日目

2日目

朝一は遅刻しました

総括

相変らず builderscon に参加すると「よっしゃ開発するぞ!」という気持ちになります。ちなみに前回 (builderscon 2018) 参加した時は emacs-nes を作るきっかけ を得られました。今回もちょっと作りたいのを思い付いたので、やっていき

おわり

来年も参加するぞー