Embed the existing code of a method in a try-finally block (2)
Asked Answered
C

3

6

Some time ago, I asked in Embed the existing code of a method in a try-finally block how to wrap the body of a method in a try-finally block using ASM. The solution was to visit a label for the try block at the beginning of the method body in visitCode() and to complete the try-finally block when visiting an instruction with a return opcode in visitInsn(). I was aware that the solution won't be working if a method has no return instruction which applies if the method is always leaving with an exception.

Though, I discovered that the former solution is sometimes inappropriate for methods with return instructions, too. It won't be working if a method has more than one return instruction. The reason is that it generates invalid bytecode because one try-finally block is added at the beginning of the method but more than one try-finally block is completed.

Usually (but probably depending on the javac compiler), a bytecode method contains a single return instruction and all return paths end at that instruction by jumping there. However, the compilation of the following code with Eclipse will lead to byte code with two return instructions:

public boolean isEven(int x) {
  return x % 2 == 0;
}

Byte code compiled with Eclipse:

   0: iload_1
   1: iconst_2
   2: irem
   3: ifne          8
   6: iconst_1
   7: ireturn       // javac compilation: goto 9
   8: iconst_0
   9: ireturn

Thus, I am wondering what the proper way to wrap the whole code of a method code is.

Comprador answered 9/2, 2015 at 17:40 Comment(0)
S
6

You have to retrace what a Java compiler does when compiling try … finally … which implies copying your finally action to every point where the protected (source) code block will be left (i.e. return instruction) and install multiple protected (resulting byte code) regions (as they shouldn’t cover your finally action) but they may all point to the same exception handler. Alternatively you can transform the code, replacing all return instruction by a branch to one instance of your “after” action followed by a sole return instruction.

That’s not trivial. So if you don’t need Hot Code Replace which usually doesn’t support adding methods to a loaded class, the easiest way to avoid all this is to rename the original method to a name not clashing with others (you may use characters not allowed in ordinary source code) and create a new method using the old name and signature which consists of a simple try … finally … construct containing an invocation of the renamed method.

E.g. change public void desired() to private void desired$instrumented() and add a new

public void desired() {
    //some logging X

    try {
        desired$instrumented();
    }
    finally {
        //some logging Y
    }
}

Note that since the debug information remains at the renamed method, stack traces will continue to report the correct line numbers if an exception is thrown in the renamed method. If you rename it by just adding an invisible character (keep in mind that you have more freedom at byte code level), it will like quite smooth.

Supple answered 9/2, 2015 at 18:43 Comment(5)
Note that redirecting all returns to a single point also means that you have to make sure the stack heights are equal. If you're only transforming ordinary compiled Java code, they probably will be already, but it's just one more issue to keep in mind.Dressage
@Antimony: right and if you don’t use ASM’s built-in stackmap calculation feature, there’s even more work to do. That’s why I recommend the move and replace solution.Supple
Thank you for your answer. I am not sure whether it is a good idea to go for the simple delegation solution because I am touching each method of the code and I will execute further bytecode analysis steps which would have to detect and skip the delegation methods. I think I will try to introduce multiple blocks (in the example from 0 - 7 and 8 - 9). The idea of replacing all returns by a jump to a single return sounds good too, but it does not cover explicitly thrown exceptions (ATHROW).Comprador
@nrainer: There's two parts. First you have to wrap all the code in a catch Throwable exception handler to handle exceptions thrown out of the method, then you also have to insert your code before returns (being careful of course not to catch exceptions from your own inserted code if you don't want to).Dressage
@Antimony, Holger: Thank you for your help, I think I found a working solution.Comprador
C
4

Thanks to the answer of Holger and the comments of Antimony I developed the following solution which satisfies my needs. Later I found that a similar approach is also described in Using ASM framework to implement common bytecode transformation patterns, E. Kuleshov, AOSD.07, March 2007, Vancouver, Canada.

This solution does not work for methods which contain no non-exceptional return (methods which throw an exception in every execution path, such as throw new NotSupportedOperationException();)!

If you need to support these methods as well you should follow Holger's advice to rename the original method and then to add a new method with the old name. Add a delegate call in the added method to the renamed method and embed the invocation in a try-finally block.


