Как стать автором
Обновить

Пошаговая отладка, inline-методы, JVM

Время на прочтение6 мин
Количество просмотров2.6K


В Java, как известно, inline-методов нет. Но такое понятие существует в других языках, исполняющихся на JVM. Например, в Scala или Kotlin. Во время компиляции вызов такого метода заменяется на его тело, как если бы разработчик написал этот код вручную.

Прекрасный инструмент для добавления синтаксического сахара и создания проблемно-ориентированных языков (DSL) малой ценой, но как это всё отлаживать?

С тем, какие ухищрения помогают не замечать расхождения исходного текста программы и её байткода во время отладки и предлагаю разобраться.

В этом сезоне в моде Kotlin, так что рассмотрим на его примере. Для экспериментов возьмём простейший, всего 17 строк, пример с вызовом inline-функции, принимающей лямбду в качестве параметра.

fun main() {
    println(">> main()")
    inlineMethod(true) {
        println("\t\tLambda function executed.")
    }
    println("<< main()")
}

inline fun inlineMethod(flag: Boolean, body: () -> Unit) {
    println("\t>> inlineMethod()")
    if (flag) {
        body()
    }
    println("\t<< inlineMethod()")
}


Дизассемблировав class-файл примера можно убедиться, что и inline-метод и переданная ему параметром лямбда действительно были встроены в тело метода main(). Для удобства чтения инструкции байткода приведены как комментарии к строкам исходного кода на Kotlin.

fun main() {
    println(">> main()")
//  0: ldc           #8                  // String >> main() 
//  2: getstatic     #14                 // Field java/lang/System.out:Ljava/io/PrintStream; 
//  5: swap 
//  6: invokevirtual #20                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 
    inlineMethod(true) {
//  9: iconst_1 
// 10: istore_0 
// 11: iconst_0 
// 12: istore_1 
    println("\t>> inlineMethod()")
// 13: ldc           #22                 // String \t>> inlineMethod() 
// 15: getstatic     #14                 // Field java/lang/System.out:Ljava/io/PrintStream; 
// 18: swap 
// 19: invokevirtual #20                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 
    if (flag) {
// 22: nop 
        body()
// 23: iconst_0 
// 24: istore_2 
        println("\t\tLambda function executed.")
// 25: ldc           #24                 // String \t\tLambda function executed. 
// 27: getstatic     #14                 // Field java/lang/System.out:Ljava/io/PrintStream; 
// 30: swap 
// 31: invokevirtual #20                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 
    }
// 34: nop 
// 35: nop 
    println("\t<< inlineMethod()")
// 36: ldc           #26                 // String \t<< inlineMethod() 
// 38: getstatic     #14                 // Field java/lang/System.out:Ljava/io/PrintStream; 
// 41: swap 
// 42: invokevirtual #20                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 
}
// 45: nop 
    println("<< main()")
// 46: ldc           #28                 // String << main() 
// 48: getstatic     #14                 // Field java/lang/System.out:Ljava/io/PrintStream; 
// 51: swap 
// 52: invokevirtual #20                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 
}
// 55: return 

Самое время проверить, как с этим справится отладчик.
JDB, консольный отладчик из состава JDK, совершенно ничего не знает ни про Kotlin, ни про inline-методы, а потому идеально подходит для нашего исследования.

Запустим отладку.

jdb -classpath … -sourcepath … SimpleInlineKt
Initializing jdb ...
> stop at SimpleInlineKt:3
Deferring breakpoint SimpleInlineKt:3.
It will be set after the class is loaded.
> run
run SimpleInlineKt
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
> 
VM Started: Set deferred breakpoint SimpleInlineKt:3

Breakpoint hit: "thread=main", SimpleInlineKt.main(), line=3 bci=0
3        println(">> main()")

main[1] step
>

>> main()

Step completed: "thread=main", SimpleInlineKt.main(), line=4 bci=9
4        inlineMethod(true) {

main[1] step
>

Step completed: "thread=main", SimpleInlineKt.main(), line=11 bci=13
11        println("\t>> inlineMethod()")

main[1] step
>

	>> inlineMethod()

Step completed: "thread=main", SimpleInlineKt.main(), line=12 bci=22
12        if (flag) {

main[1] step
>

Step completed: "thread=main", SimpleInlineKt.main(), line=13 bci=23
13            body()

main[1] step
>

Step completed: "thread=main", SimpleInlineKt.main(), line=5 bci=25
5            println("\t\tLambda function executed.")

main[1] step
>

		Lambda function executed.

Step completed: "thread=main", SimpleInlineKt.main(), line=6 bci=34
6        }

main[1] step
>

Step completed: "thread=main", SimpleInlineKt.main(), line=13 bci=35
13            body()

main[1] step
>

Step completed: "thread=main", SimpleInlineKt.main(), line=15 bci=36
15        println("\t<< inlineMethod()")

main[1] step
>

	<< inlineMethod()

Step completed: "thread=main", SimpleInlineKt.main(), line=16 bci=45
16    }

main[1] step
>

Step completed: "thread=main", SimpleInlineKt.main(), line=7 bci=46
7        println("<< main()")

main[1] step
>

<< main()

Step completed: "thread=main", SimpleInlineKt.main(), line=8 bci=55
8    }

