Создание Invokedynamic с помощью Javassist

Думаю, я пытаюсь сделать что-то относительно простое. Возьмем, к примеру, следующий байт-код Java для метода doSomething(int):

public java.lang.String doSomething(int i);
0 iload_1 [i]
1 invokestatic MyHelper.doSomething(int) : Java.lang.String
4 areturn

Этот байт-код в значительной степени перенаправляет вызов только статическому помощнику.

Что я хочу сделать сейчас, так это заменить invokestatic на invokedynamic с помощью Javassist. Я знаю, как это сделать с помощью ASM, поэтому просто предположу, что хочу знать, как это работает, из чистого любопытства. Вот несколько вопросов, которые у меня есть:

1) Верно ли следующее: я не могу использовать методы javassist CtMethod.instrument() или CtMethod.insertAt(), потому что эти методы ожидают строку, содержащую допустимое выражение Java, и я не могу написать invokedynamic в синтаксисе Java?

2) Параметры для invokestatic обрабатываются точно так же, как параметры для invokevirtual или invokestatic, верно? Я имею в виду, что вы помещаете параметры в стек перед вызовом dynamic точно так же, как если бы вы делали это для вызова виртуала?

3) Есть ли пример кода (или вы могли бы придумать), который использует Javassist для создания байт-кода invokedynamic?

Это то, что я знаю до сих пор: вы можете создать объект Bytecode, который имеет метод addInvokedynamic(). Но это предполагает индекс BootstrapMethod в BootstrapMethodsAttribute. BootstrapMethod, в свою очередь, ожидает индекс информации об дескрипторе метода в пуле констант, который хочет ссылку на метод и так далее. Таким образом, вы должны сами управлять всеми постоянными записями пула. Это нормально, но я обеспокоен тем, что я не понимаю это правильно и добавлю странные проблемы позже. Есть ли более простой способ сделать это (вспомогательный метод или около того)? Код, который у меня есть, примерно выглядит примерно так (на самом деле я не «переписываю» приведенный выше invokestatic, но:

void transform(CtMethod ctmethod, String originalCallDescriptor) {

    MethodInfo mInfo = ctmethod.getMethodInfo();
    ConstPool pool = ctmethod.getDeclaringClass().getClassFile().getConstPool();

    /* add info about the static bootstrap method to the constant pool*/
    int mRefIdx = /*somehow create a method reference entry*/
    int mHandleIdx = constPool.addMethodHandleInfo(ConstPool.REF_invokeStatic, mRefIdx);

    /* create bootstrap methods attribute; there can only be one per class file! */
    BootstrapMethodsAttribute.BootstrapMethod[] bms = new BootstrapMethodsAttribute.BootstrapMethod[] {
        new BootstrapMethodsAttribute.BootstrapMethod(mHandleIdx, new int[] {})
    };
    BootstrapMethodsAttribute bmsAttribute = new BootstrapMethodsAttribute(constPool, bms);
    mInfo.addAttribute(bmsAttribute);

    //... and then later, finally
    Bytecode bc = new Bytecode(constPool);
    ... push parameters ...
    bc.addInvokedynamic(0 /*index in bms array*/, mInfo.getName(), originalCallDescriptor);

    //replace the original method body with the one containing invokedynamic
    mInfo.removeCodeAttribute();
    mInfo.setCodeAttribute(bc.toCodeAttribute());

}

Большое спасибо за вашу помощь и время!


person Christoph    schedule 14.03.2013    source источник
comment
Итак, мой приведенный выше код кажется в значительной степени правильным. Вы получаете mRefIndex, вызывая constPool.addMethodrefInfo(), для чего вам нужен индекс, ссылающийся на класс, содержащий метод начальной загрузки (полученный с помощью constPool.addClassInfo), и информацию об имени/типе для метода начальной загрузки (через constPool.addNameAndTypeInfo()). Затем просто получите все дескрипторы ваших методов правильно и вуаля, вы там.Тем не менее, это все еще очень низкоуровневый, и мне было бы интересно узнать, есть ли лучший способ в Javassist (или будет ли он в какой-то момент) .   -  person Christoph    schedule 14.03.2013


Ответы (1)


Я должен сказать, что это довольно интересный вопрос, который у вас здесь. Извините, если мой ответ немного длинный, но я попытался дать вам как можно больше (как я думаю) полезной информации, чтобы попытаться помочь вам и другим, кто попал в этот вопрос.

Вопрос 1

Верно ли следующее: я не могу использовать методы javassist CtMethod.instrument() или CtMethod.insertAt(), потому что эти методы ожидают строку, содержащую допустимое выражение Java, и я не могу написать invokedynamic в синтаксисе Java?

Ты прав.

CtMethod.insertAt() может работать только с кодом Java, а не с кодами операций байт-кода. CtMethod.instrument(), с другой стороны, позволяет вам обрабатывать байт-код и даже изменять его очень ограниченным образом с помощью ExprEditor и CodeConverter. Но, как я уже сказал, очень ограничено то, что они позволяют вам изменить, и для того, чего вы пытаетесь достичь, оба модификатора не могут вам помочь.

вопрос 2

Параметры для invokestatic обрабатываются точно так же, как параметры для invokevirtual или invokestatic, верно? Я имею в виду, что вы помещаете параметры в стек перед вызовом dynamic точно так же, как если бы вы делали это для вызова виртуала?

Я не знаю, полностью ли я понял, о чем вы на самом деле здесь спрашиваете (вы повторили invokestatic в своем 1-м предложении). Я думаю, что вы спрашиваете — и поправьте меня, если я ошибаюсь — если параметры в invokedynamic обрабатываются так же, как и в invokevirtual и вызыватьстатический. Позволяет просто переключать invokevirtual и invokestatic на invokedynamic. Я предполагаю, что при ответе на этот вопрос...

Первое, с чем вы должны быть осторожны, это то, что invokevirtual и invokestatic сами по себе различны при обработке стека. Invokevirtual помимо помещения в стек необходимых аргументов, как это делает invokestatic, он также помещает ссылку на объект, чтобы можно было связать вызов метода.


Примечание

Вы, наверное, уже это знаете, но я добавляю эту дополнительную информацию на тот случай, если кто-то еще задаст этот вопрос и задастся вопросом, почему invokestatic и invokevirtual по-разному обрабатывают стек.

  • Код операции invokestatic используется для вызова статических методов в классе, это означает, что во время компиляции JVM точно знает, как выполнить связывание вызовов методов.

  • С другой стороны, код операции invokedynamic используется при вызове метода для экземпляра объекта. Поскольку при компиляции невозможно узнать, куда связать вызов метода, он может быть связан только во время выполнения, когда JVM знает правильную ссылку на объект.


Мой совет, если у вас есть сомнения в том, как работают коды операций, — проверить главу, посвященную набор инструкций JVM в спецификации JVM (ссылки для JVM 7, текущая версия на момент написания).

Я только что сделал это, чтобы проверить 3 опкода, о которых мы здесь говорим.

Оба кода операции, invokestatic и invokedynamic , имеют такое же определение стека операндов:

..., [arg1, [arg2 ...]] →

...

Как я уже говорил ранее, invokevirtual имеет другое определение стека операндов, а именно:

..., objectref, [arg1, [arg2 ...]] →

...

Мое первое предположение здесь (и я должен предупредить вас, что еще не слишком углублялся в код операции invokedynamic) состоит в том, что вы не можете изменить invokevirtual для invokedynamic так же просто, как с помощью invokestatic. Я говорю это потому, что invokedynamic не ожидает никаких ссылок на объекты в стеке.

Мой совет для лучшего понимания этого случая — закодировать пример на Java, используя java.lang.invoke, который позволит вам создать байт-код Java, использующий код операции invokedynamic. И после компиляции классов, проверка сгенерированного байт-кода с помощью команды javap -l -c -v -p.

Вопрос 3

Есть ли пример кода (или вы могли бы придумать), который использует Javassist для создания байт-кода invokedynamic?

Не то чтобы я в курсе. Я также немного погуглил (как и вы, вероятно, тоже) и ничего не нашел. Я думаю, что ваш пост даст первый пример кода для javassist :)

