#chiroito ’s blog

Java を中心とした趣味の技術について

OpenJDK開発記:IntelliJ IDEA を使って OpenJDK を開発する (WSL編)

これは何?

OpenJDK には、リポジトリを IntelliJ IDEA で開けるように変換するスクリプトがあります。このスクリプトを使うと、IntelliJ IDEA で OpenJDK の Java の部分の開発ができるようになります。  しかし、このスクリプトは Linux と Cygwin (たぶんMacも)では動くのですが、WSL 上では動きませんでした。

どんな実装だったのか

このスクリプトは WSL 上で実行する必要があるのですが、そもそも WSL は考慮されていませんでした。Windows で開発する場合、昔は Cygwin を使っていたため当然です。 普通の Linux としてスクリプトが実行されますが、以下の 3 つの問題がありました。

  1. Java コンパイラの実行ファイル名が違う (javacとjavac.exe)
  2. JDK の実行ファイルはWSLのディレクトリを含むと動かない (たぶん正確ではないが良く知らない・・・)
  3. IntelliJ IDEA は Windows で実行するため Windows のパスが必要なのに WSL のパスでファイルが生成される

そのため、これらの問題を解決していきます。

バグレポートはこちらです。

[JDK-8250818] idea.sh script doesn't work on WSL 1 and 2 - Java Bug System

どう直したの?

全てが WSL の絶対パスになっていたため、以下の様な方針で修正しました。

  1. Javaコンパイラのファイル名を javac と javac.exe で変更できるよう
  2. スクリプト内で行われる処理は相対パスを使って全て Windows が管理する領域上で処理されるようにする
  3. 成果物は全て Windows のパスになるようにする

WSL_DISTRO_NAME という環境変数がある場合は WSL 上での実行としています。その場合は、パスを相対パスにしてくれる realpath と、パスを Windows のパスにしてくれる wslpath を使って変換しています。

相対パスにしたものは以下の環境変数に格納されてるパスです。

  • JAVAC_CP:クラスパス
  • JAVAC_CLASSES:クラスファイルの出力先ディレクトリ
  • JAVAC_SOURCE_FILE:コンパイル対象のクラス
  • JAVAC_SOURCE_PATH:クラスまたはインタフェースの定義を検索するソースパス

ここまで修正すると、スクリプトは実行できるようになります。ただ、成果物内のパスは WSL のパスのままなので、成果物に含まれるパスを Windows のパスになるようにします。以下の環境変数に格納されているパスを修正しています。

  • MODULE_ROOTS 内の各要素
  • SPEC_DIR
  • TOPLEVEL_DIR
  • IDEA_OUTPUT
  • JT_HOME

パッチはこちらです。

8250818: idea.sh script doesn't work on WSL 1 and 2 · openjdk/jdk@ee5dc7c · GitHub

OpenJDK開発記:JDK-8249720: Generated bytecodes of EventWriter don't be output to the logを直してみた。

これは何?

今回、パッチを書いたのはJDK-8249720Generated bytecodes of EventWriter don't be output to the logです。

[JDK-8249720] Generated bytecodes of EventWriter don't be output to the log - Java Bug System

VM起動引数に-Xlog:jfr+system+bytecode=traceを付けてJFRを実行すると、JFRの中で動的に作成されて使用されるEventWriterが生成されたときに、そのクラスのオペコードをログに出力する機能があります。 たまたまその機能を使ってバイトコードの変化を探してたらこのバグを発見しました。 OpenJDK の 15 からそれがデグレにより出力されなくなったため、それを修正しました。

どんな実装だったのか

原因としては、JFR のログ出力周りのオーバーヘッドを減らすという修正が入ったときに、修正ミスによってログが出力されなくなってしまいました。

-        Logger.log(LogTag.JFR_SYSTEM_BYTECODE, LogLevel.TRACE, () -> {
+        if (Logger.shouldLog(LogTag.JFR_SYSTEM_BYTECODE, LogLevel.TRACE)) {
            ClassReader cr = new ClassReader(bytes);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            PrintWriter w = new PrintWriter(baos);
            w.println("Bytecode:");
            cr.accept(new TraceClassVisitor(w), 0);
-            return baos.toString();
-        });
+        };

以前の実装では、最後の行のreturn baos.toString();がロガーに渡されてログを出力していました。これを消してしまったためログが出なくなっています。

8247320: JFR: Reduce logging overhead · openjdk/jdk@65c461e · GitHub

どう直したの?

単純にログを出力する行を足しただけです。それに加えて、もうデグレしないようにテストケースを追加しています。

パッチはこちら

パッチの抜粋はこちらです。

