【问题标题】:How can I use ASM to generate invokedynamic calls that simulate invokevirtual如何使用 ASM 生成模拟 invokevirtual 的 invokedynamic 调用
【发布时间】:2018-06-09 20:46:52
【问题描述】:

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

我之所以问这个问题,是因为目前在线使用 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);

// 引导方法

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

【问题讨论】:

  • 这太抽象和假设性了。如果您想避免问题被搁置,请提供一些具体示例,说明您要完成的工作以及遇到的问题。

标签: java bytecode java-bytecode-asm invokedynamic


【解决方案1】:

在这种情况下没有什么可做的。您唯一需要注意的是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

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

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 方法,该方法将接收实际接收器实例以及解析的目标。然后它可能会根据实际接收者返回目标句柄或不同的句柄。

【讨论】:

  • 只有一个后续问题:是否可以在运行时调度接收者的具体类型?
  • 每次调用的接收者都可能不同,因此引导方法无法事先知道它,因为它必须链接到适合所有后续调用的目标。但它可以链接到将根据参数进行实际调度的代码,而不是直接链接到最终目标。但是您必须提供/实现该调度程序。也许this answer 有点帮助。
  • 感谢您的帮助。是否有关于 API 如何公开参数的文档?
  • 您链接的方法将接收它们作为参数值。如果您想创建一个可以处理各种不同参数的调度程序方法,您可以使用combiner methods 之一来调整句柄,将参数放在某种容器中(例如,像可变参数一样的数组),使用通用参数调用您的调度程序方法。或者调整它以仅获取实际的接收器作为参数,如果这是您的调度程序唯一需要的。考虑foldArguments(exactInvoker(type), …)
猜你喜欢
  • 1970-01-01
  • 2016-07-10
  • 2015-05-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-02-15
相关资源
最近更新 更多