java - 如何使用 ASM 生成模拟 invokevirtual 的 invokedynamic 调用

标签 java bytecode java-bytecode-asm invokedynamic

我想看看如何使用与 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.
}

最佳答案

在这种情况下没有什么可做的。您唯一需要关心的是 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 行为的 Bootstrap 方法,也是直截了当的。现在,最大的(不是很大)障碍是我们现在必须提取第一个显式参数类型并将其转换回接收者类型:

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 方法,该方法将额外接收实际的接收器实例到已解析的目标。然后它可能只返回目标句柄或不同的句柄,具体取决于实际的接收者。

关于java - 如何使用 ASM 生成模拟 invokevirtual 的 invokedynamic 调用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50778399/

相关文章:

java - 通过附加 API 检测 Java 类

Java流不匹配多个条件

java - 如何在asm访问字段方法中提取字段的访问标志

java - 如何使用bytebuddy检测无法访问的代码?

php - 如何查看php字节码文件

java - 获取ASM字节码库中的泛型信息

java - 无法理解在 Java 中使用 ASM 字节码重命名的方法

java - 打开新的JFrame时如何停止音频

java - 将 Adob​​e Flash 播放器嵌入到基于 Java 的桌面应用程序中

Java函数参数值正在改变