<% Include("/hsphere/local/home/terraint/common.phps");%> Jakarta BCEL -- バイトコード処理ライブラリ -- - Byte Code Engineering Library (BCEL)
BCEL

BCEL

ドキュメント

ダウンロード

参加するには

日本語訳 (Translations)

オリジナル

摘要

プログラム言語Java及び関連した実行環境(Java Virtual Machine, JVM)の拡張と改良に関しては、数多くの研究プロジェクトがあり、数多くの提案がなされています。例えば、Javaへのパラメタタイプの追加、Aspect-Oriented Programmingの実装、洗練された静的分析の実施、ランタイム環境の改善、を試みているプロジェクトがあります。

Javaクラスはバイトコードと呼ばれるポータブルなバイナリクラスファイルにコンパイルされるため、これらの改善を行う際、新たなコンパイラを作ったりJVMを変更するという方法ではなく、バイトコードを変換していく方法のほうが、手ごろでプラットフォーム非依存なやり方です。これらの変換は、コンパイル後あるいは(クラスの)ロード時に行ってもよいでしょう。多くのプログラマは、自分達で作った特殊なバイトコード操作ツールを用いてこの事を実現していますが、再利用する範囲が限られてしまっています。

必要なクラスファイルの変換を扱うため、開発者が変換機能を実装する為の手軽なAPIをご紹介します。


1 はじめに

Java言語は、広く一般に知れ渡るようになり、多くの研究プロジェクトが言語やランタイム環境の改善を模索しています。新しいコンセプトの元に言語拡張をするのは確かに望ましい事なのですが、ユーザからはその実現が見えない形であるべきでしょう。幸いにも、Java Virtual Machine (JVM)のコンセプトは、比較的難なくユーザから見えない拡張実現を行うことにありますので、その実現は容易でしょう。

Javaの目指す言語は、理解しやすく少ない命令のセット(バイトコード)であるインタプリタ形式の言語でありますから、開発者は、とてもエレガントなやり方で自分なりのコンセプトを具体化しテストする事ができます。実行時にクラスファイルを動的にロードしバイトコードをVirtual Machineに送るファイルシステム上のクラスローダーを、プラグインを書くことで置き換えることが可能です(see section )。クラスローダーは、このように、ロード処理を途中で中断し、JVMが実際に起動する前にクラスファイルを変換する為に使用されるのです。元のクラスファイルは常に変換されないでいる一方、クラスローダーは実行毎に再構成されたり、あるいは、動的に動作するかもしれません。

以前はJavaClassという名称で知れわたっていた、BCEL API (バイトコードエンジニアリングライブラリ)は、Javaクラスファイルの静的な分析と動的な生成、あるいはJavaクラスファイルの変換を行うツールキットです。これによって、開発者はJavaクラスファイル形式の内部詳細を扱う事無く、ハイレベルに抽象化された望ましい機能を実装する事が可能となります。 BCELは完全にJavaのみで書かれており、 Apache Software Licenseの条件の下、フリーで入手可能です。

このマニュアルは、以下で構成されます: Java Virtual Machineの簡単な説明をします。 section 2. Section 3 のクラスファイル形式で BCEL API を紹介します。 Section 4 では、典型的なアプリケーション領域とプロジェクト例をご紹介します。Appendix には、この文書のメインで書くには長すぎるコード例があります。全てのサンプルは、頒布版ダウンロードに含まれています。


2 Java仮想マシン

Java Virtual MachineやJavaクラスファイル形式に既に精通している方は、このセクションを飛ばしてsection 3に進んで構いません。

Java言語で書かれたプログラムは、バイトコード.と呼ばれるコンパクトなバイナリ形式にコンパイルされます。全てのクラスは、関連するデータ及びバイトコード命令を含む1つのクラスファイルによって表現されます。これらのファイルは、インタプリタ(Java Virtual Machine - 別名JVM) によって動的にロードされ、実行されます。

Figure 1は、コンパイルとJavaクラス実行の一連の流れを示しています:ソースファイル(HelloWorld.java)はJavaクラスファイル(HelloWorld.class),にコンパイルされ、バイトコードインタプリタによってロードされ、処理が実行されます。研究者達は処理が実際に行われる前にクラスファイルを変換する事で追加機能を実装したいと思うでしょう。この、アプリケーションエリアは、この記事の主要トピックの一つです。


Figure 1: Javaクラスのコンパイルと実行

一般用語である”Java"に実際は2つの意味が含まれている事に注意してください:一方で、プログラム言語としての”Java"という意味 - 他方、Java Virtual Machine(Java言語にのみ照準をあわせてはおらず、多言語でも同様に使われるかもしれません)です。この文書は、Java言語に精通しており、Virtual Machineの一般的な理解をしている方を想定しています。


2.1 Javaクラスファイル形式(Java class file format)

Javaクラスファイル形式のデザインの大要やそれに関連したバイトコード命令を完全に説明する事は、この文書の扱う範囲を超えます。この文書の後半を理解するのに必要な簡潔な説明だけ行います。クラスファイル形式やバイトコード命令セットに関しては、Java仮想マシン仕様書に詳細が記述されています。 特に、Java仮想マシンが実行時にチェックを行うセキュリティ系制約は取り扱いません。 例:ByteCode Verifier

Figure 2では、Javaクラスファイルの内容の例を単純化してご紹介しています: "マジックナンバー"(0xCAFEBABE)とバージョン番号を含むヘッダ(Header)から始まります。 次に来るのが、コンスタントプール(constant pool)です。これは、実行可能形式のテキスト・セグメントと 大まかに考えてもよいでしょう。次が、クラスのアクセス権(access rights)で、ビットマスクでエンコードされています。 次が、クラスによって実装されるインターフェースのリスト(Implemented Interface)です。最後に、 クラス属性(class attributes)です。例:SourceFile属性値は、ソースファイルの 名前を表します。 ”属性”は、追加的なユーザー定義の情報をクラスファイルデータ構造に入れる方式を提示します。 例えば、独自のクラスローダーが、変換を実行する為にそれらの属性データを評価する事もあり得るでしょう。 JVM仕様の中で、仮想マシン実装で"unknown"(例:ユーザー定義の)属性を無視すべきか否かを規定しています。


Figure 2: Javaクラスファイル形式

実行時に動的にクラス・フィールド・メソッドへのシンボル参照を解決する必要のある全ての情報が、 文字列定数でコード化されている為、実際、コンスタントプールは標準のクラスファイルの 大部分(およそ60%)を占めることになります。事実、この事によって、 コンスタントプールは、コード操作関連の標的になりやすくなっています。 バイトコード命令それ自体は、(クラスファイルのうちの)たったの12%です。

右上のボックス(ズームしたところ)は、コンスタントプールの抜粋です。 その下の丸で囲まれたボックスでは、例示したクラスのメソッド内にある命令を描写しています。 この中の命令は、(以下の)よく知られたコードをストレートに翻訳したものを表しています:

