Search code examples
javabytecodejava-bytecode-asm

Implementation of 'visitMethodInsn' method of MethodNode in ASM library


This is the body of visitMethodInsn method of MethodNode class:

  @Override
  public void visitMethodInsn(
      final int opcode,
      final @InternalForm String owner,
      final @Identifier String name,
      final @MethodDescriptor String descriptor,
      final boolean isInterface) {
    if (api < Opcodes.ASM5) {
      super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
      return;
    }
    instructions.add(new MethodInsnNode(opcode, owner, name, descriptor, isInterface));
  }

As you can see, the bytecode isn't added to the instructions list if the asm api version is less than 5. What's the reason behind it?


Solution

  • Some sleuthing through the history on Gitlab shows that the code in question was added in this commit, with the commit message Added support for invokespecial and invokestatic on interfaces.

    Java 8 introduced the ability to define non abstract methods in interfaces, known as default methods. At the bytecode level, the effect of the change is that you can now use interface methods as well as class methods with the invokespecial and invokestatic instructions.

    Prior to Java 8, when generating bytecode, you could determine the type of the constant pool entry simply by the instruction: if the opcode is invokeinterface, generate an InterfaceMethod entry, otherwise generate a Method entry. In Java 8, this is no longer possible, because invokespecial and invokestatic are ambiguous, which means that the user needs to be able to explicitly pass in whether the method is an interface method or not. This means that they had to add an extra parameter to pretty much all the method apis.

    However, they didn't want to break backwards compatibility, which means that they need to keep the methods with the old signature (i.e. no itf parameter). These methods will forward to the new ones, with itf defaulting to true for invokeinterface instructions, and false otherwise, which seems like a reasonable default. This is what the supercall you see above does. I'm not sure why the API < 5 switch is there, but I suspect that it is either to ensure backwards compatibility, or to break an infinite loop in their method dispatch scheme.

    On a side note, MethodNode was deleted around 8 months ago as part of a major code reorganization, so you won't see it in the most recent version of ASM.

    Edit: I see that you are confused about the method delegation. It is pretty tricky, since there are four different methods involved.

    For reference, here is the code involved: MethodNode:

    @Deprecated
    @Override
    public void visitMethodInsn(int opcode, String owner, String name,
            String desc) {
        if (api >= Opcodes.ASM5) {
            super.visitMethodInsn(opcode, owner, name, desc);
            return;
        }
        instructions.add(new MethodInsnNode(opcode, owner, name, desc));
    }
    
    @Override
    public void visitMethodInsn(int opcode, String owner, String name,
            String desc, boolean itf) {
        if (api < Opcodes.ASM5) {
            super.visitMethodInsn(opcode, owner, name, desc, itf);
            return;
        }
        instructions.add(new MethodInsnNode(opcode, owner, name, desc, itf));
    }
    

    And then in the superclass, there is

    @Deprecated
    public void visitMethodInsn(int opcode, String owner, String name,
            String desc) {
        if (api >= Opcodes.ASM5) {
            boolean itf = opcode == Opcodes.INVOKEINTERFACE;
            visitMethodInsn(opcode, owner, name, desc, itf);
            return;
        }
        if (mv != null) {
            mv.visitMethodInsn(opcode, owner, name, desc);
        }
    }
    
    public void visitMethodInsn(int opcode, String owner, String name,
            String desc, boolean itf) {
        if (api < Opcodes.ASM5) {
            if (itf != (opcode == Opcodes.INVOKEINTERFACE)) {
                throw new IllegalArgumentException(
                        "INVOKESPECIAL/STATIC on interfaces require ASM 5");
            }
            visitMethodInsn(opcode, owner, name, desc);
            return;
        }
        if (mv != null) {
            mv.visitMethodInsn(opcode, owner, name, desc, itf);
        }
    }
    

    Prior to the change, only the methods without the itf parameter existed. The overloaded versions are new.

    If you look closely, you can see that the effect of all the delegation is that when API < 5, it will end up calling the old method, regardless of which of the two you call. If you call the new method, it will verify the itf parameter before delegating. When API >= 5, it will end up calling the new method, regardless of which of the two you call. If you call the old method, it will choose a default value for itf before delegating.

    So it is not ignoring the method call, it is simply delegating to the correct implementation.