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型である
boolean ・byte ・short ・char は、Java仮想マシンによって、integer同様に扱われます。
分岐処理コントロール:
goto やif_icmpeq (2つの整数が等しいかどうかを比較します)といった分岐処理命令があります。
また、try-catch ブロックのfinally 文を実現する、jsr (サブルーチンへの分岐:Jump to Sub-Routine)命令とret (Return)命令の
ペアもあります。
例外は、athrow 命令によって"投げ"られるかもしれません。
分岐のターゲットは、現在のバイトコード位置からのオフセット(すなわち、整数値)として符号化されます。
演算ロード・格納: iload ・istore といったローカル変数用のものです。
また、整数値を配列に格納するiastore といった配列演算もあります。
フィールド・アクセス:
インスタンスのフィールドの値は、getfield 命令で引き出され、putfield 命令で書き込まれるでしょう。
静的フィールドであれば、其々、getstatic 、putstatic が対応します。
メソッド呼び出し:
静的メソッドは、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 )で表現されます。
配列は、シグニチャの最初に[ がつく形で表されます。
3 BCEL API
BCEL API は、
具体的なJava仮想マシン環境を抽象化し、バイナリJavaクラスファイルの読み書き方法を抽象化します。
APIは、主に3つのパーツから成り立っています:
クラスファイルの"static"制約を表現するクラスが入ったパッケージ、つまり、
クラスファイル形式を表し、バイトコード修正の目的に向けられていないパッケージ。
このクラス群は、ファイルへの書き込み又はファイルからの読み込みに利用されるかもしれません。
Javaクラスをソースが手元に無い状態であっても解析する際に特に有効でしょう。
主なデータ構造は、メソッドやフィールドなどを含んだJavaClass と呼ばれます。
JavaClass あるいはMethod オブジェクトを動的に生成・修正を加えるパッケージ。
解析用コードを入れたり、クラスファイルから不必要な情報を取り除いたり、Javaコンパイラのバックエンドのコードジェネレータを実装するのに
利用されるかもしれません。
様々なコードの例やユーティリティ(クラスファイル閲覧ユーティリティ・HTML変換用ツール・
クラスファイルからJasmin アセンブリ言語へのコンバータ、等)。
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.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.7 正規表現使用のコード・パターン
コードを変換する際、例えば、最適化の場合や分析メソッドの呼び出しを入れる場合、
変換を実行するあるコード・パターンで検索するのが一般的でしょう。
このような状況を単純に扱う為、BCEL
には特別の機能が導入されています:
命令リスト内で所与のコード・パターンを正規表現(regular expression)
を使って検索できます。
この表現では、オペコード名で命令が表現されるでしょう(例:LDC )
また、其々のスーパークラスを使う場合もあるでしょう(例:"IfInstruction ")。
メタ文字(+ 、* 、(..|..) といったような)には一般的な意味があります
(訳注:一般的な意味、は、「正規表現で通常使われる意味」ということ。XMLをイメージしても勿論良い)。
ですから、以下の表現は、
少なくとも1つ以上のNOP と、それに続く0以上のILOAD 命令及び
ALOAD 命令、で構成されるコードの一部を意味します。
org.apache.bcel.util.InstructionFinder クラスのsearch() メソッドは、
正規表現と(正規表現マッチの)開始点を引数にとって、命令でマッチした範囲を表現するiteratorを戻します。
命令でマッチした範囲に関する追加的な定数(正規表現を通じて実装され得ない)は、code constraint オブジェクトを通じて表現されるでしょう。
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> )のリクエストを満たしていくのか、を、
以下のアルゴリズムで説明します。
Stack クラスを探し、ロードし、追加の型情報を含むあるクラス属性(つまり、"実際の"クラス名を定義する属性:Stack<A> )をチェックします。
A 型への参照やA 型に遭遇した際、全てを実際の型であるString への参照に置き換えます。
例えば、メソッド
void push(A obj) { ... }
は、こうなります:
void push(String obj) { ... }
結果生じるクラスを仮想マシンに戻します。
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");
変数in とname の作成 :
コンストラクタを呼びます。つまり、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); }
BCELifier
もし、BCELを使ってどのようにあるモノが生成されるかを深く知りたければ、
以下に従うと良いでしょう:
必要機能を備えたプログラムをJavaで書き、通常通りコンパイルして下さい。次に、
BCELifier を用いて、BCELを使った入力用クラスを作ります。
(この一文を暫く熟考するか、もう早速試してみるか... )