Еще несколько заметок

Таким образом, вы должны сами управлять всеми постоянными записями пула. Это нормально, но я обеспокоен тем, что я не понимаю это правильно и добавлю странные проблемы позже.

Пока вы используете ConstPool для управления вашим постоянным пулом, javassist сделает все за вас, не создавая проблем.

Кроме того, если вы создаете поврежденный пул констант, чаще всего (скорее всего, всегда) происходит то, что у вас будет ошибка ClassFormatException, как только вы попытаетесь загрузить класс или вызвать модифицированный метод. Я бы сказал, что это один из тех случаев, когда это либо работает, либо нет.

Я не могу придумать сценарий, в котором какая-то странная ошибка может быть скрыта, ожидая того неприятного момента, чтобы преследовать вас, когда вы меньше ожидаете (заметьте, что я сказал, что не могу думать, на самом деле не означает, что они не могут существует). Я бы даже рискнул сказать, что можно с уверенностью сказать, что пока вы можете загружать класс и вызывать его методы без сбоя JVM, все будет в порядке.

Есть ли более простой способ сделать это (вспомогательный метод или около того)?

Я так не думаю. Javassist очень помогает вам в модификации байт-кода, но это когда вы работаете с API более высокого уровня (например, пишете код Java и внедряете этот код или перемещаете/копируете CtMethods, Ctclasses и т. д.). Когда вы используете низкоуровневый API, где вам приходится обрабатывать весь байт-код, вы в значительной степени сами по себе.

Я знаю, что это, вероятно, не тот ответ, который вы искали, но я надеюсь, что пролил свет на эту тему.

person pabrantes    schedule 17.03.2013
comment
Большое спасибо за этот отличный ответ! Это не то, на что я надеялся (= простой способ сделать это), но определенно приятно получить всю эту справочную информацию, подтверждающую мои предположения. В этом случае мой приведенный выше код может помочь другим, которые пытаются сделать то же самое, и я предполагаю, что в какой-то момент Javassist предложит более простое решение. Что касается invokedynamic и invokevirtual: вы, конечно, на 100% правы, что у invokevirtual есть дополнительный первый параметр (this). Поэтому может потребоваться убедиться, что дескриптор метода правильный (и, возможно, добавить его в качестве первого параметра, если необходимо). - person Christoph; 21.03.2013