byte-buddy icon indicating copy to clipboard operation
byte-buddy copied to clipboard

Allow extracting line number from advice

Open raphw opened this issue 4 years ago • 13 comments

If possible, allow accessing the line number from advice.

raphw avatar Oct 16 '19 17:10 raphw

I see this with the 1.10.1 milestone. Was it actually implemented? If so is there any docs on how to use it? If not is there any immediate plans for it?

tylerbenson avatar Aug 10 '20 03:08 tylerbenson

Sorry for the confusion, I use the field as "reported version" and close the issue once fixed.

I did not yet have time for it, unfortunately. Was doing a lot of Mockito stuff in the last weeks but I will certainly look into it at some point!

raphw avatar Aug 10 '20 08:08 raphw

Something like this is what I had in mind...

@Advice.Origin int lineNumber
@Advice.Origin("#t.#m:#l") String stackFrame

But I'm curious to see how you decide to implement it.

tylerbenson avatar Aug 11 '20 13:08 tylerbenson

I have looked into this, thinking it's a minor change but it turns out this would require some reorganization

The way Advice is built is that it intercepts the first "code like" instruction to play off the enter advice. This code-like instruction might just be the discovery of a label onto which the line number is later applied. There is however no guarantee that the label is not used to do something else (it might even be used for multiple things). It's therefore a bit of a gamble to delay the enter code application; if the label was used for a jump target for example, the advised code might be able to escape back to the entry of the method by using this label, breaking the assumptions of the advice.

It can still be done by:

  1. caching all discovered labels until the first non-label/non-line-number instruction.
  2. storing the last line number that was discovered in a field
  3. replaying the labels and latest line number once the first non-label/non-line-number instruction is discoverd.
  4. writing back the line number with a synthetic label before (4).

I'm still planning to add this, it's just not as trivial as I hoped.

raphw avatar Sep 20 '20 13:09 raphw

Thanks for the update! Sad that it's not straightforward.

tylerbenson avatar Sep 21 '20 14:09 tylerbenson

I made some progress already, it's mainly due to the JVM not treating it as real metadata but it's overcomeable, I'm sure.

raphw avatar Sep 21 '20 15:09 raphw

Just to let you know, I am a bit stuck on this despite looking into it for quite a while. The problem is that the line number is not known when the enter advice is dispatched. In the general (but unlikely case), it might not even be known in the exit advice. I do however have a prototype that you can plug yourself. I will continue thinking about this issue, maybe there is a way to get this done properly.

public class Sample {

    public static void main(String[] args) throws Exception {
        Advice advice = Advice.withCustomMapping()
                .bind(LineNumber.class, new StackManipulation() {
                    @Override
                    public boolean isValid() {
                        return true;
                    }

                    @Override
                    public Size apply(MethodVisitor methodVisitor, Implementation.Context implementationContext) {
                        Integer line = LineExtractingMethodVisitor.LINE.get();
                        methodVisitor.visitLdcInsn(line == null ? -1 : line);
                        return new Size(1, 1);
                    }
                }, int.class)
                .to(Sample.class);

        Class<?> type = new ByteBuddy()
                .redefine(Sample.class)
                .name("linenumber.Sample")
                .visit(new AsmVisitorWrapper.ForDeclaredMethods().method(named("bar"), new LineExtractingMethodVisitorWrapper()))
                .visit(advice.on(named("bar")))
                .make()
                .load(Sample.class.getClassLoader(), ClassLoadingStrategy.Default.CHILD_FIRST)
                .getLoaded();

        Object instance = type.getConstructor().newInstance();
        type.getMethod("bar").invoke(instance);
    }

    public void bar() {
        System.out.println("In the method");
    }

    @Retention(RetentionPolicy.RUNTIME)
    public @interface LineNumber { }

    @Advice.OnMethodEnter
    @Advice.OnMethodExit
    public static void enter(@LineNumber int lineNumber) {
        System.out.println(lineNumber);
    }

    public static class LineExtractingMethodVisitorWrapper implements AsmVisitorWrapper.ForDeclaredMethods.MethodVisitorWrapper {

        @Override
        public MethodVisitor wrap(
                TypeDescription instrumentedType,
                MethodDescription instrumentedMethod,
                MethodVisitor methodVisitor,
                Implementation.Context implementationContext,
                TypePool typePool,
                int writerFlags,
                int readerFlags
        ) {
            return new LineExtractingMethodVisitor(methodVisitor);
        }
    }

    public static class LineExtractingMethodVisitor extends MethodVisitor {

        static final ThreadLocal<Integer> LINE = new ThreadLocal<Integer>() {
            @Override
            protected Integer initialValue() {
                return -1;
            }
        };

        public LineExtractingMethodVisitor(MethodVisitor methodVisitor) {
            super(Opcodes.ASM9, methodVisitor);
        }

        @Override
        public void visitLineNumber(int line, Label start) {
            LINE.set(line);
            super.visitLineNumber(line, start);
        }

        @Override
        public void visitEnd() {
            super.visitEnd();
            LINE.remove();
        }
    }
}

raphw avatar Dec 15 '20 21:12 raphw

Thanks for the update!

tylerbenson avatar Dec 16 '20 18:12 tylerbenson

Is there a major version of this feature? Are there sample tutorials to download?

bearxiongzhen avatar Mar 30 '22 16:03 bearxiongzhen

You can use the code above but there is no plan for this feature right now.

raphw avatar Mar 30 '22 20:03 raphw

I used the above example and found that adding the parameter @Advice.Origin Method method in advice OnMethodEnter will throw an exception,as shown in the following figure: image How to solve this problem?

bearxiongzhen avatar Apr 01 '22 09:04 bearxiongzhen

You can use an Executable rather then a Method type if you also want to process constructors (which cannot be represented as Method).

raphw avatar Apr 02 '22 10:04 raphw

I understand the problem, I set all the function buried points by default, just remove the constructor, thank you very much

bearxiongzhen avatar Apr 03 '22 07:04 bearxiongzhen