argius note

プログラミング関連

JavaでJVM言語を作ってみる(3) - BCELでclassファイルを作る

今回は、javacを使わずにclassファイルを作ってみます。


Apache Commonsのライブラリに"BCEL"というものがあります。これはフルネームで"The Byte Code Engineering Library"というライブラリで、名前のとおりバイトコードをエンジニアリングするライブラリです。これを使えば、Javaのクラスファイルフォーマットを熟知していなくても、バイトコードを直接変更したりすることができます。
変更だけでなく、ゼロからclassファイルを作ることも可能です。
詳しくは、以下のページを参照してください。


それでは、実際に使ってみます。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."":()V ”と書かれています。
これは、下記のような構造になっています。

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ファイルを作る方法は準備できました。
次は、解析器側について考えていきます。

(つづく)