java.lang.invoke で遊ぼう
2012-02-11(Sat)
IWAMURO Motonori (@vmi_jp)
2012-02-23(Thu)一部追記
※ Java7 API ドキュメントの JavaDoc と同じ。
いやまぁ、パッケージサマリとか各クラスの説明とか見てるとなんとなくわかってくるんですが……。
※ 重なる部分はありますが、リフレクション API を置き換えるものではありません。
java.lang.invoke のクラスが提供する機能は、だいたい次の3種類に分けられます。
このクラス群を理解すれば、とりあえずメタプログラミングするのには困りません。
とりあえず、メタプログラミングするだけなら不要……?
ぶっちゃけよく理解していません……。
Lookup lookup = MethodHandles.lookup();
MethodHandle printf = lookup.findVirtual(
PrintStream.class, "printf",
MethodType.methodType(PrintStream.class, String.class, Object[].class));
printf.invokeWithArguments(System.out, "[%s]\n", "Hello world!");
Lookup lookup = MethodHandles.lookup();
取得できる範囲は lookup() を呼び出した場所のスコープが基準になります。
Lookup オブジェクトを外部のクラスに渡すときは注意が必要です。
単に public なメソッドやフィールドを取得するだけなら、以下を使います。
Lookup lookup = MethodHandles.publicLookup();
MethodHandle mh;
mh = lookup.findConstructor(対象クラス.class, 型指定)
mh = lookup.findSpecial(対象クラス.class, "メソッド名", 型指定, 呼出元.class)
mh = lookup.findStatic(対象クラス.class, "メソッド名", 型指定)
mh = lookup.findStaticGetter(対象クラス.class, "フィールド名", フィールド型.class)
mh = lookup.findStaticSetter(対象クラス.class, "フィールド名", フィールド型.class)
mh = lookup.findVirtual(対象クラス.class, "メソッド名", 型指定)
mh = lookup.findGetter(対象クラス.class, "フィールド名", フィールド型.class)
mh = lookup.findSetter(対象クラス.class, "フィールド名", フィールド型.class)
「型指定」は MethodType のインスタンスで、以下のように指定します。 対象のsignatureに正確に一致している必要があります。
MethodType.methodType(返り値.class, 引数1.class, ..., 引数n.class)
対象クラスへのアクセスは、全て MethodHandle を介して行うことになります。
既存のリフレクション API とのインターフェースもあります。
mh = lookup.unreflectConstructor(Constructorオブジェクト)
mh = lookup.unreflect(Methodオブジェクト)
mh = lookup.unreflectGetter(Fieldオブジェクト)
mh = lookup.unreflectSetter(Fieldオブジェクト)
MethodHandle は全て、n組の引数を取り1つの返り値を返す『関数』として抽象化されているようです。
例えば printf の MethodHandle を表示すると以下のようになります。
MethodHandle(PrintStream,String,Object[])PrintStream
返り値 = (返り値型) mh.invoke(引数1, ..., 引数n);
返り値 = (返り値型) mh.invokeExact(引数1, ..., 引数n);
返り値 = (返り値型) mh.invokeWithArguments(引数1, ..., 引数n);
invoke() / invokeExact()
の罠実は、invoke() と invokeExact() は、JavaDoc に書いてる signature 通りの挙動をしません。
どうやら @PolymorphicSignature
というアノテーションを見て、コンパイラが何やらやっているみたいです。
import static java.lang.System.*;
printf.invoke(out, "%s", "A");
// ⇒ OK
printf.invoke(new Ojbect[] {out, "%s", "B"});
// ⇒ NG (invoke の引数は Object... のはずなのに!!)
printf.invokeExact(out, "%s", "C");
// ⇒ NG (最後の引数はStringじゃない、返り値はvoidじゃない)
PrintStream ps = (PrintStream) printf.invokeExact(out, "%s", new Object[] {"D"});
// ⇒ OK (ここまでやらんとExactにならない)
なお、invokeWithArguments() にはこのような罠はないため、可変長引数を取って invoke するメソッドを書く場合はこちらを使います。
MethodHandle は、MethodHandles のスタティックメソッドを用いて、引数や返り値をいろいろ加工することができます。
利用頻度の高そうなものを以下に抜粋します。
// 返り値を別のメソッドを通して受け取る
mh = filterReturnValue(MethodHandle, MethodHandle)
// 一部の引数を別のメソッドを通してから渡す
mh = filterArguments(MethodHandle, int, MethodHandle...)
// 一部の引数を捨ててから渡す
mh = dropArguments(MethodHandle, int, Class<?>...)
// 一部の引数を事前に設定しておく (=引数の部分適用)
mh = insertArguments(MethodHandle, int, Object...)
// 引数を入れ替えてから渡す
mh = permuteArguments(MethodHandle, MethodType, int...)
俺言語のデータ型
⇒ (引数フィルタでunwrap)
⇒ Javaのメソッド
⇒ (返り値フィルタでwrap)
⇒ 俺言語のデータ型
のようなことをやりたいときに便利です。
最初は invokedynamic のことをこう思っていました。
ダックタイピングを実現するための命令だと。
実際には、
仕組みでした。 (2012-02-23次スライド追記)
2012-02-23 追記
JJUG Night Seminar「Java SE 7 InvokeDynamicがJVM言語に与えるインパクト」で聞いた話からすると、以下のように考える方が適切のようです。
invokedynamic 命令の振る舞いを図示すると、次のようになります。
invokedynamic の実行直前
定数プールの情報をブート
ストラップメソッドに渡す
返却された CallSite を保存
CallSite から呼び出し先
を取得して実行
複数の invokedynamic
で同じ CallSite を共有
することもできる
2回目以降は直接 CallSite
から呼び出し先を取得
以上の動作を Java コードで表現すると、概ね以下のようになります。
public class InvokeDynamic {
private final String name; // 呼び出し名称
private final MethodType type; // 型指定
private final MethodHandle bsm; // ブートストラップメソッド
private final Object[] exArgs; // 任意の定数値(文字列or整数)
上記に相当する情報が class ファイルの定数プールに書き込まれます。
ブートストラップメソッド呼び出し時に使用されます。
private CallSite callSite = null; // ブートストラップメソッドの返り値
CallSite は JVM が内部情報として保持します。
CallSite.setTarget() を実行すると、同一の CallSite を共有する全ての invokedynamic が影響を受けます。
public Object invokedynamic(Object... args) throws Throwable {
if (callSite == null) {
Object[] bsmArgs = new Object[3 + exArgs.length];
bsmArgs[0] = MethodHandles.lookup(); // ※1
bsmArgs[1] = name; bsmArgs[2] = type;
for (int i = 0; i < exArgs.length; i++)
bsmArgs[3 + i] = exArgs[i];
callSite = (CallSite) bsm.invokeWithArguments(bsmArgs);
}
invokedynamic 実行時、callSite が未登録ならばブートストラップメソッドを呼び出します。
※1 invokedynamic が存在する場所のスコープで実行されますが、エミュレートでは表現できません。
MethodHandle target = callSite.getTarget(); // ※2
return target.invokeWithArguments(args);
}
callSite から呼び出し先を取得し、実行します。
※2 正確には CallSite.dynamicInvoker() 相当の処理が行われます。が、エミュレートでは型の整合が取れなくて上手く動きませんでした。
新しいものを理解するには、それを使って何か作ってみるのが良いでしょう、ということで作ってみました。
Java のメソッドを呼べる Lisp っぽい俺言語を。
どうしてこうなった。
ただし、全自動は無理だったので結局ラッパークラスは必要……。
なので、内部的にはオブジェクト指向言語に拡張可能だったり。
スコープは JavaScript ライク。何故 JavaScript がああいうスコープルールなのかをちょっと実感したり。
何かあったらとりあえず EvalException (extends RuntimeException) を throw。
MethodHandle を駆使すると、Java と、Java でない世界をつなぐのが容易になります。
キモの部分はこんな感じです。
MethodHandle mh = lookup.unreflect(method);
...
mh = MethodHandles.filterReturnValue(mh, rvConv);
mh = MethodHandles.filterArguments(mh, 0, pvConvs);
if (method.isVarArgs())
mh = mh.asVarargsCollector(IndyObject[].class);
Lookup#find* では、対象となるメソッドの signature が正確にわかっている必要があります。リフレクション API でも同様です。
名前と引数列だけが存在するときに呼ぶことが可能なメソッドを一発で取得する方法というのは、実は提供されていなかったりします。
class Sp {}
class Sc extends Sp {}
class A { public void m1(Sp s) {} }
public void m2() { m3(new A(), new Sc()); }
public void m3(Object r, Object p) {
MethodHandle mh = lookup.findVirtual(
r.getClass(), "m1", MethodType.methodType(void.class, p.getClass()));
mh.invoke(r, p);
}
p に Sc オブジェクトを渡すと、A#m1(Sp) を見付けることができません。
IndyLisp では初期化時に getMethods() でまとめて取得し、自前のルックアップテーブルを構築しています。
せめて、指定した名前のメソッドだけを抜いてくる API があれば、メソッドのフルスキャンを避けることができるのですが……。
Java の一部のメソッドには、同じ名前で同じ arity を持つメソッドが複数存在しているケースがあります。(plintln() など)
また、同じ型に対する扱いが文脈依存なケースもあります。(例: int が、数値だったりコードポイントだったり enum の代替だったり)
処理系のデータ型が、Java のメソッドの引数と 1:1 に対応付けられない場合、前述のケースが重なると、どのメソッドを呼べば良いのか自動的に判定することができません。
IndyLisp ではそのようなメソッドを見付けたとき、ラッパークラスに回避ロジックが記述されていればそちらを優先、されていなければ問答無用でエラーで落とす、という割と適当な対応になっています。
対象が対象だけに Java7 をごりごり使うわけですが、Java7 フィーチャー(<>とかマルチ catch とか)を使いまくると予想外に問題があることがわかりました。
……どうやら仕事で Java7 を使う機会は、当面なさそうです。
まー、Java6 を仕事で使えるようになったのは昨年からなので、5年くらい先の話でしょうか……。
でも、趣味じゃなきゃ Java7 使えんよなぁ……。
環境の再定義を行う処理系が bytecode を生成するケースくらいしか思い付かないのですが……。
誰か使い道を教えてください。
実はプログラミング言語を作ろうとしては毎度挫折してたのですが、Lisp (っぽいもの) ならさっくり作れてしまいました。さすが Lisp。Lisp 嫌いなんですが:-)
orz
ご静聴、ありがとうございました。
/