main[1] step
>

Step completed: "thread=main", SimpleInlineKt.main(), line=-1 bci=3

main[1] step
>
The application exited


Если оставить за скобками различия в интерфейсе, то всё работает точно так же, как и в IDE от JetBrains. Мы посещаем правильные строки, в правильном порядке. Inline-методы отладчику никак не мешают.

Глубже в class-файл


Как показал эксперимент с jdb, вся нужная для отладки информация уже содержится в class-файле. Изучим его пристальнее, стандартного дизассемблера javap будет достаточно.

Выполнив команду javap -c -v SimpleInlineKt.class помимо байткода, который мы уже видели ранее, мы обнаружим некоторые интересные особенности.

Первой будет странная таблица номеров строк:

      LineNumberTable:
        line 3: 0
        line 4: 9
        line 18: 13
        line 19: 22
        line 20: 23
        line 5: 25
        line 6: 34
        line 20: 35
        line 22: 36
        line 23: 45
        line 7: 46
        line 8: 55

Эта таблица задаёт соответствие между смещением в байткоде и номером строки. Как можно заметить, номера строк в ней идут не совсем по порядку, что странно. Между четвёртой и пятой появились строки 18, 19 и 20, а между шестой и седьмой — строки 20, 22 и 23.

Вторая особенность — аттрибут SourceDebugExtension:

SourceDebugExtension:
  SMAP
  SimpleInline.kt
  Kotlin
  *S Kotlin
  *F
  + 1 SimpleInline.kt
  SimpleInlineKt
  *L
  1#1,17:1
  11#1,6:18
  *S KotlinDebug
  *F
  + 1 SimpleInline.kt
  SimpleInlineKt
  *L
  4#1:18,6
  *E

Именно этот аттрибут — ключ к разгадке.

Он был разработан в рамках JSR-045: Debugging Support for Other Languages («поддержка отладки для других языков»). В первую очередь, конечно же, это делалось ради Java2EE и Java Server Pages, помните такие аббревиатуры?

При его помощи можно задать правила перевода номеров строк, записанных в class-файле в пары (имя файла, новый номер строки).

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

Как кто-то сказал, «все проблемы в программировании решаются путём создания дополнительного уровня косвенности».

Рассмотрим на примере


Вначале идёт заголовок — сигнатура SMAP, имя исходного файла, имя набора правил по умолчанию. В терминах JSR-045 набор правил называется «слой» (Stratum).

  SMAP
  SimpleInline.kt
  Kotlin

Затем идёт декларация слоя с именем «Kotlin».

  *S Kotlin

Наш inline-метод определён в том же файле, что и используется и потому в секции файлов у нас ровно одна запись, назначающая файлу SimpleInline.kt идентификатор 1.

  *F
  + 1 SimpleInline.kt
  SimpleInlineKt

В секции номеров строк у нас две записи:

  *L
  1#1,17:1
  11#1,6:18

  • Первые 17 строк отображаются с номеров строк в class-файле на номера строк в исходном файле один-к-одному.
  • «Виртуальные» строки в class-файле с 18 по 23 отображаются на строки декларации inline-метода в исходном файле

На этом декларация слоя заканчивается.

Дальше идёт дополнительный слой KotlinDebug, используемый в IntelliJ IDEA для реконструкции стека вызовов с учётом inline-функций, но для пошаговой отладки он не критичен. Интересующиеся деталями могут найти их в InlineStackTraceCalculator.kt.

  *S KotlinDebug
  *F
  + 1 SimpleInline.kt
  SimpleInlineKt
  *L
  4#1:18,6

Окончание данных промаркировано секцией *E:

  *E

А что там со Scala?


В 2017 году появилось предложение поработать над удобством отладки под номером SCP-011:
A major criticism of Scala is that the debugging experience is poor compared to Java. The main reason for this is because Scala's representation as JVM bytecode is not always intuitive. Although visual debuggers (Scala IDE, IntelliJ and ENSIME) are able to hide much of the demangling detail from the developer, there remains a great deal of ambiguity regarding the block of code that is executing.

В 2019 году про это вспомнили вновь и создали предложение SCP-022:
This is a proposal to prioritize the completion of SCP-11. To reduce the scope of SCP-11, this proposal suggests to focus only contributing JSR-45 support to the Scala 2 compiler.

В середине 2020 появился Pull Request #9121 для реализации этой функциональности в Scala 2.13.x, проделан некий объём работ, но всё закончилось закрытием PR в 2021 году по неактивности.

Сейчас открыт PR #15684, нацеленный на Scala 3:
Rebase of #11492 to the latest main. At Scala Center, we're planning to bring this over the finish line.

Пожелаем разработчикам порвать финишную ленту этого семилетнего супермарафона.

Заключение


В Kotlin есть поддержка inline-методов и для упрощения отладки таких методов применяется изящный подход с «виртуальными» строками. Виртуальные строки назначаются байткоду заинлайненой функции и затем отображаются на реальные строки в исходном файле при помощи спецификации JSR-045, разработанной в далёких нулевых для поддержки Java 2 Enterprise Edition.

В каждой программе на Kotlin есть немного Ынырпрайза.
Теги:
Хабы:
Всего голосов 4: ↑3 и ↓1+2
Комментарии4

Публикации