--- old/src/jdk.jfr/share/classes/jdk/jfr/internal/ASMToolkit.java   2020-07-19 21:31:04.125291400 +0900
+++ new/src/jdk.jfr/share/classes/jdk/jfr/internal/ASMToolkit.java    2020-07-19 21:31:03.955544600 +0900
@@ -155,6 +155,7 @@
             PrintWriter w = new PrintWriter(baos);
             w.println("Bytecode:");
             cr.accept(new TraceClassVisitor(w), 0);
+            Logger.log(LogTag.JFR_SYSTEM_BYTECODE, LogLevel.TRACE, baos.toString());
         };
     }
 
--- /dev/null 2020-07-19 21:20:25.577740400 +0900
+++ new/test/jdk/jdk/jfr/jvm/TestEventWriterLog.java  2020-07-19 21:31:05.847842600 +0900
@@ -0,0 +1,44 @@
+ * @test TestEventWriterLog
+ * @summary Test that log message of JFR when handle bytecodes
+ * @key jfr
+ * @requires vm.hasJFR
+ * @library /test/lib /test/jdk
+ * @run main/othervm TestEventWriterLog
+ */
+
+import jdk.test.lib.process.ProcessTools;
+import jdk.test.lib.process.OutputAnalyzer;
+
+public class TestEventWriterLog {
+    public static void main(String[] args) throws Exception {
+        ProcessBuilder pb = ProcessTools.createJavaProcessBuilder("-Xlog:jfr+system+bytecode=trace", "-XX:StartFlightRecording", "-version");
+        OutputAnalyzer output = new OutputAnalyzer(pb.start());
+        output.shouldContain("extends jdk/jfr/internal/handlers/EventHandler");
+    }
+}

Java のメモリ上にあるバイトコードをクラスファイルとして出力

開発者は Java 言語でソースコードを書き、そのソースコードは Javac というコンパイラでバイトコードに変換されクラスファイルとなり、クラスファイルは Jar ファイルとしてまとめられ、 Java コマンドでアプリケーションとして実行します。

アプリケーション上で実行されるバイトコードはコンパイルされたものと一緒であることが普通です。ASM などのバイトコードを操作するツールなど一部のフレームワークや Java の機能を使うと、このバイトコードは変更されます。操作結果を確認したい場合などそのバイトコードを見たいことは多々あると思いますので、取り出す方法を紹介します。

手順

取り出すためには JVM のデバッガである jhsdb というツールを使います。これは JDK に標準で含まれているツールです。javaコマンドと一緒に$JAVA_HOME/binに含まれています。これにhsdbという引数を付けて実行することでツールをGUIで起動します。

$ jhsdb hsdb

f:id:chiroito:20200719133335p:plain

デバッグをする方法は、プロセスへの接続、コアファイルを与えるなどいろいろあります。今回はプロセスIDを指定してプロセスへ接続します。 [File] - [Attach to HotSpot Process... Alt-A] を実行します。

f:id:chiroito:20200719133453p:plain

接続したいアプリケーションのプロセスIDを取得します。これはjhsdbと同じく Java に標準で含まれる jcmd を使うのが良いでしょう。これも$JAVA_HOME/binに含まれています。以下の様に実行すると、プロセスID と実行しているクラスが表示されます。今回はSampleApplicationへ接続しますのでプロセスIDは 82884 になります。

$ jcmd
82884 SampleApplication
157688 jdk.jcmd/sun.tools.jcmd.JCmd
75868 jdk.hotspot.agent/sun.jvm.hotspot.SALauncher hsdb

プロセスID を入力して[OK]を押すとそのプロセスへ接続します。接続が完了すると、以下の様に JVM で動いているスレッドの一覧が表示されます。

f:id:chiroito:20200719134049p:plain

今回の目的はクラスファイルを得ることです。その役割である JVM 上にロードされているクラスを見るクラスブラウザを起動しましょう。[Tools] - [Class Browser] を選択します。

f:id:chiroito:20200719134333p:plain

クラスブラウザが起動すると、JVM に読み込まれているクラスが一覧で表示されます。上の方にあるテキストフィールドに取得したいクラス名を入力することで絞ることができます。取得したいクラスが見つかったらそのクラス名をクリックします。

f:id:chiroito:20200719134555p:plain

クラスをクリックすると、そのクラスのスーパークラス、フィールド、メソッド、コンスタントプールの一覧が表示されます。 また、上の方にはクラスファイルを出力するためのCreate .class Fileがあります。これをクリックすることでクラスファイルが出力されます。

f:id:chiroito:20200719135205p:plain

出力先はjhsdbを起動しているディレクトリ直下です。パッケージ構造をディレクトリとした形で出力されます。移行した画面に表示されているクラス名を選択すると先ほどの画面に戻れます。

得られたクラスファイルは JD や IntelliJ に含まれているデコンパイラを使うことで開けます。注意したいのはフィールドやメソッドを自動生成したことを示す ACC_SYNTHETIC というフラグです。デコンパイラによってはこのフラグがあるフィールドやメソッドを無視する事もあるので注意してください。