System.out.println("Hello, world");

最初の命令は、java.lang.Systemクラスのoutフィールドの内容を オペランド(演算子)スタックに読み込みます。これは、java.io.PrintStream クラスのインスタンスです。 ldc ("Load constant") は、スタックに、"Hello world"という文字列のリファレンスをpushします(訳注:push, pop等今後出て来た場合、スタック処理のpush, popをイメージするようにして下さい)。 次の命令は、インスタンス・メソッドであるprintlnをinvokeします。 printlnは、パラメタ値として2つの値を(引数に)取ります。 (インスタンス・メソッドは、常に、1番目の引数としてインスタンス・リファレンスを暗に取ります)

命令群・クラスファイル内のデータ構造・定数それ自身は、コンスタントプール内の定数を参照するかもしれません。 これらのリファレンスは、エンコードされた索引(固定)を通じて、直接、命令に実装されます。 このことは、(訳注:figure 2 参照のこと)囲まれたボックスで強調されたアイテムで図解されています。

例えば、invokevirtual命令はMethodRef定数を参照します。 MethodRef定数は、呼び出されるメソッド名の情報や、 シグネチャ(例:エンコードされた引数や戻り値の型)、そのメソッドがどのクラスに 属するかの情報、を含みます。 事実、囲みで強調したように、MethodRef定数それ自体は、単に 実際のデータを持つ別のエントリを参照するのみです。例:MethodRef定数の中のConstantClassエントリは、 java.io.PrintStreamへのシンボリック参照です。 クラスファイルをコンパクトに保つ為、異なる命令や他のコンスタントプールのエントリによって、これらの定数は共有されます。 似たように、フィールドはFieldref定数で表現されます。 Fieldref定数は、フィールドの名前・型の情報や、そのフィールドを含むクラス名の情報を含んでいます。

コンスタントプールは基本的に以下のタイプの定数を持ちます:メソッド・フィールド・クラスへのリファレンス型、文字列値型・32ビット符号付き整数型・単精度浮動小数点数値型・64ビット符号付き整数型・倍精度浮動小数点数値型


2.2 バイトコード命令セット

JVMは、各々のメソッド呼び出しのサイズが固定であるローカル・スタック・フレームを生成する、スタック指向型のインタプリタです。 ローカル・スタックのサイズは、コンパイラによって見積もられます。 フレーム領域に、レジスタセットのように用いられるローカル変数を含め、中継的に、値が保存されます。 これらのローカル変数は、0から65535の番号がつきます。すなわち、 一つのメソッド毎に最大65536個のローカル変数が使える、というわけです。 呼び出し元/呼び出し先メソッドのスタック・フレームは、部分的に重なっています。すなわち、 呼び出し元は、引数をオペランドスタックにpushし、呼び出された方のメソッドはそれらをローカル変数として受け取ります。

バイトコード命令セットは、現在、212の命令から成り立っており、44の演算コードが、将来的な拡張あるいは仮想マシン内での中間的な最適化に使われるよう、予約されています。 命令セットは、大まかに以下のようにグループ化されます:

スタック演算: ldc命令、あるいは"略式"命令(演算子が命令名に埋め込まれている)によって、 コンスタントプールから定数がロードされてスタックにpushされます。 例:iconst_0あるいはbipush(="push" "by"te value)

算術演算: Java仮想マシンの命令セットは、特定の型の値への処理の異なる命令を使って、演算子の型を識別します。 iで始まる算術演算、例えば、整数("i"nteger)型を意味する演算、つまり iaddは、2つの整数を足し算し、スタックにその結果をpushします。Java型である booleanbyteshortcharは、Java仮想マシンによって、integer同様に扱われます。

分岐処理コントロール: gotoif_icmpeq(2つの整数が等しいかどうかを比較します)といった分岐処理命令があります。 また、try-catchブロックのfinally文を実現する、jsr(サブルーチンへの分岐:Jump to Sub-Routine)命令とret(Return)命令の ペアもあります。 例外は、athrow命令によって"投げ"られるかもしれません。 分岐のターゲットは、現在のバイトコード位置からのオフセット(すなわち、整数値)として符号化されます。

演算ロード・格納:iloadistoreといったローカル変数用のものです。 また、整数値を配列に格納するiastoreといった配列演算もあります。

フィールド・アクセス: インスタンスのフィールドの値は、getfield命令で引き出され、putfield命令で書き込まれるでしょう。 静的フィールドであれば、其々、getstaticputstaticが対応します。

メソッド呼び出し: 静的メソッドは、invokestatic命令を通じて呼び出されるか、invokevirtual命令で仮想的に関連付けられるでしょう。 スーパークラスのメソッドやprivate型メソッドは、invokespecialで呼び出されます。 インターフェースのメソッドという特別な場合は、invokeinterface命令によってメソッドが呼び出されます。

オブジェクト割り当て: クラスインスタンスは、new命令が割り当てられています。 int[]のような基本型の配列はnewarray命令、 String[][]のようなリファレンス配列はanewarray命令あるいはmultianewarray命令が割り当てられています。

変換・型チェック: 基本型のスタック演算子では、浮動小数点値を整数値に変換するf2iといったような 型変換用の演算が存在します。型変換の妥当性は、checkcastでチェックされ、 instanceofオペレータは、同一名の命令に直接マッピングされます。

殆どの命令は固定長ですが、可変長の命令がいくつかあります:特に、 lookupswitch命令とtableswitch命令で、これらは switch()文を実装するのに使われます。case文の 数は変わる事がありますので、これらの命令には、可変であるステートメント(文)の「数」が含まれます。

ここで、全てのバイトコード命令をリストアップはしません。というのも、 詳細は、JVM仕様書 で説明されていますので。演算コードの名前は、殆どが自己説明型ですから、 かなり直感的に以下のコードサンプルを理解出来る事でしょう。


2.3 メソッド・コード

抽象型ではない(ネイティブでもない)メソッドには、 以下のデータを保持する"Code"という属性を含んでいます: メソッドのスタックフレームの最大サイズ・ローカル変数の数・バイトコード命令の配列。 オプションとして、ローカル変数の名前やソースファイルの行数(デバッガが使うでしょう) についての情報も含みます。

処理中に例外が発生すると、JVMは、例外ハンドラのテーブルを調べることでその例外を処理します。 そのテーブルは、ハンドラすなわちコードの塊に印をつけ、バイトコードの所与の領域で発生したある種の例外の責任を持ちます。 適切なハンドラがない場合、例外はメソッドの呼び出し元に伝播します。 ハンドラ情報それ自体は、Code属性内の属性に格納されています。


2.4 バイトコード・オフセット

