如何使用ASM生成模拟invokevirtual

时间:2018-06-09 20:46:53

标签: java bytecode java-bytecode-asm invokedynamic

我想了解如何使用与invokedynamic相同的调度逻辑进行invokevirtual次调用。

我问的是这个问题,因为目前在线使用ASM生成动态方法调用的例子太简单了,我认为这个案例对于任何想要实现自己的调度逻辑的人来说都是一个很好的起点。

显然,我知道只需用invokevirtual来取代invokedynamic次来电,这在实践中是没有意义的。

要明确我想替换它:

methodVisitor.visitMethodInsn(
    Opcodes.INVOKEVIRTUAL,
    myClassName,
    methodName,
    descriptor,
    false);

用这个:

MethodType methodType =
    MethodType.methodType(
        CallSite.class,
        MethodHandles.Lookup.class,
        String.class,
        MethodType.class);

Handle handle =
    new Handle(
        Opcodes.H_INVOKESTATIC,
        "bytecode/generating/Class",
        "bootstrap",
        methodType.toMethodDescriptorString(),
        false);

methodVisitor.visitInvokeDynamicInsn(
    methodName,
    descriptor,
    handle);

// bootstrap方法

public static CallSite bootstrap(
    MethodHandles.Lookup caller,
    String name,
    MethodType type)
{
    // Dispatch logic here.
}

1 个答案:

答案 0 :(得分:2)

在这种情况下没有太多事情要做。你必须要关心的唯一事情是,invokevirtual有一个隐含的第一个参数,即接收器,你必须作为显式的第一个参数插入到invokedynamic指令的描述符中:

public class ConvertToInvokeDynamic extends MethodVisitor {
    public static byte[] convertInvokeVirtual(
        InputStream in, String linkerClass, String linkerMethod) throws IOException {
        ClassReader cr = new ClassReader(in);
        ClassWriter cw = new ClassWriter(cr, 0);
        cr.accept(new ClassVisitor(Opcodes.ASM5, cw) {
            @Override
            public MethodVisitor visitMethod(int access, String name, String desc,
                                             String signature, String[] exceptions) {
                return new ConvertToInvokeDynamic(
                    super.visitMethod(access, name, desc, signature, exceptions),
                    linkerClass, linkerMethod);
            }
        }, 0);
        return cw.toByteArray();
    }
    private final Handle bsm;

    public ConvertToInvokeDynamic(
        MethodVisitor target, String linkerClass, String linkerMethod) {
        super(Opcodes.ASM5, target);
        bsm = new Handle(Opcodes.H_INVOKESTATIC, linkerClass, linkerMethod,
          "(Ljava/lang/invoke/MethodHandles$Lookup;"
         + "Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;");
    }

    @Override
    public void visitMethodInsn(
        int opcode, String owner, String name, String desc, boolean itf) {
        if(opcode == Opcodes.INVOKEVIRTUAL) {
            desc = '('+(owner.charAt(0)!='['? 'L'+owner+';': owner)+desc.substring(1);
            super.visitInvokeDynamicInsn(name, desc, bsm);
        }
        else super.visitMethodInsn(opcode, owner, name, desc, itf);
    }
}

只要这是唯一的更改,堆栈状态将与原始代码保持一致,因此,我们不需要重新计算堆栈帧,也不需要重新计算最大变量/操作数堆栈大小。

代码假定原始类的版本足以支持invokedynamic指令。否则,转换将变得非常重要,因为我们不仅可能需要计算堆栈映射,我们还可能在旧的类文件中遇到现在被禁止的jsrret指令。

提供重新建立原始invokevirtual行为的引导方法也很简单。现在,最大(不是很大)的障碍是我们现在必须提取第一个显式参数类型并将其转换回接收器类型:

public class LinkLikeInvokeVirtual {
    public static CallSite bootstrap(MethodHandles.Lookup l, String name, MethodType type){
        Class<?> receiver = type.parameterType(0);
        type = type.dropParameterTypes(0, 1);
        System.out.println("linking to "+name+type+" in "+receiver);
        MethodHandle target;
        try {
            target = l.findVirtual(receiver, name, type);
        } catch(NoSuchMethodException|IllegalAccessException ex) {
            throw new BootstrapMethodError(ex);
        }
        return new ConstantCallSite(target);
    }
}

现在,我们可以在一个简单的测试用例中组合这两个类:

public class Test {
    public static void main(String[] args) throws IOException,ReflectiveOperationException{
        byte[] code;
        try(InputStream is = Test.class.getResourceAsStream("Test.class")) {
            code = ConvertToInvokeDynamic.convertInvokeVirtual(is,
                LinkLikeInvokeVirtual.class.getName(), "bootstrap");
        }
        Class<?> transformed = new ClassLoader() {
            Class<?> get() {return defineClass("Test", code, 0, code.length); }
        }.get();
        transformed.getMethod("example").invoke(null);
    }

    public static void example() {
        System.out.println(Runtime.getRuntime().freeMemory()+" bytes free");
    }
}

其转化后的example()生成

linking to freeMemory()long in class java.lang.Runtime
linking to append(long)StringBuilder in class java.lang.StringBuilder
linking to append(String)StringBuilder in class java.lang.StringBuilder
linking to toString()String in class java.lang.StringBuilder
linking to println(String)void in class java.io.PrintStream
131449472 bytes free
第一次执行时

(因为链接的调用站点保持链接,所以我们在下次调用时不会看到bootstrap方法的输出。)

StringBuilder方法是在Java 9之前编译的字符串连接的工件,因此从Java 9开始,它只会打印

linking to freeMemory()long in class java.lang.Runtime
linking to println(String)void in class java.io.PrintStream
131449472 bytes free

(当然,数字会有所不同)

如果您想根据实际的接收器执行替代动态调度,您可以用以下内容替换LinkLikeInvokeVirtual

public class LinkWithDynamicDispatch {
    static final MethodHandle DISPATCHER;
    static {
        try {
            DISPATCHER = MethodHandles.lookup().findStatic(LinkWithDynamicDispatch.class, "simpleDispatcher",
                MethodType.methodType(MethodHandle.class, MethodHandle.class, String.class, Object.class));
        } catch(NoSuchMethodException|IllegalAccessException ex) {
            throw new ExceptionInInitializerError(ex);
        }
    }
    public static CallSite bootstrap(MethodHandles.Lookup l, String name, MethodType type){
        MethodHandle target;
        try {
            target = l.findVirtual(type.parameterType(0), name, type.dropParameterTypes(0, 1));
        } catch(NoSuchMethodException|IllegalAccessException ex) {
            throw new BootstrapMethodError(ex);
        }
        MethodHandle d = MethodHandles.insertArguments(DISPATCHER, 0, target, name);
        target = MethodHandles.foldArguments(MethodHandles.exactInvoker(type),
            d.asType(d.type().changeParameterType(0, type.parameterType(0))));
        return new ConstantCallSite(target);
    }
    public static MethodHandle simpleDispatcher(
            MethodHandle invokeVirtualTarget, String methodName, Object rec) {
        System.out.println("simpleDispatcher(): invoke "+methodName+" on "
            + "declared receiver type "+invokeVirtualTarget.type().parameterType(0)+", "
            + "actual receiver "+(rec==null? "null": "("+rec.getClass().getName()+"): "+rec));
        return invokeVirtualTarget;
    }
}

这将基于静态类型执行类似invokevirtual的查找,然后链接到simpleDispatcher方法,该方法将另外接收已解析目标的实际接收方实例。然后它可以根据实际的接收器返回目标句柄或不同的句柄。