Offset error when writing type annotations on NEW instructions

Rafael Winterhalter rafael.wth at gmail.com
Mon Nov 11 09:17:53 UTC 2024


Hei Chen,

I think I understand the problem then. With ASM, type annotations on
elements are processed just after the element was encountered. From the
javadoc of ASM: "Visits an annotation on an instruction. This method must
be called just after the annotated instruction." In contrast, the Class
File API requires a label to be inserted "just before" the instruction is
added.

Ideally, CodeBuilder would allow me to call something like
"newBoundLabelOnPreviousInstruction", but I understand if you do not want
to add something like it. If the Class File API allowed to add labels both
prior or after encountering an instruction, one could work with both
approaches of inserting type annotations prior or after an instruction is
added. This would make migrating from ASM more straight-forward, too, as I
assume that this is the prevalent model today with ASM being so dominant.

Otherwise, I will need to work around and delay any ASM invocation for my
bridge, to consider if a subsequent call represents a type annotation and
apply a reordering this way. This is not a big deal, but of course it would
be much easier solved if I could bind a label with a BCI offset further up
the stack. Possibly, I could even add a label before any annotatable
instruction, just in case. Would this be overly expensive to do?

Cheers, Rafael

Am Mo., 11. Nov. 2024 um 00:12 Uhr schrieb Chen Liang <
chen.l.liang at oracle.com>:

> Hi Rafael,
> I believe this is due to a misunderstanding with how ClassFile API labels
> work.  A ClassFile API label models an insertion point represented by a
> BCI; so in your example, the bound label should be located right before the
> new instruction, so that the next immediately executed instruction would be
> the new instruction.  This is like how the labels for control flows work.
>
> Unfortunately, I understand this is distinct from the ASM model - ASM
> visitor model treats this as if this is a tiny tree structure and considers
> a type annotation as a subsidiary of an instruction, and this brings
> challenges to porting the implementations.  Here's some context: we
> ClassFile API developers long believed that type annotations interact
> poorly with class file transformations because of hard-to-track arbitrary
> references such as interface indices, and thus we did not integrate it as a
> part of the ClassFile API code streaming model, and we drops type
> annotation attributes in transformations by the attribute stability
> settings.
>
> Regards,
> Chen Liang
> ------------------------------
> *From:* classfile-api-dev <classfile-api-dev-retn at openjdk.org> on behalf
> of Rafael Winterhalter <rafael.wth at gmail.com>
> *Sent:* Saturday, November 9, 2024 3:09 PM
> *To:* classfile-api-dev <classfile-api-dev at openjdk.org>
> *Subject:* Offset error when writing type annotations on NEW instructions
>
> With the most recent build of JDK 24, I discover one test failure in my
> test suite when comparing with ASM (
> https://github.com/raphw/asm-jdk-bridge): Any type annotation that is
> added to a "new" instruction is added to the subsequent instruction
> instead. This might irritate parsers as the label will point to an
> instruction that cannot normally be annotated, as NEW is normally followed
> by DUP.
>
> I created a reproducers that compares a javac compiled class and one that
> is created with the class file API. The error is likely in the attribute
> mapper's write method as I cannot reproduce it when testing a class file
> reader using the class file API compared to ASM. The reproducer is as
> follows and is visualized in the first line of each of the two
> representations where the correct byte code offset is 0, not 3 as the
> annotation is added to the first instruction in the method:
>
>
> import java.io.InputStream;
> import java.lang.annotation.ElementType;
> import java.lang.annotation.Retention;
> import java.lang.annotation.RetentionPolicy;
> import java.lang.annotation.Target;
> import java.lang.classfile.*;
> import java.lang.classfile.attribute.RuntimeVisibleTypeAnnotationsAttribute;
> import java.lang.classfile.instruction.LineNumber;
> import java.lang.classfile.instruction.LocalVariable;
> import java.lang.constant.ClassDesc;
> import java.lang.constant.MethodTypeDesc;
> import java.util.List;
>
> public class TypeAnnotationSample {
>
>     void m() {
>         new @A Object();
>     }
>
>     public static void main(String[] args) throws Exception {
>         byte[] compiled;
>         try (InputStream inputStream = TypeAnnotationSample.class.getClassLoader().getResourceAsStream(
>             TypeAnnotationSample.class.getName().replace('.', '/') + ".class"
>         )) {
>             compiled = inputStream.readAllBytes();
>         }
>         print(compiled);
>         System.out.println("-----");
>         byte[] created = ClassFile.of().build(ClassDesc.of("Synthetic"), classBuilder -> {
>             classBuilder.withMethod("m", MethodTypeDesc.of(void.class.describeConstable().orElseThrow()), 0, methodBuilder -> {
>                 methodBuilder.withCode(codeBuilder -> {
>                     codeBuilder.new_(Object.class.describeConstable().orElseThrow());
>                     Label label = codeBuilder.newBoundLabel();
>                     codeBuilder.with(RuntimeVisibleTypeAnnotationsAttribute.of(TypeAnnotation.of(
>                             TypeAnnotation.TargetInfo.ofNewExpr(label),
>                             List.of(),
>                             Annotation.of(A.class.describeConstable().orElseThrow())
>                     )));
>                     codeBuilder.dup()
>                             .invokespecial(Object.class.describeConstable().orElseThrow(), "<init>", MethodTypeDesc.of(void.class.describeConstable().orElseThrow()))
>                             .pop()
>                             .return_();
>                 });
>             });
>         });
>         print(created);
>     }
>
>     private static void print(byte[] classFile) {
>         ClassModel classModel = ClassFile.of().parse(classFile);
>         MethodModel methodModel = classModel.methods().stream()
>                 .filter(element -> element.methodName().equalsString("m"))
>                 .findFirst()
>                 .orElseThrow();
>         CodeModel codeModel = methodModel.code().orElseThrow();
>         codeModel.elementStream().forEach(element -> {
>             switch (element) {
>                 case LocalVariable _ -> { }
>                 case LineNumber _ -> { }
>                 case RuntimeVisibleTypeAnnotationsAttribute a -> System.out.println(a.annotations().stream()
>                         .map(x -> x.annotation()
>                                 + "@" + x.targetPath()
>                                 + "/" + ((TypeAnnotation.OffsetTarget) x.targetInfo()).target())
>                         .toList());
>                 default -> System.out.println(element);
>             }
>         });
>     }
>
>     @Retention(RetentionPolicy.RUNTIME)
>     @Target(ElementType.TYPE_USE)
>     @interface A {
>     }
> }
>
>
> I think that the attribute mapper must subtract the bytes of the
> instruction as it is currently adding the bytes at the end of the
> instruction, not at its beginning. ASM and javac set the annotation offset
> to the beginning of the statement, so I understand that this should be the
> right approach.
>
> Thanks, Rafael
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/classfile-api-dev/attachments/20241111/19eea698/attachment.htm>


More information about the classfile-api-dev mailing list