ASM MethodVisitor :: visitMethodInsn получает дескриптор при чтении enum - ошибка или неправильное использование? - PullRequest
2 голосов
/ 02 апреля 2020

Аннотация

Я читаю файлы классов, используя ASM, и мой MethodVisitor получает странный аргумент при посещении перечисления: аргумент owner для visitMethodInsn должен быть внутренним name (например, mre/DoStuff), но для перечисления я получаю owner в форме дескриптора массива, например, [Lmre/Stuff;.

Объяснение с примером

Сокращенная Groovy версия того, как я использую ClassReader, ClassVisitor и MethodVisitor, выглядит следующим образом:

package mre

import org.objectweb.asm.*
import java.nio.file.Paths

class ClassTracer extends ClassVisitor {
    ClassTracer() { super(Opcodes.ASM8) }

    @Override
    void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        println "C:visit($version, $access, $name, $signature, $superName, $interfaces)"
    }

    @Override
    MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        println "C:visitMethod($access, $name, $descriptor, $signature, $exceptions)"
        new MethodTracer(super.visitMethod(access, name, descriptor, signature, exceptions))
    }
}

class MethodTracer extends MethodVisitor {
    MethodTracer(MethodVisitor parent) { super(Opcodes.ASM8, parent) }

    @Override
    void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
        println "M:visitMethodInsn($opcode, $owner, $name, $descriptor, $isInterface)"
        super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
    }
}

static void main(String[] args) {
    if (!args) throw new IllegalArgumentException(("Need class file path argument"))
    new ClassReader(Paths.get(args[0]).toFile().bytes).accept(new ClassTracer(), ClassReader.SKIP_FRAMES)
}

При использовании, например, с mre/OneClass.class из этого примера:

class OtherClass { void run() {} }

class OneClass {
    void runOther() {
        new OtherClass().run();
    }
}

... затем я получаю ожидаемый аргумент внутреннего имени mre/OtherClass для вызова метода run:

M:visitMethodInsn(182, mre/OtherClass, run, ()V, false)

Однако при запуске на mre/OneEnum.class этого перечисления:

enum OneEnum {a, b}

... тогда я неожиданно получаю аргумент дескриптора [Lmre/OneEnum; для посещения метода клона:

M:visitMethodInsn(182, [Lmre/OneEnum;, clone, ()Ljava/lang/Object;, false)

Хотя это несоответствие выглядит как ошибка для меня, мне интересно, я что-то упустил. Я попытался переключить сгенерированную версию байт-кода между 7,8 и 11, но, похоже, это не имеет значения.

Вопрос

Итак, в двух словах: правильно ли я использую посетителей и оправдано ли мое заблуждение относительно аргумента дескриптора перечисления?

1 Ответ

4 голосов
/ 03 апреля 2020

Получатель вызова метода может иметь тип массива.

Чтобы продемонстрировать это без использования библиотеки ASM:

public class ArrayMethodCall {
    enum SomeEnum { ;
        public static String[] example(String[] array) {
            return array.clone();
        }
    }
    public static void main(String[] args) throws IOException, InterruptedException {
        Path javap = Paths.get(System.getProperty("java.home"), "bin", "javap");
        new ProcessBuilder(
                javap.toString(), "-c",// "-v",
                "-cp", System.getProperty("java.class.path"),
                "ArrayMethodCall$SomeEnum"
        ).inheritIO().start().waitFor();
    }
}

печатает

Compiled from "ArrayMethodCall.java"
final class ArrayMethodCall$SomeEnum extends java.lang.Enum<ArrayMethodCall$SomeEnum> {
  public static ArrayMethodCall$SomeEnum[] values();
    Code:
       0: getstatic     #1                  // Field $VALUES:[LArrayMethodCall$SomeEnum;
       3: invokevirtual #2                  // Method "[LArrayMethodCall$SomeEnum;".clone:()Ljava/lang/Object;
       6: checkcast     #3                  // class "[LArrayMethodCall$SomeEnum;"
       9: areturn

  public static ArrayMethodCall$SomeEnum valueOf(java.lang.String);
    Code:
       0: ldc           #4                  // class ArrayMethodCall$SomeEnum
       2: aload_0
       3: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
       6: checkcast     #4                  // class ArrayMethodCall$SomeEnum
       9: areturn

  public static java.lang.String[] example(java.lang.String[]);
    Code:
       0: aload_0
       1: invokevirtual #7                  // Method "[Ljava/lang/String;".clone:()Ljava/lang/Object;
       4: checkcast     #8                  // class "[Ljava/lang/String;"
       7: areturn

  static {};
    Code:
       0: iconst_0
       1: anewarray     #4                  // class ArrayMethodCall$SomeEnum
       4: putstatic     #1                  // Field $VALUES:[LArrayMethodCall$SomeEnum;
       7: return
}

, который показывает, что оба вызова clone(), один в example для строкового массива и один в сгенерированном компилятором методе values() в массиве enum, используют тип массива в качестве приемника метода.

Примечание эти типы массивов также могут появляться в литералах класса (String[].class), приведениях типов и в качестве второго аргумента оператора instanceof. Приведение типа к типу массива уже происходит в показанном коде сразу после вызовов clone(). Во всех этих случаях инструкция будет ссылаться на CONSTANT_Class_info запись пула , внутреннее имя которой будет сигнатурой массива.

Рассмотрим §5.1 спецификации JVM :

Символическая c ссылка на класс или интерфейс получена из структуры CONSTANT_Class_info ( §4.4.1 ). Такая ссылка дает имя класса или интерфейса в следующем виде:

  • Для класса без массива или интерфейса имя является двоичным именем ( §4.2.1 ) класса или интерфейса.

  • Для класса массива n имя начинается с n вхождений Символ ASCII [, за которым следует представление типа элемента:

    • Если тип элемента является примитивным типом, он представляется соответствующим дескриптором поля ( §4.3. 2 ).

    • В противном случае, если тип элемента является ссылочным типом, он представлен символом ASCII L, за которым следует двоичное имя типа элемента, за которым следует символ ASCII ;.

Всякий раз, когда в этой главе упоминается имя класса или интерфейса, имя следует понимать в приведенной выше форме. (Это также форма, возвращаемая методом Class.getName.)

...