大昔、「Javaのクラス内が依存しているフィールド、メソッドの一覧を表示する」という記事を書いたのだが、久々に使ってみるとどうも一部のメソッド呼び出しが抜けている。いろいろ調べていたのだが、なるほど invoke dynamic の仕業だった。
なんせ前回の記事を書いたのが 2013 年だから 7 年前か。無駄に歳を重ねてしまったものだ。おそらく当時は Java 1.7 だった……と考えるとそれほど変わったわけでもないな。1.6 と 1.8 の時代が大変長かったし、世の中的にはまだまだ Java 8 が現役だ。
閑話休題。invoke dynamic 命令自体は Java 1.7 の頃から存在はしたものの、コンパイラが生成したコードに含まれるようになったのは Java 8 からで、主にラムダ式に使われている。そのため、前回の記事の時点ではあのコードで十分だったのだが、Java 8 以降で使うと漏れが発生してしまうわけだ。
そういうわけで、Javassist で invoke dynamic を扱う方法を調べていたのだが、これが意外とやっかいで困ってしまった。invoke dynamic 命令自体は、クラスの属性として定義されている Bootstrap Methods 属性に定義されている Bootstrap Method 配列の番号しか持っていない。そして Bootstrap Method は、実際のメソッドの呼び出しをラッピングするメソッド ハンドルを持っている。結局、invoke dynamic 命令⇒Bootstrap Methods 属性⇒Bootstrap Method 配列のひとつ⇒メソッド ハンドル⇒実際のメソッドと4回たどる必要があった。
最終的なソースコードは以下の通り。Bootstrap Method を取得するところと invoke virtual の前処理を除けば前回から大きな違いはない。
ClassPool cp = ClassPool.getDefault(); CtClass cc = cp.get(クラス名); BootstrapMethod[] bsms = null; for (AttributeInfo attr : cc.getClassFile().getAttributes()) { if (attr instanceof BootstrapMethodsAttribute) { BootstrapMethodsAttribute bsma = (BootstrapMethodsAttribute)attr; bsms = bsma.getMethods(); } } for (CtBehavior cb : cc.getDeclaredBehaviors()) { MethodInfo info = cb.getMethodInfo2(); ConstPool pool = info.getConstPool(); CodeAttribute code = info.getCodeAttribute(); if (code == null) return; CodeIterator i = code.iterator(); while (i.hasNext()) { int pos = i.next(); int opecode = i.byteAt(pos); int dynIndex = -1; if (opecode == Opcode.INVOKEDYNAMIC) { int index = i.u16bitAt(pos + 1); int bootstrap = pool.getInvokeDynamicBootstrap(index); BootstrapMethod bm = bsms[bootstrap]; for (int arg : bm.arguments) { int tag = pool.getTag(arg); if (tag == ConstPool.CONST_MethodHandle) { switch (pool.getMethodHandleKind(arg)) { case ConstPool.REF_invokeVirtual: opecode = Opcode.INVOKEVIRTUAL; dynIndex = pool.getMethodHandleIndex(arg); break; case ConstPool.REF_invokeSpecial: opecode = Opcode.INVOKESPECIAL; dynIndex = pool.getMethodHandleIndex(arg); break; case ConstPool.REF_invokeStatic: opecode = Opcode.INVOKESTATIC; dynIndex = pool.getMethodHandleIndex(arg); break; case ConstPool.REF_invokeInterface: opecode = Opcode.INVOKEINTERFACE; dynIndex = pool.getMethodHandleIndex(arg); break; } break; } } } switch (opecode) { case Opcode.NEW: case Opcode.ANEWARRAY: case Opcode.MULTIANEWARRAY: { int index = i.u16bitAt(pos + 1); System.out.print(cc.getName()); System.out.print('\t'); System.out.print(cb.getLongName()); System.out.print('\t'); System.out.print("new"); System.out.print('\t'); String className = pool.getClassInfo(index); if (opecode == Opcode.ANEWARRAY || opecode == Opcode.MULTIANEWARRAY) { className = "new " + className + "[]"; } System.out.print(className); System.out.println(); break; } case Opcode.NEWARRAY: { System.out.print(cc.getName()); System.out.print('\t'); System.out.print(cb.getLongName()); System.out.print('\t'); System.out.print("new"); System.out.print('\t'); String className = null; switch (i.byteAt(pos + 1)) { case Opcode.T_BOOLEAN: className = "new boolean[]"; break; case Opcode.T_CHAR: className = "new char[]"; break; case Opcode.T_BYTE: className = "new byte[]"; break; case Opcode.T_SHORT: className = "new short[]"; break; case Opcode.T_INT: className = "new int[]"; break; case Opcode.T_LONG: className = "new long[]"; break; case Opcode.T_FLOAT: className = "new float[]"; break; case Opcode.T_DOUBLE: className = "new double[]"; break; } System.out.print(className); System.out.println(); break; } case Opcode.CHECKCAST: { int index = i.u16bitAt(pos + 1); System.out.print(cc.getName()); System.out.print('\t'); System.out.print(cb.getLongName()); System.out.print('\t'); System.out.print("cast"); System.out.print('\t'); String className = "(" + pool.getClassInfo(index) + ")"; System.out.print(className); System.out.println(); break; } case Opcode.GETSTATIC: case Opcode.PUTSTATIC: case Opcode.GETFIELD: case Opcode.PUTFIELD: { int index = i.u16bitAt(pos + 1); System.out.print(cc.getName()); System.out.print('\t'); System.out.print(cb.getLongName()); System.out.print('\t'); System.out.print("field"); String className = pool.getFieldrefClassName(index); String fieldName = pool.getFieldrefName(index); String desc = pool.getFieldrefType(index); String typeName = Descriptor.toClassName(desc); System.out.print('\t'); System.out.print(typeName); System.out.print(' '); System.out.print(className); System.out.print('.'); System.out.print(fieldName); System.out.println(); break; } case Opcode.INVOKEVIRTUAL: case Opcode.INVOKESPECIAL: case Opcode.INVOKESTATIC: case Opcode.INVOKEINTERFACE: { int index = dynIndex == -1 ? i.u16bitAt(pos + 1) : dynIndex; System.out.print(cc.getName()); System.out.print('\t'); System.out.print(cb.getLongName()); System.out.print('\t'); System.out.print("method"); String className; if (opecode == Opcode.INVOKEINTERFACE) { className = pool.getInterfaceMethodrefClassName(index); } else { className = pool.getMethodrefClassName(index); } String methodName; if (opecode == Opcode.INVOKEINTERFACE) { methodName = pool.getInterfaceMethodrefName(index); } else { methodName = pool.getMethodrefName(index); } String desc; if (opecode == Opcode.INVOKEINTERFACE) { desc = pool.getInterfaceMethodrefType(index); } else { desc = pool.getMethodrefType(index); } String ret = null; List<String> params = new ArrayList<String>(); if (desc.charAt(0) == '(') { int start = 1; boolean inClass = false; for (int j = start; j < desc.length(); j++) { char c = desc.charAt(j); if (inClass) { if (c == ';') { params.add(Descriptor.toClassName(desc.substring(start, j + 1))); start = j + 1; inClass = false; } } else if (c == ')') { ret = Descriptor.toClassName(desc.substring(j + 1)); break; } else if (c == 'L') { inClass = true; } else if (c != '[') { params.add(Descriptor.toClassName(desc.substring(start, j + 1))); start = j + 1; } } } else { ret = Descriptor.toClassName(desc); } System.out.print('\t'); System.out.print(ret); System.out.print(' '); System.out.print(className); System.out.print('.'); System.out.print(methodName); System.out.print('('); for (int j = 0; j < params.size(); j++) { if (j > 0) System.out.print(','); System.out.print(params.get(j)); } System.out.println(')'); break; } } } }