gotoといった分岐命令の対象は、バイトコード配列の相対的なオフセットとしてエンコードされています。 例外ハンドラやローカル変数は、バイトコード内での絶対的な番地(address)を参照します。 前者は、tryブロックの始まりと終わりへの参照、命令ハンドラコードへの参照を含んでいます。 後者は、ローカル変数が有効である領域、すなわちスコープに印をつけます。 ですから、オフセットを毎回再計算しなければならず、参照オブジェクトを更新しなければならないので、 アブストラクトレベルでコード領域内での挿入・削除が困難になっています。 セクション3.3で、どのようにBCELがこの制約を改善するかをお教えいたします。


2.5 型情報

Javaは、"type-safe"の言語であり、フィールドのtype・ローカル変数・メソッドについての情報は signaturesと呼ばれる所に格納されます。 これらは、コンスタントプールに格納される文字列であり、特別な形式でエンコードされています。 例えば、mainメソッドの引数と戻り値の型

public static void main(String[] argv)

は、以下の文字列で表現されます。

([java/lang/String;)V

クラスは、内部的に"java/lang/String"といった文字列で表現され、 Java基本型は、floatという名前の整数値、などといった形で表現されます。 シグニチャ内では、1文字(例:整数の場合I)で表現されます。 配列は、シグニチャの最初に[がつく形で表されます。


2.6 コード例

以下のプログラム例では、プロンプト上で数字を要求し、その数字の階乗(n!)を表示します。 標準入力から値を読み取るreadLine()メソッドは、IOException例外を 発生させるかもしれませんし、もし誤った形式の数字(訳注:1000ではなく1,000等)がparseInt()に渡されれば、 NumberFormatException例外が投げられるでしょう。 このように、コードの重要な領域は、try-catchブロックにカプセル化されなければなりません。

  
    import java.io.*;

    public class Faculty {
      private static BufferedReader in = new BufferedReader(new
                                InputStreamReader(System.in));

      public static final int fac(int n) {
        return (n == 0)? 1 : n * fac(n - 1);
      }

      public static final int readInt() {
        int n = 4711;
        try {
        System.out.print("Please enter a number> ");
        n = Integer.parseInt(in.readLine());
        } catch(IOException e1) { System.err.println(e1); }
        catch(NumberFormatException e2) { System.err.println(e2); }
        return n;
      }

      public static void main(String[] argv) {
        int n = readInt();
        System.out.println("Faculty of " + n + " is " + fac(n));
      }
    }
  

このコード例は、以下のバイトコードの塊にコンパイルされます:

    0:  iload_0
    1:  ifne            #8
    4:  iconst_1
    5:  goto            #16
    8:  iload_0
    9:  iload_0
    10: iconst_1
    11: isub
    12: invokestatic    Faculty.fac (I)I (12)
    15: imul
    16: ireturn

    LocalVariable(start_pc = 0, length = 16, index = 0:int n)
  

fac(): facメソッドは、一つだけローカル変数の引数(index = 0)をとります。 この変数のスコープ範囲は、バイトコード列の始まりから丁度終わりまでです。 変数nの値(iload_0で引っ張られる値)が0でなければ ifne命令はオフセット8のバイトコードに分岐し、さもなくば オペランド・スタックに1がpushされて処理フローは最後のreturn(訳注:ここでは、16のireturn)に 分岐します。ここでは、読みやすくするため、これらの例での分岐命令のオフセットは 絶対アドレスで表示しています(実際は相対アドレスです)。

再帰処理が続く場合、 掛け算(nfac(n - 1))用の独立変数が評価され、オペランド・スタックにその結果がpushされます。 乗法演算が行われた後、関数は、計算された値をスタックの上部から戻します。

    0:  sipush        4711
    3:  istore_0
    4:  getstatic     java.lang.System.out Ljava/io/PrintStream;
    7:  ldc           "Please enter a number> "
    9:  invokevirtual java.io.PrintStream.print (Ljava/lang/String;)V
    12: getstatic     Faculty.in Ljava/io/BufferedReader;
    15: invokevirtual java.io.BufferedReader.readLine ()Ljava/lang/String;
    18: invokestatic  java.lang.Integer.parseInt (Ljava/lang/String;)I
    21: istore_0
    22: goto          #44
    25: astore_1
    26: getstatic     java.lang.System.err Ljava/io/PrintStream;
    29: aload_1
    30: invokevirtual java.io.PrintStream.println (Ljava/lang/Object;)V
    33: goto          #44
    36: astore_1
    37: getstatic     java.lang.System.err Ljava/io/PrintStream;
    40: aload_1
    41: invokevirtual java.io.PrintStream.println (Ljava/lang/Object;)V 
    44: iload_0
    45: ireturn

    Exception handler(s) = 
    From    To      Handler Type
    4       22      25      java.io.IOException(6)
    4       22      36      NumberFormatException(10)
  

readInt(): 初めに、ローカル変数n (index 0 で)は、4711という値で初期化されます。 次の命令(getstatic)は、System.out静的フィールドによって保持されるリファレンスをスタックにロードします。 そして、文字列(訳注:Please enter...部分です)がロードされ、出力され、標準入力からの数値が読み取られ、nに関連付けられます。

呼び出されたメソッド(readLine()parseInt())の一つが例外を投げた場合、 例外の種類に基づき、Java仮想マシンは宣言された例外ハンドラの一つを呼び出します。 try-文自体は何もコードを生成しません。単に其の後に続くハンドラがactiveである 範囲を規定するのみです。 この例では、ある特定のソースコード範囲が、オフセット4(4を含む)からオフセット22(22を含まない)までのバイトコード範囲にマップされます。 もし例外が何も発生しなければ("通常の"処理フロー)、goto命令が例外ハンドラの後ろの部分に分岐を行います。 n値がロードされ、戻されます。

java.io.IOExceptionのハンドラは、オフセット25から始まります。 このハンドラでは、単にエラーを出力し、通常の処理フローに戻ります。すなわち、 何の例外も起こらなかったかのように振舞います。


3 BCEL API

BCEL API は、 具体的なJava仮想マシン環境を抽象化し、バイナリJavaクラスファイルの読み書き方法を抽象化します。 APIは、主に3つのパーツから成り立っています:

  1. クラスファイルの"static"制約を表現するクラスが入ったパッケージ、つまり、 クラスファイル形式を表し、バイトコード修正の目的に向けられていないパッケージ。 このクラス群は、ファイルへの書き込み又はファイルからの読み込みに利用されるかもしれません。 Javaクラスをソースが手元に無い状態であっても解析する際に特に有効でしょう。 主なデータ構造は、メソッドやフィールドなどを含んだJavaClassと呼ばれます。
  2. JavaClassあるいはMethodオブジェクトを動的に生成・修正を加えるパッケージ。 解析用コードを入れたり、クラスファイルから不必要な情報を取り除いたり、Javaコンパイラのバックエンドのコードジェネレータを実装するのに 利用されるかもしれません。
  3. 様々なコードの例やユーティリティ(クラスファイル閲覧ユーティリティ・HTML変換用ツール・ クラスファイルからJasminアセンブリ言語へのコンバータ、等)。


3.1 JavaClass

BCEL APIの"static"コンポーネントは、 org.apache.bcel.classfileパッケージ内にあり、closely represents class files. JVM仕様書 で定義されているバイナリコンポーネントやデータ構造、 セクション2で記述した バイナリコンポーネントやデータ構造は、全て、クラスにマッピングされます。 Figure 3は、BCEL APIのクラス階層構造のUMLダイアグラムを表しています。 附記のFigure 8も、ConstantPoolコンポーネントの詳細のダイアグラムを表しています。


Figure 3: JavaClass APIのUML図

データ構造の上位レベルはJavaClassであり、殆どの場合、 バイナリクラスファイルの解析が可能なClassParserオブジェクトによって 生成されます。 JavaClassオブジェクトは基本的に、フィールド、メソッド、スーパークラスや実装されたインターフェースへの シンボリックリファレンスで構成されます。

コンスタントプールは、ある種の中心的なレポジトリの役割を担い、従って、全てのコンポーネントにとって極めて重要です。 ConstantPoolオブジェクトは、 固定サイズのConstantエントリ配列を含んでいます。 Constantエントリは、引数として整数インデックス値をとる getConstant()メソッドを通じて引き出されるかもしれません。 コンスタントプールのインデックス値は、 クラスファイルのその他コンポーネントやコンスタントプールエントリそれ自体に含まれる のと同様、命令そのものにも含まれるかもしれません。

メソッド及びフィールドには、記号として型を定義しているシグニチャが含まれます。 public static finalといったアクセスフラグは、いくつかの場所で現れ、 整数値のビットマスクでエンコードされています。例えば、public static finalは 以下のJava表現にマッチします:

int access_flags = ACC_PUBLIC | ACC_STATIC | ACC_FINAL;

既にセクション2.1で言明した通り、 コンポーネントの中にはattributeオブジェクト:クラス・フィールド・メソッド、 Codeオブジェクト(セクション2.3でご紹介しました)を含むものがいくつかあります。 後者は、実際のバイトコード配列・スタック最大サイズ・変数の数・例外ハンドラのテーブル・ LineNumberTable属性やLocalVariableTable属性でコード化される オプションのデバッグ情報、といったものを含む、属性そのものです。 属性は、一般的にあるデータ構造に特有のものとなっています。つまり、 はっきりと禁止されていないにせよ、異なるコンポーネントが同じ種の属性を共有する事はありません。 図では、Attributeクラスは、属するコンポーネントで定型化されています。


3.2 Classレポジトリ

既に用意されているRepositoryクラスを使うと、クラスファイルのJavaClassオブジェクトへの読み込みが非常にシンプルになります:

JavaClass clazz = Repository.lookupClass("java.lang.String");

また、Repositoryクラスは、動的に、instanceofオペレータや他の有用な手順と同等の機能を提供します:

  if(Repository.instanceOf(clazz, super_class) {
    ...
  }


3.2.1 クラスファイルデータへのアクセス

クラスファイルコンポーネント内の情報のアクセスは、Java Beansに似て直感的である、 set/getメソッドを使います。 toString()メソッドも規定されていますので、シンプルなクラスビュワーを 作るのも容易です。実際、ここで使われているサンプルもこのように生成されています:

  System.out.println(clazz);
  printCode(clazz.getMethods());
  ...
  public static void printCode(Method[] methods) {
    for(int i=0; i < methods.length; i++) {
      System.out.println(methods[i]);

      Code code = methods[i].getCode();
      if(code != null) // Non-abstract method
        System.out.println(code);
    }
  }
  


3.2.2 クラスデータ解析

最後に(これだけではありませんが)、BCELは、Visitorのデザインパターンをサポートしています。 ですから、クラスファイルの中身をトラバースし解析するためのvisitorオブジェクトを書くことが出来ます。 頒布パッケージには、Jasminアセンブラ言語へクラスファイルを 変換するためのJasminVisitorが含まれています。


3.3 ClassGen

このセクションのAPI(org.apache.bcel.genericパッケージ)は、動的なクラスファイルの作成・変換といった抽象レベル(abstraction level)を供給します。 Javaクラスファイルの"static"制約を、ハードコードされたバイトコード番地のように、"generic"(ジェネリック)にします。 例えば、ジェネリック(generic)コンスタントプールは、異種の定数を追加するメソッドを持つConstantPoolGenクラスによって実装されます。 従って、ClassGenは、メソッド追加、フィールド追加、属性追加のインターフェースを提供します。 Figure 4は、このセクションのAPIの概観を表しています。


Figure 4: ClassGen API の UMLダイアグラム


3.3.1 型

Typeクラスを導入する事で、型シグニチャ文法(セクション2.5をご覧下さい) の具体的な詳細を抽象化します。Typeクラスは、例えば、戻り値や引数の型を定義するメソッドによって使われます。 それを具体化するサブクラスは、要素の型や次元数から成るBasicTypeObjectTypeArrayTypeクラスです。 一般に使われる型では、このクラスは事前に定義された定数を与えます。 例えば、セクション2.5で示されるmainメソッドのメソッドシグニチャは、以下のように表現されます:

  Type   return_type = Type.VOID;
  Type[] arg_types   = new Type[] { new ArrayType(Type.STRING, 1) };
  

Typeクラスには、テキストシグニチャへの型の変換やその逆の為のメソッドもあります。 そのサブクラスは、Java言語仕様で明記されている手順や定数の実装を含んでいます。


3.3.2 ジェネリック・フィールド及びメソッド

フィールドは、FieldGenオブジェクトで表現されます。FieldGenオブジェクトは、ユーザによって自由に修正可能です。 static finalアクセス権があれば、すなわち、定数や基本型であれば、 オプションとして初期値を持っていることもあります。

ジェネリック・メソッド(generic method) には、メソッドが投げる例外・ローカル変数・例外ハンドラ、を追加するメソッドがあります。 後の2つ(ローカル変数・例外ハンドラ)は、ユーザ設定可能なオブジェクトとしても表現されます。 ローカル変数・例外ハンドラは、バイトコード/アドレスへの参照を含むため、用語的にはinstruction targeterとしての役割も果たします。 instruction targeterは、参照へリダイレクトするupdateTarget()メソッドが含まれています。 これは、幾分、Observerデザインパターンに関連しています。 ジェネリック・メソッド(抽象メソッドではない)は、命令オブジェクトから成る命令リストを参照します。 バイトコード・アドレスは、命令オブジェクトのハンドラによって実装されています。 もしリストが更新されれば、instruction targeterは其の事を知らされるでしょう。 この事は、以下のセクションでより深く説明されます。

メソッドに必要な最大スタックサイズと、ローカル変数の最大数は、手動設定されるか、あるいは setMaxStack()メソッドやsetMaxLocals()メソッドによって自動的に計算されます。


3.3.3 命令

命令をオブジェクトとしてモデル化するのは、一見、変な事に見えるでしょうが、 実際は、プログラマにハイレベルな(具体的なバイトコード・オフセット等の詳細を扱う事の無い)フロー・コントロールの見地を与えてくれるのです。 命令は、オペコード(opecode:tagと呼ばれることもあります)・バイト長・バイトコード内でのオフセット(あるいはインデックス)で構成されています。 多くの命令が不変である為(例としてスタック処理)、InstructionConstantsインターフェースは、予め定義済みで共有可能な"軽量の"定数を使おうとします。

命令は、サブクラス化を通じてグループ化されます。命令クラスの階層型は附記のfigureで(不完全ですが)示されています。 最も重要な命令群は、バイトコードの中で対象となるどこかへの分岐を行う分岐命令(例:goto)です。 明らかに、これはInstructionTargeterの役割を演じる候補にもなるでしょう。 命令は、それが実装するインターフェースによってより一層グループ化されます。 例えば、ldcといった特定の型と関連付けられたTypedInstructionや、処理時に例外を発生させるExceptionThrower命令などが、其の一例でしょう。

accept(Visitor v)メソッド、すなわちVisitorデザインパターンを通じて、 全ての命令をトラバースする事が出来ます。 しかし、特定の命令グループの合併を扱う事の出来るこれらのメソッドには、ある特殊なトリックがあります。 accept()メソッドは、対応するvisit()メソッドだけを呼ぶのではなく、其々のスーパークラスと実装しているインターフェースのvisit()メソッドを最初に呼び出します。 つまり、最も限定的なvisit()メソッドは最後に呼ばれるのです。 このようにして、いわばBranchInstructionの全ての扱いが、1つのメソッドにグループ化する事が出来るのです。

デバッグの目的のために、独自の命令を"開発"する事が意味のある場合もあります。 洗練されたコード・ジェネレータ( Baratフレームワークのバックエンド として静的分析用に使われるコード・ジェネレータのような)では、テンポラリ用のnop (No operation)命令を挿入する必要がしばしばあります。 生成されるコードを検査する際に、nopが実際に挿入された箇所を追跡するのは非常に困難である場合があります。 追加デバッグ情報を含む派生したnop2命令を想像するかもしれません。 命令リストがバイトコードにダンプされる際、余分なデータは単に切り落とされてしまうのです。

ロード時に通常のバイトコードと置き換わる、あるいは、新しいJVMによって認識される、 複素数の演算を行う新しいバイトコード命令を想像する事もあるでしょう。


3.3.4 命令リスト

命令リストは、命令オブジェクトをカプセル化する命令ハンドルのリストによって実装されています。 リスト内の命令への参照は、命令への直接的なポインタによって実装されるのではなく、命令ハンドルへのポインタによって実装されます。 これによって、コードの追加・挿入・削除エリアが非常にシンプルになり、不変命令オブジェクト(軽量オブジェクト)の再利用も可能となります。 シンボリック参照を使うので、具体的なバイトコード・オフセットの計算は、ファイナライズまで(すなわち、コード生成・変換処理をユーザが終えるまで)起こりません。 これからは、「命令ハンドル(instruction handle)」と「命令(instruction)」を同義として使います。 命令ハンドルは、addAttribute()メソッドを使う追加ユーザ定義データ、を含む場合もあります。

Appending: 現在のリストのどこへでも、命令や他の命令リストを追加する事ができます。 所与の命令ハンドルの後に命令が追加されます。 全てのappendメソッドは、分岐処理命令の対象として使われる可能性のある新しい命令ハンドルを戻します。例えば:

  InstructionList il = new InstructionList();
  ...
  GOTO g = new GOTO(null);
  il.append(g);
  ...
  // Use immutable fly-weight object
  InstructionHandle ih = il.append(InstructionConstants.ACONST_NULL);
  g.setTarget(ih);
  

Inserting: Instructions may be inserted anywhere into an existing list. They are inserted before the given instruction handle. All insert methods return a new instruction handle which may then be used as the start address of an exception handler, for example.

  InstructionHandle start = il.insert(insertion_point,
    InstructionConstants.NOP);
  ...
  mg.addExceptionHandler(start, end, handler, "java.io.IOException");
  

Deleting: 命令の削除もまた、極めて直感的です:全ての命令ハンドルと 所与の範囲にある命令が命令リストから取り除かれ、処分されます。 しかし、delete()メソッドは、instruction targeters が依然として削除された命令の一つを参照している場合、 TargetLostException例外を投げます。 ユーザは、try-catch節でそれら例外を扱う必要があり、 それら参照のどこかにリダイレクトする必要があります。 附記に説明するpeep holeオプティマイザは、この詳しい例です。

  try {
    il.delete(first, last);
  } catch(TargetLostException e) {
    InstructionHandle[] targets = e.getTargets();
    for(int i=0; i < targets.length; i++) {
      InstructionTargeter[] targeters = targets[i].getTargeters();
      for(int j=0; j < targeters.length; j++)
         targeters[j].updateTarget(targets[i], new_target);
    }
  }
  

Finalizing: 命令リストが純粋なバイトコードにダンプされる準備が出来た際、 全てのシンボリック参照は現実のバイトコード・オフセットにマップされなければなりません。 これは、getByteCode()メソッド(デフォルトではMethodGen.getMethod()メソッドで呼ばれる)によって行われます。 その後、内部で命令ハンドルが再利用可能となるようdispose()メソッドを呼ぶべきです。 これによってメモリ消費を改善できます。

  InstructionList il = new InstructionList();

  ClassGen  cg = new ClassGen("HelloWorld", "java.lang.Object",
                              "<generated>", ACC_PUBLIC | ACC_SUPER,
                              null);
  MethodGen mg = new MethodGen(ACC_STATIC | ACC_PUBLIC,
                               Type.VOID, new Type[] { 
                                 new ArrayType(Type.STRING, 1) 
                               }, new String[] { "argv" },
                               "main", "HelloWorld", il, cp);
  ...
  cg.addMethod(mg.getMethod());
  il.dispose(); // Reuse instruction handles of list
  


3.3.5 コードサンプル(再)

命令リストを使うと、コードに対する包括的な見地が得られます: Figure 5で、セクション2.6での機能例の中のreadInt()メソッドのコードチャンク を再びお見せします:ローカル変数ne1の両方とも、命令への参照を2つ持っています(スコープを定義しつつ)。 メソッドの最後に、iloadへのgoto分岐が2つあります。 例外ハンドラの一つが表示されています:tryブロックの始まりと終わり、更に、例外ハンドラコードに対して参照しています。


Figure 5: readInt()メソッド用命令リスト


3.3.6 命令ファクトリ

ある命令の生成を単純化するため、さっとなめるだけで命令を生成する多くの有用なメソッドを持つ、 備え付けのInstructionFactoryクラスを使う事が出来ます。 その代わりに、混合命令(compound instruction)を使う事も出来ます: バイトコード生成時に、非常に頻繁に典型的なパターンが発生する場合があります...例えば、算術表現や比較表現などの編集時です。 きっと、それらの表現が現れる全ての箇所で、バイトコードにそれらの表現を変換するコードを書きたくない、と思うでしょう。 これをサポートするため、BCEL API には、 混合命令用インターフェース(CompoundInstruction)(ただ一つgetInstructionList()メソッドがあるインターフェースです) が含まれています。 このクラスのインスタンスは、通常の命令が発生す箇所、特に append 演算がある箇所、のどこであっても使われる事でしょう。

例: 定数の"PUSH" オペランドスタックに定数を"push"するコードには、色々なやり方があるでしょう。 セクション2.2で説明したように、 生成されるバイトコードをよりコンパクトにするのに使われるであろう"ショートカットの"命令があります。 1つの1をスタックに"push"する最も小さい命令はiconst_1であり、 他の可能性としては、bipush(-128から127まで値をpushする事が出来ます)、 sipush(-32768から32767まで)、ldc(コンスタントプールから定数をロードします)、 があります。

最もコンパクトな命令を繰り返し選択する変わりに、定数や文字列をpushする場合はいつでも、混合PUSH命令を使う事ができます。 これは、適切なバイトコード命令を生成し、必要とあらばエントリをコンスタントプールに挿入します。

  InstructionFactory f  = new InstructionFactory(class_gen);
  InstructionList    il = new InstructionList();
  ...
  il.append(new PUSH(cp, "Hello, world"));
  il.append(new PUSH(cp, 4711));
  ...
  il.append(f.createPrintln("Hello World"));
  ...
  il.append(f.createReturn(type));
  


3.3.7 正規表現使用のコード・パターン

コードを変換する際、例えば、最適化の場合や分析メソッドの呼び出しを入れる場合、 変換を実行するあるコード・パターンで検索するのが一般的でしょう。 このような状況を単純に扱う為、BCEL には特別の機能が導入されています: 命令リスト内で所与のコード・パターンを正規表現(regular expression) を使って検索できます。 この表現では、オペコード名で命令が表現されるでしょう(例:LDC) また、其々のスーパークラスを使う場合もあるでしょう(例:"IfInstruction")。 メタ文字(+*(..|..) といったような)には一般的な意味があります (訳注:一般的な意味、は、「正規表現で通常使われる意味」ということ。XMLをイメージしても勿論良い)。 ですから、以下の表現は、

"NOP+(ILOAD|ALOAD)*"

少なくとも1つ以上のNOPと、それに続く0以上のILOAD命令及び ALOAD命令、で構成されるコードの一部を意味します。

org.apache.bcel.util.InstructionFinderクラスのsearch()メソッドは、 正規表現と(正規表現マッチの)開始点を引数にとって、命令でマッチした範囲を表現するiteratorを戻します。 命令でマッチした範囲に関する追加的な定数(正規表現を通じて実装され得ない)は、code constraint オブジェクトを通じて表現されるでしょう。


3.3.8 例: ブール型表現の最適化

Javaでは、ブール型の値はそれぞれ1あるいは0にマッピングされます。 ですから、ブール値表現を評価するシンプルなやり方は、 オペランドスタックに、その表現の値如何で1あるいは0を"push"する事でしょう。 しかしこのやり方では、 ブール値表現が連続して繋がる場合(例えば、&&で)、 多くの1や0をスタックに"push"する長いコードのチャンクが発生する事になります。

コードがファイナライズされる時に、 これらのチャンクをpeep holeアルゴリズムで最適化することが出来ます: 1あるいは0をスタックに生成しifne命令(スタック値が0であれば分岐)が其の後に続く IfInstruction(例:2つの整数値の比較であれば--if_icmpeq)が、 IfInstructionで置き換わり、 その分岐対象がifne命令の対象で置き換わる事もあります:

  CodeConstraint constraint = new CodeConstraint() {
    public boolean checkCode(InstructionHandle[] match) {
      IfInstruction if1 = (IfInstruction)match[0].getInstruction();
      GOTO          g   = (GOTO)match[2].getInstruction();
      return (if1.getTarget() == match[3]) &&
             (g.getTarget() == match[4]);
    }  
  };

  InstructionFinder f    = new InstructionFinder(il);
  String            pat = "IfInstruction ICONST_0 GOTO ICONST_1 NOP(IFEQ|IFNE)";

  for(Iterator e = f.search(pat, constraint); e.hasNext(); ) {
    InstructionHandle[] match = (InstructionHandle[])e.next();;
    ...
    match[0].setTarget(match[5].getTarget()); // Update target
    ...
    try {
      il.delete(match[1], match[5]);
    } catch(TargetLostException e) { ... }
  }
  

適用されたコード制約オブジェクトは、マッチしたコードが実際に対象となる正規表現に対応しているかどうかを保証します。 連続してこのアルゴリズムを適用すると、 全ての不要なスタック演算と分岐命令がバイトコードから削除されます。 削除された命令で、InstructionTargeterオブジェクトを依然として参照している ものがあれば、その参照はcatch-節で修正されなければなりません。

適用例: この表現:

  if((a == null) || (i < 2))
    System.out.println("Ooops");
  

は、figure 6で示される バイトコードのチャンクの両者にマッピングされ得ます。 列の左が最適化されていないコードである一方、列の右は peep holeアルゴリズムが適用された後の同じコードを表しています:

5:  aload_0
6:  ifnull        #13
9:  iconst_0
10: goto          #14
13: iconst_1
14: nop
15: ifne          #36
18: iload_1
19: iconst_2
20: if_icmplt     #27
23: iconst_0
24: goto          #28
27: iconst_1
28: nop
29: ifne          #36
32: iconst_0
33: goto          #37
36: iconst_1
37: nop
38: ifeq          #52
41: getstatic     System.out
44: ldc           "Ooops"
46: invokevirtual println
52: return
  
10: aload_0
11: ifnull        #19
14: iload_1
15: iconst_2
16: if_icmpge     #27
19: getstatic     System.out
22: ldc           "Ooops"
24: invokevirtual println
27: return
  


4 適用範囲

BCELの適用範囲は、 クラス・プロファイラ・バイトコードオプティマイザ・コンパイラから、 実行時の解析ツールやJava言語の拡張に至るまで非常に多くの可能性があります。

Baratといったコンパイラは、バイトコードを生成するのにBCELをバックエンドで使っています。 他の適用範囲の可能性としては、コードにプロファイル用メソッドの呼び出しを入れる事による、「バイトコードの静的分析」や「実行時のクラスの振る舞いの検査」があるでしょう。 より深い例としては、Eiffel風のアサーション (assertion) 機能や、自動delegation、あるいはAspect-Oriented Programmingの概念のものでしょう(訳注:ここらへんは、Java1.4リリースで実装されているものもあるでしょう。JDK1.4関連のドキュメントを読む事をお奨めします)。 BCELをつかったプロジェクトのリストは、こちらにあります。


4.1 クラスローダー

ファイルシステムあるいは他のリソースからクラスファイルをロードし、 バイトコードを仮想マシンに渡す、という責任を担うのがクラスローダーです。 独自のClassLoaderオブジェクトがクラスのロードの標準的な処理手順(つまり、システムのクラスローダー)の代わりとして使われる事もあるでしょう。 そして、これらオブジェクトが、JVMに実際にバイトコードを渡す前にいくつかの変換を実行するのに使われる事もあるでしょう。

あり得るシナリオが、figure 7で示されています: 実行時の間に、仮想マシンは所与のクラスをロードするために独自のクラスローダーを要求します。 しかし、JVMに実際のバイトコードが渡される前に、そのクラスローダーは"一時停止"してクラスの変換処理を先に行います。 修正されたバイトコードが依然として正しいものである事と、JVM規則に決して違反していない事を保証するため、 JVMが最終的に処理を行う前にベリファイヤによってチェックが為されます。


Figure 7: クラスローダー

クラスローダーを使うのはJava仮想マシンを拡張(新規機能を追加し、実際の修正を必要としない)する上でエレガントな方法です。 この概念は、 Java Reflection API でサポートされる静的なリフレクションと対照的に、 開発者は、自分のアイディアの実装をするロード時のリフレクション(reflection)を 使えるようになります。ロード時変換は、ユーザに新規レベルのアブストラクションを与えます。 オリジナルのクラスの開発者が書いたstatic制約に厳格に縛られる、という事ではなく、 新しい機能の恩恵を受けるために、第三者のコードのアプリケーションをカスタマイズする、という事です。 これらの変換はオン・デマンドで処理され、他のユーザと干渉もしないしオリジナルのバイトコードを変える事もありません。 事実、ファイルをロードせずに、クラスローダーがアド・ホック(ad hoc)にクラス生成を行う事すらあります。 既に、BCEL には、 動的クラス生成のビルト・インのサポートがあります。 その例は、ProxyCreatorクラスです。


4.1.1 例: Poor Man's Genericity

例えば、パラメタ化されたクラスでJavaを拡張する 「"Poor Man's Genericity" プロジェクト」 では、パラメタ化されたクラスのインスタンス生成の為にBCELが2箇所で使われています: コンパイル時(標準のjavacとちょっとだけ変更されたクラスで)と実行時に、独自のクラスローダーを使っています。 コンパイラは 追加情報をクラスファイル(属性)に与え、そのファイルがクラスローダーによるロード時に評価されます。 クラスローダーはロードされたクラスにいくつかの変換を行い、仮想マシンにそれらを渡します。 どのようにクラスローダーのロード用メソッドがパラメタ化されたクラス(例:Stack<String>)のリクエストを満たしていくのか、を、 以下のアルゴリズムで説明します。

  1. Stackクラスを探し、ロードし、追加の型情報を含むあるクラス属性(つまり、"実際の"クラス名を定義する属性:Stack<A>)をチェックします。
  2. A型への参照やA型に遭遇した際、全てを実際の型であるStringへの参照に置き換えます。 例えば、メソッド
  3. void push(A obj) { ... }

    は、こうなります:

    void push(String obj) { ... }
  4. 結果生じるクラスを仮想マシンに戻します。


附記


HelloWorldBuilder

以下のプログラムでは、nameを標準入力から読み取り、よく見かける"Hello"を印字します。 readLine()メソッドはIOException例外を投げる事がありますので、 try-catch節で囲みます。

  import java.io.*;

  public class HelloWorld {
    public static void main(String[] argv) {
      BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
      String name = null;

      try {
	System.out.print("Please enter your name> ");
	name = in.readLine();
      } catch(IOException e) { return; }

      System.out.println("Hello, " + name);
    }
  }
  

ここで、BCEL APIを使った 走り書きからどのように上記Javaクラスが生成されるかをスケッチします。 読みやすくする為、テキストのシグニチャを使い、動的に生成はしません。 例えば、シグニチャはこうなります:

"(Ljava/lang/String;)Ljava/lang/StringBuffer;"

実際は、以下と共に作られます:

Type.getMethodSignature(Type.STRINGBUFFER, new Type[] { Type.STRING });

Initialization: 先ず初めに、空のクラスと命令リストを作ります:

  ClassGen cg = new ClassGen("HelloWorld", "java.lang.Object",
                             "<generated>", ACC_PUBLIC | ACC_SUPER,
                             null);
  ConstantPoolGen cp = cg.getConstantPool(); // cg creates constant pool
  InstructionList il = new InstructionList();
  

次に、mainメソッドを作成します。メソッド名を与え、Typeオブジェクトでエンコードされたsymbolicタイプのシグニチャを与えます。

  MethodGen  mg = new MethodGen(ACC_STATIC | ACC_PUBLIC, // access flags
                                Type.VOID,               // return type
                                new Type[] {             // argument types
                                  new ArrayType(Type.STRING, 1) },
                                new String[] { "argv" }, // arg names
                                "main", "HelloWorld",    // method, class
                                il, cp);
  InstructionFactory factory = new InstructionFactory(cg);
  

ここで、通常使われる型を定義します:

  ObjectType i_stream = new ObjectType("java.io.InputStream");
  ObjectType p_stream = new ObjectType("java.io.PrintStream");
  

変数innameの作成: コンストラクタを呼びます。つまり、BufferedReader(InputStreamReader(System.in))を実行します。 BufferedReaderオブジェクトへの参照は、スタックの上位部に残り、 新規に割り当てられたin変数で格納されます。

  il.append(factory.createNew("java.io.BufferedReader"));
  il.append(InstructionConstants.DUP); // Use predefined constant
  il.append(factory.createNew("java.io.InputStreamReader"));
  il.append(InstructionConstants.DUP);
  il.append(factory.createFieldAccess("java.lang.System", "in", i_stream,
                                      Constants.GETSTATIC));
  il.append(factory.createInvoke("java.io.InputStreamReader", "<init>",
                                 Type.VOID, new Type[] { i_stream },
                                 Constants.INVOKESPECIAL));
  il.append(factory.createInvoke("java.io.BufferedReader", "<init>", Type.VOID,
                                 new Type[] {new ObjectType("java.io.Reader")},
                                 Constants.INVOKESPECIAL));

  LocalVariableGen lg = mg.addLocalVariable("in",
                          new ObjectType("java.io.BufferedReader"), null, null);
  int in = lg.getIndex();
  lg.setStart(il.append(new ASTORE(in))); // "i" valid from here
  

ローカル変数nameを作成し、初期値としてnullを入れます。

  lg = mg.addLocalVariable("name", Type.STRING, null, null);
  int name = lg.getIndex();
  il.append(InstructionConstants.ACONST_NULL);
  lg.setStart(il.append(new ASTORE(name))); // "name" valid from here
  

Create try-catch block: ブロックの初めを覚えておきます。標準入力からラインを読み込み、name変数に格納します。

  InstructionHandle try_start =
    il.append(factory.createFieldAccess("java.lang.System", "out", p_stream,
                                        Constants.GETSTATIC));

  il.append(new PUSH(cp, "Please enter your name> "));
  il.append(factory.createInvoke("java.io.PrintStream", "print", Type.VOID, 
                                 new Type[] { Type.STRING },
                                 Constants.INVOKEVIRTUAL));
  il.append(new ALOAD(in));
  il.append(factory.createInvoke("java.io.BufferedReader", "readLine",
                                 Type.STRING, Type.NO_ARGS,
                                 Constants.INVOKEVIRTUAL));
  il.append(new ASTORE(name));
  

通常の処理では、例外ハンドラを飛び越えますが、対象のアドレスはまだわかりません。

  GOTO g = new GOTO(null);
  InstructionHandle try_end = il.append(g);
  

メソッドから単に戻る例外ハンドラを追加します。

  InstructionHandle handler = il.append(InstructionConstants.RETURN);
  mg.addExceptionHandler(try_start, try_end, handler, "java.io.IOException");
  

"正常な"コードは処理を継続し、GOTOの対象分岐をセットすることが出来ます。

  InstructionHandle ih =
    il.append(factory.createFieldAccess("java.lang.System", "out", p_stream,
                                        Constants.GETSTATIC));
  g.setTarget(ih);
  

Printing "Hello": 文字列結合がStringBuffer処理にコンパイルされます。

  il.append(factory.createNew(Type.STRINGBUFFER));
  il.append(InstructionConstants.DUP);
  il.append(new PUSH(cp, "Hello, "));
  il.append(factory.createInvoke("java.lang.StringBuffer", "<init>",
                                 Type.VOID, new Type[] { Type.STRING },
                                 Constants.INVOKESPECIAL));
  il.append(new ALOAD(name));
  il.append(factory.createInvoke("java.lang.StringBuffer", "append",
                                 Type.STRINGBUFFER, new Type[] { Type.STRING },
                                 Constants.INVOKEVIRTUAL));
  il.append(factory.createInvoke("java.lang.StringBuffer", "toString",
                                 Type.STRING, Type.NO_ARGS,
                                 Constants.INVOKEVIRTUAL));
    
  il.append(factory.createInvoke("java.io.PrintStream", "println",
                                 Type.VOID, new Type[] { Type.STRING },
                                 Constants.INVOKEVIRTUAL));
  il.append(InstructionConstants.RETURN);
  

Finalization: 最後に、スタックサイズをセットし、デフォルトのコンストラクタ・メソッド(この場合、引数は空)をクラスに追加する必要があります。 スタックサイズは、通常、動いている時に計算される必要があります。

  mg.setMaxStack();
  cg.addMethod(mg.getMethod());
  il.dispose(); // Allow instruction handles to be reused
  cg.addEmptyConstructor(ACC_PUBLIC);
  

最後に(これだけではありませんが)、JavaClassオブジェクトをファイルにダンプします。

  try {
    cg.getJavaClass().dump("HelloWorld.class");
  } catch(java.io.IOException e) { System.err.println(e); }
  


Peepholeオプティマイザ

このクラスは、NOP命令を所与のクラスから削除する、シンプルなPeepholeオプティマイザを実装します。

import java.io.*;

import java.util.Iterator;
import org.apache.bcel.classfile.*;
import org.apache.bcel.generic.*;
import org.apache.bcel.Repository;
import org.apache.bcel.util.InstructionFinder;

public class Peephole {
  public static void main(String[] argv) {
    try {
      /* Load the class from CLASSPATH.
       */
      JavaClass       clazz   = Repository.lookupClass(argv[0]);
      Method[]        methods = clazz.getMethods();
      ConstantPoolGen cp      = new ConstantPoolGen(clazz.getConstantPool());

      for(int i=0; i < methods.length; i++) {
	if(!(methods[i].isAbstract() || methods[i].isNative())) {
	  MethodGen mg       = new MethodGen(methods[i],
					     clazz.getClassName(), cp);
	  Method    stripped = removeNOPs(mg);
	  
	  if(stripped != null)     // Any NOPs stripped?
	    methods[i] = stripped; // Overwrite with stripped method
	}
      }

      /* Dump the class to "class name"_.class
       */
      clazz.setConstantPool(cp.getFinalConstantPool());
      clazz.dump(clazz.getClassName() + "_.class");
    } catch(Exception e) { e.printStackTrace(); }
  }

  private static final Method removeNOPs(MethodGen mg) {
    InstructionList   il    = mg.getInstructionList();
    InstructionFinder f     = new InstructionFinder(il);
    String            pat   = "NOP+"; // Find at least one NOP
    InstructionHandle next  = null;
    int               count = 0;

    for(Iterator i = f.search(pat); i.hasNext(); ) {
      InstructionHandle[] match = (InstructionHandle[])e.next();
      InstructionHandle   first = match[0];
      InstructionHandle   last  = match[match.length - 1];
      
      /* Some nasty Java compilers may add NOP at end of method.
       */
      if((next = last.getNext()) == null)
	break;

      count += match.length;

      /* Delete NOPs and redirect any references to them to the following
       * (non-nop) instruction.
       */
      try {
	il.delete(first, last);
      } catch(TargetLostException e) {
	InstructionHandle[] targets = e.getTargets();
	for(int i=0; i < targets.length; i++) {
	  InstructionTargeter[] targeters = targets[i].getTargeters();
	  
	  for(int j=0; j < targeters.length; j++)
	    targeters[j].updateTarget(targets[i], next);
	}
      }
    }

    Method m = null;
    
    if(count > 0) {
      System.out.println("Removed " + count + " NOP instructions from method " +
			 mg.getName());
      m = mg.getMethod();
    }

    il.dispose(); // Reuse instruction handles
    return m;
  }
}
 


BCELifier

もし、BCELを使ってどのようにあるモノが生成されるかを深く知りたければ、 以下に従うと良いでしょう: 必要機能を備えたプログラムをJavaで書き、通常通りコンパイルして下さい。次に、 BCELifierを用いて、BCELを使った入力用クラスを作ります。
(この一文を暫く熟考するか、もう早速試してみるか... )


コンスタントプールUMLダイアグラム


Figure 8: コンスタントプール・クラスの UML ダイアグラム



Copyright © 1999-2005, Apache Software Foundation
Translated into Japanese by Tetsuya Kitahata , powered by Terra-International, Inc.
<% orig();%>