I use a simple MethodVisitor to visit the code. In the visitCode() method I add instructions to be executed when entering the method. Then, I add the beginning of the try block by visiting a new Label. When I visit a return opcode in visitInsn() I will complete the try block and add the finally block. Moveover, I add a new Label to begin a new try block in case the method contains further return instructions. (If no return instructions follow the label visit won't have any effect.)

The simplified code is as follows:

public abstract class AbstractTryFinallyMethodVisitor extends MethodVisitor {

  private Label m_currentBeginLabel;
  private boolean m_isInOriginalCode = true;

  protected void execBeforeMethodCode() {
    // Code at the beginning of the method and not in a try block
  }

  protected void execVisitTryBlockBegin() {
    // Code at the beginning of each try block
  }

  protected void execVisitFinallyBlock() {
    // Code in each finally block
  }

  @Override
  public void visitCode() {
    try {
      m_isInOriginalCode = false;
      execBeforeMethodCode();
      beginTryFinallyBlock();
    }
    finally {
      m_isInOriginalCode = true;
    }
  }

  protected void beginTryFinallyBlock() {
    m_currentBeginLabel = new Label();
    visitLabel(m_currentBeginLabel);
    execVisitTryBlockBegin();
  }

  @Override
  public void visitInsn(int opcode) {
    if (m_inOriginalCode && isReturnOpcode(opcode) {
      try {
        m_isInOriginalCode = false;
        completeTryFinallyBlock();

        super.visitInsn(opcode);

        beginTryBlock();
      }
      finally {
        m_isInOriginalCode = true;
      }
    }
    else {
      super.visitInsn(opcode);
    }
  }

  protected void completeTryFinallyBlock() {
    Label l1 = new Label();
    visitTryCatchBlock(m_currentBeginLabel, l1, l1, null);
    Label l2 = new Label();
    visitJumpInsn(GOTO, l2);
    visitLabel(l1);
    // visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] { "java/lang/Throwable" });
    visitVarInsn(ASTORE, 1);

    execVisitFinallyBlock();

    visitVarInsn(ALOAD, 1);
    super.visitInsn(ATHROW);
    visitLabel(l2);
    // visitFrame(Opcodes.F_SAME, 0, null, 0, null);

    execVisitFinallyBlock();
  }

   protected static boolean isReturnOpcode(int opcode) {
     return opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN;
   }
}

Notes:

  • You are not supposed to invoke visitFrame if you instantiate the ClassWriter with the COMPUTE_FRAMES flag.
  • It is also possible (and possibly preferable) to use the AdviceAdapter and to perform the bytecode manipulation in its onMethodEnter() and onMethodExit() methods.
  • As mentioned earlier, the try-finally block will only be added if the bytecode contains at least one return instruction.

The transformed byte code output for the isEven() method of the question will be:

public boolean isEven(int);
Code:
 0: ldc           #22                 //isEven(int)
 2: invokestatic  #28                 //log/Logger.push:(Ljava/lang/String;)V
 5: iload_1                  *1*
 6: iconst_2                 *1*  
 7: irem                     *1*
 8: ifne          25         *1*
11: iconst_1                 *1*
12: goto          21         *1*
15: astore_1
16: invokestatic  #31                 //log/Logger.pop:()V
19: aload_1            
20: athrow
21: invokestatic  #31                 //log/Logger.pop:()V
24: ireturn
25: iconst_0                 *2*
26: goto          35         *2*
29: astore_1
30: invokestatic  #31                 //log/Logger.pop:()V
33: aload_1
34: athrow
35: invokestatic  #31                 //log/Logger.pop:()V
38: ireturn

Exception table:
 from    to  target type
     5    15    15   any     *1*
    25    29    29   any     *2*
}
Comprador answered 9/2, 2015 at 17:40 Comment(0)
B
3

It is impossible to wrap whole constructors into try-finally blocks because try blocks cannot span a call to a super constructor. Although I couldn't find this limitation in the specification, I could find two tickets that discuss it: JDK-8172282, asm #317583.

If you don't care about constructors, you can wrap methods into other methods that catch the exceptions as written by Holger. It is an uncomplicated solution that might be fine in many scenarios. This answer, however, describes an alternative solution that does not require the generation of a second method.


The solution is roughly based on "Compiling finally" in the JVM specification. The solution uses the JSR instruction. The instruction is not supported since langue level 7. Therefore, we use the JSRInlinerAdapter to replace the instructions afterwards.

We will start by creating our own MethodVisitor. Note that we extend MethodNode instead of MethodVisitor. We do this to collect the whole method before we pass the information to the next visitor. More about that later.

public class MyMethodVisitor extends MethodNode {

The visitor needs three labels. The first label designates the begin of the original content and the begin of the try block. The second label designates the end of the original content and the end of the try block. It also designates the start of the exception handler. The last label designates the subroutine that represents the finally block.

  private final Label originalContentBegin = new Label();
  private final Label originalContentEnd = new Label();
  private final Label finallySubroutine = new Label();

The constructor reuses the field mv of MethodVisitor. It is not used by MethodNode. We could have created our own field as well. The constructor also creates the JSRInlinerAdapter to replace the JSR instruction as mentioned above.

  public MyMethodVisitor(
      MethodVisitor methodVisitor,
      int access, String name, String descriptor,
      String signature, String[] exceptions)
  {
    super(Opcodes.ASM8, access, name, descriptor, signature, exceptions);
    mv = new JSRInlinerAdapter(methodVisitor, access, name, descriptor, signature, exceptions);
  }

Next, we declare the methods which generate the bytecode that shall be executed before and after the original code is executed.

  protected void generateBefore() { /* Generate your code here */ }
  protected void generateAfter() { /* Generate your code here */ }

According to the Javadoc of MethodVisitor, ASM calls

  • visitCode() before the content of the method is visited, and
  • visitMaxs(int,int) after the content of the method is visited.

Before ASM visits the content of the method, we want to inject our own bytecode and visit our label which designates the begin of the original content.

  @Override
  public void visitCode() {
    super.visitCode();
    generateBefore();
    super.visitLabel(originalContentBegin);
  }

Whenever the original method returns, we want to call our code of the finally block.

  @Override
  public void visitInsn(int opcode) {
    if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) {
      super.visitJumpInsn(Opcodes.JSR, finallySubroutine);
    }
    super.visitInsn(opcode);
  }

At the end of the method, we inject the exception handler for our try block and the subroutine that contains the finally block.

  @Override
  public void visitMaxs(int maxStack, int maxLocals) {
    super.visitLabel(originalContentEnd);
    super.visitJumpInsn(Opcodes.JSR, finallySubroutine);
    super.visitInsn(Opcodes.ATHROW);

    super.visitLabel(finallySubroutine);
    super.visitVarInsn(Opcodes.ASTORE, 0);
    generateAfter();
    super.visitVarInsn(Opcodes.RET, 0);

    super.visitMaxs(maxStack, maxLocals);
  }

Finally, we have to create the try-catch block and forward the method to the next method visitor. We couldn't create the try-catch block earlier using the visitor pattern because of an unfavorable order of visitTryCatchBlock(…) calls (see issue #317617). This This is why we extended MethodNode instead of MethodVisitor.

  @Override
  public void visitEnd() {
    super.visitEnd();
    tryCatchBlocks.add(new TryCatchBlockNode(
        getLabelNode(originalContentBegin),
        getLabelNode(originalContentEnd),
        getLabelNode(originalContentEnd),
        null));
    accept(mv);
  }
}

Since the transformation does not work with constructors, our method visitor can be used like this in a ClassVisitor.

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
  if (name.equals("<init>")) {
    return super.visitMethod(access, name, descriptor, signature, exceptions);
  }
  else {
    return new MyMethodVisitor(
        super.visitMethod(access, name, descriptor, signature, exceptions),
        access, name, descriptor, signature, exceptions);
  }
}

There is still some room for improvement.

  • You could avoid the JSR instruction and remove the JSRInlinerAdapter. This might also provide some opportunities to reduce the size of the generated code because the JSRInlinerAdapter might duplicate the code of the finally block multiple times.

  • Even so you cannot catch exceptions of the super constructor, you could add limited support for constructors that handles exceptions before and after the super constructor is called.

Anyway, this changes might also make the code much more complicated.

Bearish answered 28/6, 2020 at 19:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.