JavaでJVM言語を作ってみる(3) - BCELでclassファイルを作る
今回は、javacを使わずにclassファイルを作ってみます。
Apache Commonsのライブラリに"BCEL"というものがあります。これはフルネームで"The Byte Code Engineering Library"というライブラリで、名前のとおりバイトコードをエンジニアリングするライブラリです。これを使えば、Javaのクラスファイルフォーマットを熟知していなくても、バイトコードを直接変更したりすることができます。
変更だけでなく、ゼロからclassファイルを作ることも可能です。
詳しくは、以下のページを参照してください。
- Apache Commons BCEL™ - Byte Code Engineering Library (BCEL)(英語)
- Jakarta BCEL -- バイトコード処理ライブラリ -- - Byte Code Engineering Library (BCEL)
それでは、実際に使ってみます。Apache Commons BCEL(バージョン5.2)を使います。以下、bcel5.2.jarがクラスパスに設定されているものとします。
準備として、以下のJavaファイルをコンパイルして、Empty.classを作っておきます。
// Empty.java class Empty {}
まずは、最小限のクラスファイルを作ってみましょう。
// バージョン2のジェネレータ その1 // class "Adder" final String className = "Adder"; JavaClass base; try { ClassParser classParser = new ClassParser("Empty.class"); base = classParser.parse(); } catch (IOException ex) { throw new RuntimeException(ex); } ClassGen cg = new ClassGen(base); cg.setClassName(className); // generate JavaClass c = cg.getJavaClass(); c.setSourceFileName(className + ".java"); try { c.dump(className + ".class"); } catch (IOException ex) { throw new RuntimeException(ex); }
これを実行すると、"Adder.class"が作られます。
ゼロから新しいJavaClassオブジェクトを生成するのは少し面倒なので、Empty.classをコピーして作っています。ですので、デフォルトコンストラクタしかないクラスになっています。
(確認は省略します。)
追記(2012-02-16):Repository.lookupClassを使ってclasspathに含まれるクラスを指定する方法もあります。どちらにするかはお好みで。
JavaClass base; // = new ClassParser("Empty.class").parse(); try { base = Repository.lookupClass(Empty.class); } catch (ClassNotFoundException ex) { throw new RuntimeException(ex); }
次に、mainメソッドを追加してみましょう。
コメント"// generate"の前に、下記のコードを追加します。
// バージョン2のジェネレータ その2 InstructionFactory factory = new InstructionFactory(cg); // method "main": public static void main(String[] args) InstructionList il = new InstructionList(); il.append(factory.createPrintln("Adder.main")); il.append(InstructionConstants.RETURN); MethodGen mg = new MethodGen(ACC_PUBLIC | ACC_STATIC, Type.VOID, new Type[]{new ArrayType(Type.STRING, 1)}, new String[]{"args"}, "main", className, il, cg.getConstantPool()); mg.setMaxStack(); cg.addMethod(mg.getMethod());
メソッドの処理として、命令コード(instruction codes)をInstructionListに追加していきます。
InstructionFactoryを使うと、手間がかかる手続きをまとめて追加できたりします。この例では、InstructionFactory#createPrintlnという便利なメソッドがあるので、それを使っています。
RETURNを忘れてしまうと、
java.lang.VerifyError: (class: Adder, method: main signature: ([Ljava/lang/String;)V) Falling off the end of the code
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2442)
(以下略)
のように怒られます。
"new MethodGen"の箇所は、ほぼシグネチャの通りです。それ以外に、InstructionListとConstantPoolオブジェクトを設定しています。ConstantPoolについては、後述します。
setMaxStackは、そのメソッドで使用するstackの最大サイズを指定します。これを指定しないと、
java.lang.VerifyError: (class: Adder, method: main signature: ([Ljava/lang/String;)V) Stack size too large
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2442)
(以下略)
のように怒られます。
InstructionListを設定した後で、引数無しのsetMaxStackメソッドを呼べば、計算してくれるみたいです。
最後に、MethodをClassGenにセットします。
これを実行した結果は、下記のJavaソースコードをコンパイルしたのとほぼ同じになります。
// バージョン2 (バージョン0?) class Adder { public static void main(String... args) { System.out.println("Adder.main"); } }
ところで、ConstantPool(コンスタントプール)とは、そのクラスで使用している内部的な定数のセットです。
先ほど作った"Adder.class"を"javap -v"で見てみます。
// 一部省略 class Adder Constant pool: #1 = Methodref #3.#10 // java/lang/Object."<init>":()V #2 = Class #11 // Empty #3 = Class #12 // java/lang/Object #4 = Utf8 <init> #5 = Utf8 ()V #6 = Utf8 Code #7 = Utf8 LineNumberTable #8 = Utf8 SourceFile #9 = Utf8 0.java #10 = NameAndType #4:#5 // "<init>":()V #11 = Utf8 Empty #12 = Utf8 java/lang/Object #13 = Utf8 Adder #14 = Class #13 // Adder #15 = Utf8 java/lang/System #16 = Class #15 // java/lang/System #17 = Utf8 out #18 = Utf8 Ljava/io/PrintStream; #19 = NameAndType #17:#18 // out:Ljava/io/PrintStream; #20 = Fieldref #16.#19 // java/lang/System.out:Ljava/io/PrintStream; #21 = Utf8 println #22 = Utf8 (Ljava/lang/String;)V #23 = NameAndType #21:#22 // println:(Ljava/lang/String;)V #24 = Utf8 java/io/PrintStream #25 = Class #24 // java/io/PrintStream #26 = Methodref #25.#23 // java/io/PrintStream.println:(Ljava/lang/String;)V #27 = Utf8 Adder.main #28 = String #27 // Adder.main #29 = Utf8 main #30 = Utf8 ([Ljava/lang/String;)V #31 = Utf8 args #32 = Utf8 [Ljava/lang/String; #33 = Utf8 LocalVariableTable Adder(); flags: Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 public static void main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #28 // String Adder.main 5: invokevirtual #26 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return
Constant#1は、型がMethodref、引数(?)が#3.#10となっており、コメントには”java/lang/Object."
これは、下記のような構造になっています。
Methodref(Class(Utf8),NameAndType(Utf8,Utf8)) (※Utf8≒文字列) #1 = Methodref(Class("java/lang/Object"),NameAndType("<init>","()V") = java/lang/Object."<init>":()V
他も同様です。
クラスが参照される箇所ではClass型のコンスタントプールが使われ、引数として文字列が使われる箇所ではUtf8のコンスタントプールが使われることになります。(実際はもう少し複雑ですが、詳細は割愛します。)
ちょっと駆け足になってしまいますが、最後にAdderの最終バージョンを作ります。
// バージョン3 (final?) class Adder { static int add(int... a) { int r = 0; for (int i : a) { r += i; } return r; } public static void main(String... args) { int[] a = new int[args.length]; for (int i = 0; i < args.length; i++) { a[i] = Integer.parseInt(args[i]); } System.out.println(add(a)); } }
// バージョン3のジェネレータ import static org.apache.bcel.Constants.*; import java.io.*; import org.apache.bcel.classfile.*; import org.apache.bcel.generic.*; public final class AdderGenerator { private static final String CLASS_NAME = "Adder"; static Method createAddMethod(ConstantPoolGen cp) { // method: static int add(int[] a) InstructionList il = new InstructionList(); MethodGen mg = new MethodGen(ACC_STATIC, Type.INT, new Type[]{new ArrayType(Type.INT, 1)}, new String[]{"a"}, "add", CLASS_NAME, il, cp); // 変数0: 参照型 パラメータ a // 変数1: int total // 変数2: 参照型 無名(パラメータaのコピー?) // 変数3: int 無名(a.lengthの値) // 変数4: int 無名(配列カウンタ) // 変数5: int i mg.addLocalVariable("total", Type.INT, null, null); mg.addLocalVariable("", new ArrayType(Type.INT, 1), null, null); mg.addLocalVariable("", Type.INT, null, null); mg.addLocalVariable("", Type.INT, null, null); mg.addLocalVariable("i", Type.INT, null, null); // instruction codes final InstructionHandle ih1; final InstructionHandle ih2; final BranchHandle bh1; il.append(InstructionConstants.ICONST_0); il.append(InstructionConstants.ISTORE_1); il.append(InstructionConstants.ALOAD_0); il.append(InstructionConstants.ASTORE_2); il.append(InstructionConstants.ALOAD_2); il.append(InstructionConstants.ARRAYLENGTH); il.append(InstructionFactory.createStore(Type.INT, 3)); il.append(InstructionConstants.ICONST_0); il.append(InstructionFactory.createStore(Type.INT, 4)); ih1 = il.append(InstructionFactory.createLoad(Type.INT, 4)); il.append(InstructionFactory.createLoad(Type.INT, 3)); bh1 = il.append(InstructionFactory.createBranchInstruction(IF_ICMPGE, null)); il.append(InstructionConstants.ALOAD_2); il.append(InstructionFactory.createLoad(Type.INT, 4)); il.append(InstructionConstants.IALOAD); il.append(InstructionFactory.createStore(Type.INT, 5)); il.append(InstructionConstants.ILOAD_1); il.append(InstructionFactory.createLoad(Type.INT, 5)); il.append(InstructionConstants.IADD); il.append(InstructionConstants.ISTORE_1); il.append(new IINC(4, 1)); il.append(new GOTO(ih1)); ih2 = il.append(InstructionConstants.ILOAD_1); il.append(InstructionConstants.IRETURN); // append target bh1.setTarget(ih2); // (end of instruction codes) mg.setMaxStack(); return mg.getMethod(); } static Method createMainMethod(ConstantPoolGen cp, InstructionFactory factory) { // method: public static void main(String[] args) InstructionList il = new InstructionList(); MethodGen mg = new MethodGen(ACC_PUBLIC | ACC_STATIC, Type.VOID, new Type[]{new ArrayType(Type.STRING, 1)}, new String[]{"args"}, "main", CLASS_NAME, il, cp); // 変数1: 参照型 a (int[]) // 変数2: int i mg.addLocalVariable("a", new ArrayType(Type.INT, 1), null, null); mg.addLocalVariable("i", Type.INT, null, null); // instruction codes final InstructionHandle ih1; final InstructionHandle ih2; final BranchHandle bh1; il.append(InstructionConstants.ALOAD_0); il.append(InstructionConstants.ARRAYLENGTH); il.append(factory.createNewArray(Type.INT, (short)1)); il.append(InstructionConstants.ASTORE_1); il.append(InstructionConstants.ICONST_0); il.append(InstructionConstants.ISTORE_2); ih1 = il.append(InstructionConstants.ILOAD_2); il.append(InstructionConstants.ALOAD_0); il.append(InstructionConstants.ARRAYLENGTH); bh1 = il.append(InstructionFactory.createBranchInstruction(IF_ICMPGE, null)); il.append(InstructionConstants.ALOAD_1); il.append(InstructionConstants.ILOAD_2); il.append(InstructionConstants.ALOAD_0); il.append(InstructionConstants.ILOAD_2); il.append(InstructionConstants.AALOAD); il.append(factory.createInvoke(Integer.class.getName(), "parseInt", Type.INT, new Type[]{Type.STRING}, INVOKESTATIC)); il.append(InstructionConstants.IASTORE); il.append(new IINC(2, 1)); il.append(new GOTO(ih1)); ih2 = il.append(factory.createGetStatic(System.class.getName(), "out", Type.getType(PrintStream.class))); il.append(InstructionConstants.ALOAD_1); il.append(factory.createInvoke(CLASS_NAME, "add", Type.INT, new Type[]{new ArrayType(Type.INT, 1)}, INVOKESTATIC)); il.append(factory.createInvoke(java.io.PrintStream.class.getName(), "println", Type.VOID, new Type[]{Type.INT}, INVOKEVIRTUAL)); il.append(InstructionFactory.createReturn(Type.VOID)); // append target bh1.setTarget(ih2); // (end of instruction codes) mg.setMaxStack(); return mg.getMethod(); } public static void main(String[] args) { // class "Adder" final String className = "Adder"; JavaClass base; try { ClassParser classParser = new ClassParser("Empty.class"); base = classParser.parse(); } catch (IOException ex) { throw new RuntimeException(ex); } ClassGen cg = new ClassGen(base); cg.setClassName(className); InstructionFactory factory = new InstructionFactory(cg); cg.addMethod(createAddMethod(cg.getConstantPool())); cg.addMethod(createMainMethod(cg.getConstantPool(), factory)); // generate JavaClass c = cg.getJavaClass(); c.setSourceFileName(className + ".java"); try { c.dump(className + ".class"); } catch (IOException ex) { throw new RuntimeException(ex); } } }
いくつか補足。
ローカル変数を使用する場合は、無名も含めてMethodGen#addLocalVariableで設定します。追記(2012-02-15):MethodGen#setMaxLocalsを使えば、setMaxStackと同様に自動的に計算して設定してくれるようです。
分岐がある場合(BranchInstructionのGotoやIfなど)は、ジャンプ先をInstructionHandleで指定します。InstructionHandleはInstructionList#appendの戻り値です。
後方へのジャンプの場合は、ジャンプ先にnullを設定しておき、後からBranchHandle#setTargetで指定できます。
実行してみます。
$ java Adder 9 8 2 19 $
ちゃんと動きました。
このように、javacを使わずにclassファイルを作ることができました。BCELがとても便利なので、概念的なことが理解できれば、細かいところを意識せずにclassファイルを作ることができると思います。
これで、Javaソースコード以外からclassファイルを作る方法は準備できました。
次は、解析器側について考えていきます。
(つづく)