Javaのバイトコードで遊ぶ (ラムダ式)
以下のメソッドのカリー化を例にバイトコードでラムダ式を作ってみます。
int add(int a, int b, int c);
ラムダ式の生成にはinvokedynamicと、そのブートストラップにLambdaMetafactory.metafactoryを用います。 metafactoryがFunctionalインターフェースの実装クラスを動的に生成すると理解していますが正しく説明できる自信はありません。 むしろこの記事はサンプルコードの備忘録です。。
過去に説明を試みたことはありますけど… ラムダとinvokedynamic
さらっとやっていることを。まず上記のメソッドを以下の3つに分解することを考えます。
IntFunction<IntFunction<IntUnaryOperator>> add$0(){ return a -> add$1(a); } IntFunction<IntUnaryOperator> add$1(int a){ return b -> add$2(a, b); } IntUnaryOperator add$2(int a, int b){ return c -> add(a, b, c); }
それぞれのラムダ式の部分にinvokedynamicを用います。 実行時、metafactoryは(多分)それぞれに対応する以下のようなクラスを生成しますので、これを念頭にvisitInvokeDynamicInsnの引数を設定します。
class Add$0 implements IntFunction<IntFunction<IntUnaryOperator>> { public IntFunction apply(int a){ return add$1(a); } } class Add$1 implements IntFunction<IntUnaryOperator> { // クロージャの環境をキャプチャ private int a; public Add$1(int a){ this.a = a; } public IntUnaryOperator apply(int b){ return add$2(a, b); } } class Add$2 implements IntUnaryOperator { // クロージャの環境をキャプチャ private int a; private int b; public Add$2(int a, int b){ this.a = a; this.b = b; } public int applyAsInt(int c){ return add(a, b, c); } }
前述の分解された3つのメソッドの中身のinvokedynamicの部分は実際には以下のようなコードとして動作します。と解釈しています。
IntFunction<IntFunction<IntUnaryOperator>> add$0(){ return new Add$0(); } IntFunction<IntUnaryOperator> add$1(int a){ return new Add$1(a); } IntUnaryOperator add$2(int a, int b){ return new Add$2(a, b); }
呼び出し元のコードはこのようになります。
add$0().apply(10).apply(20).applyAsInt(30);
サンプルのコードです。
package example; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Handle; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; public class Lambda implements Opcodes { private static byte[] generate(String className) { ClassWriter cw = new ClassWriter(0); MethodVisitor mv; cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, className, null, "java/lang/Object", null); // constructor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv.visitInsn(RETURN); mv.visitMaxs(1, 1); mv.visitEnd(); // int add(int a, int b, int c) { return a + b + c; } mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "add", "(III)I", null, null); mv.visitCode(); mv.visitVarInsn(ILOAD, 0); mv.visitVarInsn(ILOAD, 1); mv.visitInsn(IADD); mv.visitVarInsn(ILOAD, 2); mv.visitInsn(IADD); mv.visitInsn(IRETURN); mv.visitMaxs(2, 3); mv.visitEnd(); // metafactory Handle metafactoryHandle = new Handle(H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory", "metafactory", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;"); // add$0() { return a -> add$1(a); } mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "add$0", "()Ljava/util/function/IntFunction;", null, null); mv.visitCode(); // -- function objectの生成 mv.visitInvokeDynamicInsn( // function objectが実装するメソッドの名前 "apply", // CallSiteのシグニチャ // 引数の型はキャプチャされる値の型 // 戻り値の型はfunction objectが実装するインターフェース "()Ljava/util/function/IntFunction;", // LambdaMetafactory.metafactoryのハンドル metafactoryHandle, // function objectが実装するメソッドの型 Type.getType("(I)Ljava/lang/Object;"), // function objectのメソッド実行時にコールされるべきメソッドのハンドル // 引数はキャプチャされた値とfunction object実行時に渡されたものが使われる new Handle(H_INVOKESTATIC, className, "add$1", "(I)Ljava/util/function/IntFunction;"), // function objectが実装するメソッドの具体化された型 Type.getType("(I)Ljava/util/function/IntFunction;")); mv.visitInsn(ARETURN); mv.visitMaxs(1, 0); mv.visitEnd(); // add$1(int a) { return b -> add$2(a); } mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "add$1", "(I)Ljava/util/function/IntFunction;", null, null); mv.visitCode(); mv.visitVarInsn(ILOAD, 0); // function objectにキャプチャされる // -- function objectの生成 mv.visitInvokeDynamicInsn( // function objectが実装するメソッドの名前 "apply", // CallSiteのシグニチャ // 引数の型はfunction objectにキャプチャされる値の型 // 戻り値の型はfunction objectが実装するインターフェース "(I)Ljava/util/function/IntFunction;", // LambdaMetafactory.metafactoryのハンドル metafactoryHandle, // function objectが実装するメソッドの型 Type.getType("(I)Ljava/lang/Object;"), // function objectのメソッド実行時にコールされるべきメソッドのハンドル // 引数はキャプチャされた値とfunction object実行時に渡されたものが使われる new Handle(H_INVOKESTATIC, className, "add$2", "(II)Ljava/util/function/IntUnaryOperator;"), // function objectが実装するメソッドの具体化された型 Type.getType("(I)Ljava/util/function/IntUnaryOperator;")); mv.visitInsn(ARETURN); mv.visitMaxs(1, 1); mv.visitEnd(); // add$2(int a, int b) { return c -> add(a, b, c); } mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "add$2", "(II)Ljava/util/function/IntUnaryOperator;", null, null); mv.visitCode(); mv.visitVarInsn(ILOAD, 0); // function objectにキャプチャされる mv.visitVarInsn(ILOAD, 1); // function objectにキャプチャされる // -- function objectの生成 mv.visitInvokeDynamicInsn( // function objectが実装するメソッドの名前 "applyAsInt", // CallSiteのシグニチャ // 引数の型はfunction objectにキャプチャされる値の型 // 戻り値の型はfunction objectが実装するインターフェース "(II)Ljava/util/function/IntUnaryOperator;", // LambdaMetafactory.metafactoryのハンドル metafactoryHandle, // function objectが実装するメソッドの型 Type.getType("(I)I"), // function objectのメソッド実行時にコールされるべきメソッドのハンドル // 引数はキャプチャされた値とfunction object実行時に渡されたものが使われる new Handle(H_INVOKESTATIC, className, "add", "(III)I"), // function objectが実装するメソッドの具体化された型 Type.getType("(I)I")); mv.visitInsn(ARETURN); mv.visitMaxs(2, 2); mv.visitEnd(); // main mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); mv.visitCode(); // -- System.out.prinln(add$0().apply(10).apply(20).applyAsInt(30)) mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); // ---- add$0() mv.visitMethodInsn(INVOKESTATIC, className, "add$0", "()Ljava/util/function/IntFunction;", false); // ---- .apply(10) -> checkcast (IntFunction) mv.visitIntInsn(BIPUSH, 10); mv.visitMethodInsn(INVOKEINTERFACE, "java/util/function/IntFunction", "apply", "(I)Ljava/lang/Object;", true); mv.visitTypeInsn(CHECKCAST, "java/util/function/IntFunction"); // ---- .apply(20) -> checkcast (IntUnaryOperator) mv.visitIntInsn(BIPUSH, 20); mv.visitMethodInsn(INVOKEINTERFACE, "java/util/function/IntFunction", "apply", "(I)Ljava/lang/Object;", true); mv.visitTypeInsn(CHECKCAST, "java/util/function/IntUnaryOperator"); // ---- .applyAsInt(30) mv.visitIntInsn(BIPUSH, 30); mv.visitMethodInsn(INVOKEINTERFACE, "java/util/function/IntUnaryOperator", "applyAsInt", "(I)I", true); // ---- .println() mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false); mv.visitInsn(RETURN); mv.visitMaxs(3, 1); mv.visitEnd(); cw.visitEnd(); return cw.toByteArray(); } public static void main(String... args) throws Exception { String className = "Lambda"; byte[] bytes = generate(className); ExampleUtil.execMain(className, bytes); ExampleUtil.write(className, bytes); } }
Javaのバイトコードで遊ぶ (フィールドとメソッドの定義)
まずはフィールドの定義について。 ClassWriter.visitFieldを用います。 こんな感じです。
// public static int a = 100; cw.visitField(ACC_PUBLIC + ACC_STATIC, "a", "I", null, 100); // public static String; cw.visitField(ACC_PUBLIC + ACC_STATIC, "b", "Ljava/lang/String;", null, null); // public int c; cw.visitField(ACC_PUBLIC, "c", "I", null, null);
各フィールドの値はstaticブロックやコンストラクタで初期化するのが基本路線ですが、 staticなフィールドの場合はintやStringなどの型なら初期値を第4引数に指定可能です。
続いてメソッドの定義です。 ClassWriter.visitMethodで開始して、visitCode〜メソッドの内容〜visitMaxs〜visitEndという流れです。
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); mv.visitCode(); // // ここにメソッドの内容 // mv.visitMaxs(2, 1); mv.visitEnd();
staticブロックはメソッド名を<clinit>
、コンストラクタはメソッド名を<init>
にします。
// static {} mv = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null); // constructor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
ジェネリック型や例外を使う場合は、visitMethodの第3引数、第4引数を使います。この記事では触れません。面倒なので。。
最後にサンプルコード。
package example; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; public class FieldAndMethod implements Opcodes { private static byte[] generate(String className) { ClassWriter cw = new ClassWriter(0); MethodVisitor mv; cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, className, null, "java/lang/Object", null); // fields // public static int a = 100; cw.visitField(ACC_PUBLIC + ACC_STATIC, "a", "I", null, 100); // public static String; cw.visitField(ACC_PUBLIC + ACC_STATIC, "b", "Ljava/lang/String;", null, null); // public int c; cw.visitField(ACC_PUBLIC, "c", "I", null, null); // static block mv = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null); mv.visitCode(); // -- System.out.println(a) mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitFieldInsn(GETSTATIC, className, "a", "I"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false); // -- b = "ABC" mv.visitLdcInsn("ABC"); mv.visitFieldInsn(PUTSTATIC, className, "b", "Ljava/lang/String;"); mv.visitInsn(RETURN); mv.visitMaxs(2, 0); mv.visitEnd(); // constructor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); // -- this.c = 200 mv.visitVarInsn(ALOAD, 0); mv.visitIntInsn(SIPUSH, 200); mv.visitFieldInsn(PUTFIELD, className, "c", "I"); mv.visitInsn(RETURN); mv.visitMaxs(3, 1); mv.visitEnd(); // main mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); mv.visitCode(); // -- System.out.println(b) mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitFieldInsn(GETSTATIC, className, "b", "Ljava/lang/String;"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitInsn(RETURN); mv.visitMaxs(2, 1); mv.visitEnd(); cw.visitEnd(); return cw.toByteArray(); } public static void main(String... args) throws Exception { String className = "FieldAndMethod"; byte[] bytes = generate(className); ExampleUtil.execMain(className, bytes); ExampleUtil.write(className, bytes); } }
Javaのバイトコードで遊ぶ (例外処理)
例外の送出と捕捉を試してみます。
try-catchブロックの定義にはvisitTryCatchBlockを用います。
visitTryCatchBlock(Label start, Label end, Label handler, String type)
startとendは、例外ハンドラのスコープの開始と終了を示します。 handlerは例外ハンドラのラベルです。 typeは捕捉する例外クラスです。
これを使うと、クラスファイルにException tableなるものが出力されるようになります。
Exception table: from to target type 0 16 19 Class java/lang/IllegalArgumentException 0 16 25 Class java/lang/Exception
あと自分がハマったところですが… 例外ハンドラの開始時には、スタックに例外インスタンスが積まれています。 これは使わない場合でもPOPするなり、ローカル変数に退避するなりしてスタックから除いておくのが無難です。 無視して放置していたら、後続の処理時にstack mapの状態が違う、というエラーが出てハマりました。
以下サンプルのコードです。
package example; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; public class TryCatch implements Opcodes { private static byte[] generate(String className) { ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); MethodVisitor mv; cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, className, null, "java/lang/Object", null); // constructor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv.visitInsn(RETURN); mv.visitMaxs(1, 1); mv.visitEnd(); // main mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); mv.visitCode(); Label from = new Label(); Label to = new Label(); Label illegalArgExHandler = new Label(); Label anyExHandler = new Label(); Label finallyHandler = new Label(); // try-catch block definitions mv.visitTryCatchBlock(from, to, illegalArgExHandler, "java/lang/IllegalArgumentException"); mv.visitTryCatchBlock(from, to, anyExHandler, "java/lang/Exception"); // from mv.visitLabel(from); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("try-catch example"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); // throw mv.visitTypeInsn(NEW, "java/lang/Exception"); mv.visitInsn(DUP); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Exception", "<init>", "()V", false); mv.visitInsn(ATHROW); // to mv.visitLabel(to); mv.visitJumpInsn(GOTO, finallyHandler); // catch IllegalArgumentException mv.visitLabel(illegalArgExHandler); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/IllegalArgumentException", "printStackTrace", "()V", false); mv.visitJumpInsn(GOTO, finallyHandler); // catch Exception mv.visitLabel(anyExHandler); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Exception", "printStackTrace", "()V", false); mv.visitJumpInsn(GOTO, finallyHandler); // finally mv.visitLabel(finallyHandler); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("finally"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitInsn(RETURN); mv.visitMaxs(2, 1); mv.visitEnd(); cw.visitEnd(); return cw.toByteArray(); } public static void main(String... args) throws Exception { String className = "TryCatch"; byte[] bytes = generate(className); ExampleUtil.execMain(className, bytes); ExampleUtil.write(className, bytes); } }
Javaのバイトコードで遊ぶ (stack map frame)
stack map frameについては以下を読みました。
- https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.4
- http://download.forge.objectweb.org/asm/asm4-guide.pdf
- java - Is there a better explanation of stack map frames? - Stack Overflow
これらを読んで、次のように理解しました。 stack map frameとは、バイトコードのある位置におけるローカル変数とスタックの型の状態を示すもので、VMの各命令が扱う値が正しい型かを検証するために使われます。 全ての型を推論することは難しく、クラスローディングを遅くしてしまうため、その解決のために導入されたようです。 クラスファイルのサイズが大きくなりすぎないようフレームで型を明示する位置はジャンプのターゲットに絞っており、またフレームの差分で指定できるようにしています。
各stack map frameはバイトコードのある位置(bytecode offset)における型の状態を表します。 bytecode offsetを算出するために各stack map frameはoffset_deltaという値を持ちます。 あるstack map frameが示すbytecode offsetは、1つ前のstack map frameのbytecode offset+offset_delta+1で求められます。 ただし1つ前のframeがinitial frame(メソッド開始時のframe)の場合(つまりbytecode offset=0)、offset_deltaの値がそのままbytecode offsetになります。
次にstack map frameの種類についてです。
same_frame
frameの種類を示すframe_typeの値が[0-63]のものがこれに該当します。 このframeは1つ前のframeとローカル変数が同じでスタックが空であることを意味します。 offset_deltaはframe_typeと同じ値になります。(frame_type=0ならoffset_delat=0)
same_locals_1_stack_item_frame
frame_typeの値が[64-127]のものがこれに該当します。 このframeは1つ前のframeとローカル変数が同じでスタックに1つ値が積まれていることを意味します。 offset_deltaはframe_type-64になります。(frame_type=64ならoffset_delat=0)
same_locals_1_stack_item_frame_extended
frame_typeの値が247のものがこれに該当します。(frame_type=[128-246]は将来のために予約されています) このframeはsame_locals_1_stack_item_frameと同じで、1つ前のframeとローカル変数が同じでスタックに1つ値が積まれていることを意味します。 明示的にoffset_deltaを与える必要がある点がsame_locals_1_stack_item_frameと異なります。
chop_frame
frame_typeの値が[248-250]のものがこれに該当します。 このframeはスタックが空で、1つ前のframeからいくつかのローカル変数が削除されていることを意味します。 削除されているローカル変数の数は、251-frame_typeで求められます。(frame_type=248なら削除されたローカル変数は3つ) 明示的にoffset_deltaを与える必要があります。
same_frame_extended
frame_typeの値が251のものがこれに該当します。 意味するところはsame_frameと同じですが、明示的にoffset_deltaを与える必要がある点で異なります。
append_frame
frame_typeの値が[252-254]のものがこれに該当します。 このframeはスタックが空で、1つ前のframeからいくつかのローカル変数が追加されていることを意味します。 追加されているローカル変数の数は、frame_type-251で求められます。(frame_type=252なら削除されたローカル変数は1つ) 明示的にoffset_deltaを与える必要があります。
full_frame
frame_typeの値が255のものがこれに該当します。 ローカル変数、スタックの状態を全て明示的に示す必要があります。 offset_deltaも明示します。
実際のバイトコードにてstack map frameを確認します。javaコードは以下です。
int a = 1; if (a > 0) { System.out.println("x"); } if (a > 0) { float b = 2.0F; System.out.println("y"); } else { long c = 3L; if (c > 0) { double d = 4.0; } System.out.println("z"); }
これは以下のバイトコードになります。
stack=4, locals=6, args_size=1 0: iconst_1 1: istore_1 2: iload_1 3: ifle 14 6: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream; 9: ldc #21 // String x 11: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 14: iload_1 15: ifle 31 18: fconst_2 19: fstore_2 20: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream; 23: ldc #29 // String y 25: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 28: goto 54 31: ldc2_w #31 // long 3l 34: lstore_2 35: lload_2 36: lconst_0 37: lcmp 38: ifle 46 41: ldc2_w #33 // double 4.0d 44: dstore 4 46: getstatic #15 // Field java/lang/System.out:Ljava/io/PrintStream; 49: ldc #35 // String z 51: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 54: return ... StackMapTable: number_of_entries = 4 frame_type = 252 /* append */ offset_delta = 14 locals = [ int ] frame_type = 16 /* same */ frame_type = 252 /* append */ offset_delta = 14 locals = [ long ] frame_type = 250 /* chop */ offset_delta = 7
それでは順にstack map frameを見ていきます。
frame_type = 252 /* append */ offset_delta = 14 locals = [ int ]
initial frameの次のframeのためbytecode offsetはoffset_deltaである14。
このbytecode offsetは3: ifle 14
からのジャンプのターゲットと一致します。
14: iload_1
においてローカル変数にintが1つ(252-251)増えていることを示します。
frame_type = 16 /* same */
bytecode offsetは31(14+16+1)です。
31: ldc2_w #31
においてローカル変数に前のframeから変化がないことを示します。
frame_type = 252 /* append */ offset_delta = 14 locals = [ long ]
bytecode offsetは46(31+14+1)です。
46: getstatic #15
においてローカル変数にlongが1つ(252-251)増えていることを示します。
frame_type = 250 /* chop */ offset_delta = 7
bytecode offsetは54(46+7+1)です。
54: return
においてローカル変数が1つ(251-250)減っていることを示します。
このstack map frameをASMで指定するにはMethodVisitor.visitFrameを用います。
visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack)
typeには、F_NEW, F_FULL, F_SAME, F_SAME1, F_APPEND, F_CHOPを指定できます。
前述のframe_typeとは若干異なりますが、名前から対応付けはわかります。 F_NEWはClassReaderで使うもののようなのでClassWriterでは不要。
nLocal以降はtypeに応じてスタックとローカル変数の内容を指定します。
このvisitFrameを使ってFizzBuzzを書き直してみました。
package example; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; public class FizzBuzzWithFrame implements Opcodes { private static byte[] generate(String className) { ClassWriter cw = new ClassWriter(0); MethodVisitor mv; cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, className, null, "java/lang/Object", null); // constructor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv.visitInsn(RETURN); mv.visitMaxs(1, 1); mv.visitEnd(); // main mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); mv.visitCode(); Label loopBegin = new Label(); Label loopNext = new Label(); Label loopEnd = new Label(); Label fizzBranch = new Label(); Label buzzBranch = new Label(); Label elseBranch = new Label(); // i = 1 int _i = 1; mv.visitInsn(ICONST_1); mv.visitVarInsn(ISTORE, _i); // max = 100 int _max = 2; mv.visitIntInsn(BIPUSH, 100); mv.visitVarInsn(ISTORE, _max); // -- loopBegin -- mv.visitLabel(loopBegin); // ループ開始時と2回目以降でローカル変数が異なる // (j, kが追加)のでF_APPEND mv.visitFrame(F_APPEND, 2, new Object[] { Opcodes.INTEGER, Opcodes.INTEGER }, 0, null); // j = i % 3 int _j = 3; mv.visitVarInsn(ILOAD, _i); mv.visitInsn(ICONST_3); mv.visitInsn(IREM); mv.visitVarInsn(ISTORE, _j); // k = i % 5 int _k = 4; mv.visitVarInsn(ILOAD, _i); mv.visitInsn(ICONST_5); mv.visitInsn(IREM); mv.visitVarInsn(ISTORE, _k); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); // if j == 0 && k == 0 then print "FizzBuzz" mv.visitVarInsn(ILOAD, _j); mv.visitJumpInsn(IFNE, fizzBranch); mv.visitVarInsn(ILOAD, _k); mv.visitJumpInsn(IFNE, fizzBranch); mv.visitLdcInsn("FizzBuzz"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitJumpInsn(GOTO, loopNext); // if j == 0 then print "Fizz" mv.visitLabel(fizzBranch); // メソッド開始時のフレームとだいぶ差がある // (ローカル変数にi, max, j, k、 スタックにSystem.out)ので // F_FULLで全部指定 mv.visitFrame(F_FULL, 5, new Object[] { "[Ljava/lang/String;", Opcodes.INTEGER, Opcodes.INTEGER, Opcodes.INTEGER, Opcodes.INTEGER }, 1, new Object[] { "java/io/PrintStream" }); mv.visitVarInsn(ILOAD, _j); mv.visitJumpInsn(IFNE, buzzBranch); mv.visitLdcInsn("Fizz"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitJumpInsn(GOTO, loopNext); // if k == 0 then print "Buzz" mv.visitLabel(buzzBranch); // 前フレームと差はないがスタックに1つ値があるのでF_SAME1 mv.visitFrame(F_SAME1, 0, null, 1, new Object[] { "java/io/PrintStream" }); mv.visitVarInsn(ILOAD, _k); mv.visitJumpInsn(IFNE, elseBranch); mv.visitLdcInsn("Buzz"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitJumpInsn(GOTO, loopNext); // else print i mv.visitLabel(elseBranch); // 前フレームと差はないがスタックに1つ値があるのでF_SAME1 mv.visitFrame(F_SAME1, 0, null, 1, new Object[] { "java/io/PrintStream" }); mv.visitVarInsn(ILOAD, _i); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false); // -- loopNext -- mv.visitLabel(loopNext); // 前フレームと同一、スタックも空なのでF_SAME mv.visitFrame(F_SAME, 0, null, 0, null); // i++ mv.visitIincInsn(_i, 1); // if i <= max then goto loopBegin mv.visitVarInsn(ILOAD, _i); mv.visitVarInsn(ILOAD, _max); mv.visitJumpInsn(IF_ICMPLE, loopBegin); // -- loopEnd -- mv.visitLabel(loopEnd); mv.visitInsn(RETURN); mv.visitMaxs(2, 5); mv.visitEnd(); cw.visitEnd(); return cw.toByteArray(); } public static void main(String... args) throws Exception { String className = "FizzBuzzWithFrame"; byte[] bytes = generate(className); ExampleUtil.execMain(className, bytes); ExampleUtil.write(className, bytes); } }
Javaのバイトコードで遊ぶ (条件分岐と繰り返し)
条件分岐と繰り返しはジャンプを使います。
ジャンプ先の指定はLabelを用います。ジャンプ元ではインスタンス化したLabelを指定します。 visitLabelにLabelインスタンスを渡せば、そこがラベルになります。
ジャンプ元において、スタックの値と0やNULLを比較する場合は、IFEQ, IFNE, IFLT, IFGT, IFNULLなどの命令を用います。 スタックに積まれている2つの値を比較する場合は、IF_ICMPEQ, IF_ICMPNE, IF_ICMPLT, IF_ICMPGTなどの命令を用います。 単純にジャンプするだけのGOTOもあります。
また、ジャンプを使う場合は、ジャンプ先の命令の直前で、visitFrameによりstack map frameを明示する必要があります。
具体的にはフレーム(スタックとローカル変数のスロット)の中身を教えてやります。
JVMがクラスを素早く検証するためにJava6で導入され、Java7から必須になったようです。
ClassWriterのコンストラクタにClassWriter.COMPUTE_FRAMES
を指定すればASMが自動で算出してくれます。その場合はvisitFrameは不要です。
なおCOMPUTE_FRAMESを指定する場合はvisitMaxsも無視されるとのことです。 これはASMがフレームだけでなく、スタックやローカル変数も計算してくれるからなのですが、なぜかvisitMaxsは呼んでおかないとエラーが出ます。引数は適当な値でも動くように見えますが。
定番ですが、FizzBuzzを書いてみました。
package example; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; public class FizzBuzz implements Opcodes { private static byte[] generate(String className) { ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); MethodVisitor mv; cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, className, null, "java/lang/Object", null); // constructor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv.visitInsn(RETURN); mv.visitMaxs(1, 1); mv.visitEnd(); // main mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); mv.visitCode(); Label loopBegin = new Label(); Label loopNext = new Label(); Label loopEnd = new Label(); Label fizzBranch = new Label(); Label buzzBranch = new Label(); Label elseBranch = new Label(); // i = 1 int _i = 1; mv.visitInsn(ICONST_1); mv.visitVarInsn(ISTORE, _i); // max = 100 int _max = 2; mv.visitIntInsn(BIPUSH, 100); mv.visitVarInsn(ISTORE, _max); // -- loopBegin -- mv.visitLabel(loopBegin); // j = i % 3 int _j = 3; mv.visitVarInsn(ILOAD, _i); mv.visitInsn(ICONST_3); mv.visitInsn(IREM); mv.visitVarInsn(ISTORE, _j); // k = i % 5 int _k = 4; mv.visitVarInsn(ILOAD, _i); mv.visitInsn(ICONST_5); mv.visitInsn(IREM); mv.visitVarInsn(ISTORE, _k); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); // if j == 0 && k == 0 then print "FizzBuzz" mv.visitVarInsn(ILOAD, _j); mv.visitJumpInsn(IFNE, fizzBranch); mv.visitVarInsn(ILOAD, _k); mv.visitJumpInsn(IFNE, fizzBranch); mv.visitLdcInsn("FizzBuzz"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitJumpInsn(GOTO, loopNext); // if j == 0 then print "Fizz" mv.visitLabel(fizzBranch); mv.visitVarInsn(ILOAD, _j); mv.visitJumpInsn(IFNE, buzzBranch); mv.visitLdcInsn("Fizz"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitJumpInsn(GOTO, loopNext); // if k == 0 then print "Buzz" mv.visitLabel(buzzBranch); mv.visitVarInsn(ILOAD, _k); mv.visitJumpInsn(IFNE, elseBranch); mv.visitLdcInsn("Buzz"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); mv.visitJumpInsn(GOTO, loopNext); // else print i mv.visitLabel(elseBranch); mv.visitVarInsn(ILOAD, _i); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false); // -- loopNext -- mv.visitLabel(loopNext); // i++ mv.visitIincInsn(_i, 1); // if i <= max then goto loopBegin mv.visitVarInsn(ILOAD, _i); mv.visitVarInsn(ILOAD, _max); mv.visitJumpInsn(IF_ICMPLE, loopBegin); // -- loopEnd -- mv.visitLabel(loopEnd); mv.visitInsn(RETURN); mv.visitMaxs(2, 5); mv.visitEnd(); cw.visitEnd(); return cw.toByteArray(); } public static void main(String... args) throws Exception { String className = "FizzBuzz"; byte[] bytes = generate(className); ExampleUtil.execMain(className, bytes); ExampleUtil.write(className, bytes); } }
Javaのバイトコードで遊ぶ (四則演算)
加算や減算などの基本的な数値演算をやってみます。
まずはintやfloatの計算。 定数値をスタックに積む命令にはバリエーションがあります。 値の大小に応じて、BIPUSHやSIPUSHを使う方法や、visitLdcInsnで任意の値を指定することができます。 visitLdcInsnは渡された値の型に応じたコードを生成してくれます。 また0や1に対しては、ICONST_0やICONST_1といった専用の命令も用意されています。
// intの演算 // 123 + 456 mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitIntInsn(BIPUSH, 123); // Byte.MIN_VALUE - Byte.MAX_VALUE の範囲の値 mv.visitIntInsn(SIPUSH, 456); // SHORT.MIN_VALUE - SHORT.MAX_VALUE の範囲の値 mv.visitInsn(IADD); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false); // intの演算 // 123 - 456 mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitIntInsn(SIPUSH, 123); mv.visitLdcInsn(456); // このような指定も可能 mv.visitInsn(ISUB); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false); // floatの演算 // 123.0 / 456.0 mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn(123.0F); mv.visitLdcInsn(456.0F); mv.visitInsn(FDIV); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(F)V", false);
次にlongやdoubleの計算。 intやfloatと異なり、1つの値に対してスタックやローカル変数領域において 2スロット消費しますので、visitMaxsの指定には注意が必要です。 またキャストも用いてみました。
// longの演算 // Integer.MAX_VALUE + 1 // 1つの数が2スロット消費するので注意 mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn(Integer.MAX_VALUE); mv.visitInsn(I2L); // int -> long のキャスト mv.visitInsn(LCONST_1); mv.visitInsn(LADD); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false); // doubleの演算 // 123.321 * 456.654 // 1つの数が2スロット消費するので注意 mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn(123.321); mv.visitLdcInsn(456.654); mv.visitInsn(DMUL); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(D)V", false);
実行すると次の結果が表示されます。
579 -333 0.26973686 2147483648 56315.027934
最後にコードの全体を載せておきます。
Calculation
package example; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; public class Calculation implements Opcodes { private static byte[] generate(String className) { ClassWriter cw = new ClassWriter(0); MethodVisitor mv; cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, className, null, "java/lang/Object", null); // constructor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv.visitInsn(RETURN); mv.visitMaxs(1, 1); mv.visitEnd(); // main mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); mv.visitCode(); // intの演算 // 123 + 456 mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitIntInsn(BIPUSH, 123); // Byte.MIN_VALUE - Byte.MAX_VALUE の範囲の値 mv.visitIntInsn(SIPUSH, 456); // SHORT.MIN_VALUE - SHORT.MAX_VALUE の範囲の値 mv.visitInsn(IADD); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false); // intの演算 // 123 - 456 mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitIntInsn(SIPUSH, 123); mv.visitLdcInsn(456); // このような指定も可能 mv.visitInsn(ISUB); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false); // floatの演算 // 123.0 / 456.0 mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn(123.0F); mv.visitLdcInsn(456.0F); mv.visitInsn(FDIV); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(F)V", false); // longの演算 // Integer.MAX_VALUE + 1 // 1つの数が2スロット消費するので注意 mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn(Integer.MAX_VALUE); mv.visitInsn(I2L); // int -> long のキャスト mv.visitInsn(LCONST_1); mv.visitInsn(LADD); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false); // doubleの演算 // 123.321 * 456.654 // 1つの数が2スロット消費するので注意 mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn(123.321); mv.visitLdcInsn(456.654); mv.visitInsn(DMUL); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(D)V", false); mv.visitInsn(RETURN); mv.visitMaxs(5, 1); mv.visitEnd(); cw.visitEnd(); return cw.toByteArray(); } public static void main(String... args) throws Exception { String className = "Calculation"; byte[] bytes = generate(className); ExampleUtil.execMain(className, bytes); ExampleUtil.write(className, bytes); } }
Javaのバイトコードで遊ぶ (メソッド呼び出し)
INVOKEDYNAMIC以外のINVOKE系の命令を使ってみます。
INVOKESTATICはstaticメソッドを呼び出します。引数を順にスタックに積んでから呼び出します。 呼び出し後は引数がスタックから取り除かれ、戻り値がある場合はスタックに積まれています。
// System.getProperty("java.home") mv.visitLdcInsn("java.home"); mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "getProperty", "(Ljava/lang/String;)Ljava/lang/String;", false);
INVOKESPECIALはコンストラクタやプライベートメソッド、スーパークラスのインスタンスメソッドを呼び出します。 以下はコンストラクタ呼び出しの例です。
// new ArrayList(); mv.visitTypeInsn(NEW, "java/util/ArrayList"); mv.visitInsn(DUP); mv.visitMethodInsn(INVOKESPECIAL, "java/util/ArrayList", "<init>", "()V", false);
INVOKESPECIALを呼ぶ前にNEWを使ってインスタンスを生成し、DUPでインスタンスのポインタ(という表現が適切か不明)をスタック上で複製しています。 コンストラクタやインスタンスメソッドはインスタンス自体を最初の引数としてスタックに積む必要があります。 複製しておかないとコンストラクタ呼び出しでスタックから取り除かれ、その後の処理でインスタンスを使えないためです。
INVOKEVIRTUALはインスタンスのメソッドを呼び出します。 最初にインスタンス自体をスタックに積んだ後、引数を順に積んでから、INVOKEVIRTUALで呼び出します。
// ArrayList l1; // l1.add(0, "ABC"); mv.visitVarInsn(ALOAD, l1); mv.visitInsn(ICONST_0); mv.visitLdcInsn("ABC"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/util/ArrayList", "add", "(ILjava/lang/Object;)V", false);
INVOKEINTERFACEはインターフェースのメソッドを呼び出します。
// List l2; // l2.add(1, "XYZ"); mv.visitVarInsn(ALOAD, l2); mv.visitInsn(ICONST_1); mv.visitLdcInsn("XYZ"); mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "add", "(ILjava/lang/Object;)V", true);
インターフェースのメソッドを呼び出す場合に第5引数にtrueを指定します。 Java8からインターフェースがstaticメソッドを持てるようになりました。 INVOKESTATICで第5引数がtrueの例です。
// l2.sort(Comparator.reverseOrder()); mv.visitVarInsn(ALOAD, l2); mv.visitMethodInsn(INVOKESTATIC, "java/util/Comparator", "reverseOrder", "()Ljava/util/Comparator;", true); mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "sort", "(Ljava/util/Comparator;)V", true);
最後にコードの全体を載せておきます。
package example; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; public class Invoke implements Opcodes { private static byte[] generate(String className) { ClassWriter cw = new ClassWriter(0); MethodVisitor mv; cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, className, null, "java/lang/Object", null); // constructor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv.visitInsn(RETURN); mv.visitMaxs(1, 1); mv.visitEnd(); // main mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null); mv.visitCode(); // System.out.println(System.getProperty("java.home")); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("java.home"); mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "getProperty", "(Ljava/lang/String;)Ljava/lang/String;", false); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V", false); // ArrayList l1 = new ArrayList(); int l1 = 1; mv.visitTypeInsn(NEW, "java/util/ArrayList"); mv.visitInsn(DUP); mv.visitMethodInsn(INVOKESPECIAL, "java/util/ArrayList", "<init>", "()V", false); mv.visitVarInsn(ASTORE, l1); // l1.add(0, "ABC"); mv.visitVarInsn(ALOAD, l1); mv.visitInsn(ICONST_0); mv.visitLdcInsn("ABC"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/util/ArrayList", "add", "(ILjava/lang/Object;)V", false); // List l2 = l1; int l2 = 2; mv.visitVarInsn(ALOAD, l1); mv.visitVarInsn(ASTORE, l2); // l2.add(1, "XYZ"); mv.visitVarInsn(ALOAD, l2); mv.visitInsn(ICONST_1); mv.visitLdcInsn("XYZ"); mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "add", "(ILjava/lang/Object;)V", true); // System.out.println(l2); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitVarInsn(ALOAD, l2); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V", false); // l2.sort(Comparator.reverseOrder()); mv.visitVarInsn(ALOAD, l2); mv.visitMethodInsn(INVOKESTATIC, "java/util/Comparator", "reverseOrder", "()Ljava/util/Comparator;", true); mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "sort", "(Ljava/util/Comparator;)V", true); // System.out.println(l2); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitVarInsn(ALOAD, l2); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V", false); mv.visitInsn(RETURN); mv.visitMaxs(3, 3); mv.visitEnd(); cw.visitEnd(); return cw.toByteArray(); } public static void main(String... args) throws Exception { String className = "Invoke"; byte[] bytes = generate(className); ExampleUtil.execMain(className, bytes); ExampleUtil.write(className, bytes); } }