Skip to content

Commit

Permalink
Add ByteCode Support
Browse files Browse the repository at this point in the history
Line number data is included in the output but embedded into the code yet.
  • Loading branch information
alonalbert committed May 3, 2024
1 parent 64c3fbc commit 2c08c74
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 22 deletions.
15 changes: 9 additions & 6 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/CodeTextArea.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea
import java.awt.BasicStroke
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.Polygon
import java.awt.RenderingHints
import java.awt.geom.GeneralPath
import javax.swing.event.CaretEvent
Expand Down Expand Up @@ -104,19 +103,23 @@ open class CodeTextArea(
if (jump != null) {
val srcOffset = start + caretLine.countPadding()

var dstLine = srcLine + jump.direction
while (dstLine in 0..<lineCount) {
val (startLine, endLine) = when (jump.direction) {
1 -> srcLine + 1 to lineCount
-1 -> 0 to srcLine - 1
else -> 0 to lineCount
}

for (dstLine in startLine..endLine) {
start = getLineStartOffset(dstLine)
end = getLineEndOffset(dstLine)
val line = document.getText(start, end - start).trimEnd()

val addressed = jumpDetector.detectAddressed(line) ?: break
val addressed = jumpDetector.detectAddressed(line) ?: continue
if (addressed == jump.address) {
val dstOffset = start + line.countPadding()
jumpOffsets = JumpOffsets(srcOffset, dstOffset)
break
}

dstLine += jump.direction
}
}

Expand Down
34 changes: 33 additions & 1 deletion src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
package dev.romainguy.kotlin.explorer

import dev.romainguy.kotlin.explorer.dex.DexDumpParser
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.launch
import java.nio.file.Files
import java.nio.file.Path
import java.util.stream.Collectors
Expand All @@ -26,6 +29,7 @@ import kotlin.io.path.extension
suspend fun disassemble(
toolPaths: ToolPaths,
source: String,
onByteCode: (String) -> Unit,
onDex: (String) -> Unit,
onOat: (String) -> Unit,
onStatusUpdate: (String) -> Unit,
Expand Down Expand Up @@ -55,6 +59,22 @@ suspend fun disassemble(
return@launch
}

launch(ui) { onStatusUpdate("Disassembling bytecode…") }

val javap = process(
*buildJavapCommand(directory),
directory = directory
)

launch { onByteCode(javap.output) }

if (javap.exitCode != 0) {
launch(ui) {
onStatusUpdate("Ready")
}
return@launch
}

launch(ui) {
onStatusUpdate(if (optimize) {
"Optimizing with R8…"
Expand Down Expand Up @@ -154,6 +174,18 @@ suspend fun disassemble(
}
}

private fun buildJavapCommand(directory: Path): Array<String> {
val command = mutableListOf("javap", "-p", "-l", "-c")
val classFiles = Files
.list(directory)
.filter { path -> path.extension == "class" }
.map { file -> file.fileName.toString() }
.sorted()
.collect(Collectors.toList())
command.addAll(classFiles)
return command.toTypedArray()
}

private fun buildR8Command(
toolPaths: ToolPaths,
directory: Path,
Expand Down
45 changes: 31 additions & 14 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.SwingPanel
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.Key.Companion.B
import androidx.compose.ui.input.key.Key.Companion.D
import androidx.compose.ui.input.key.Key.Companion.F
import androidx.compose.ui.input.key.Key.Companion.G
Expand All @@ -36,6 +37,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.*
import dev.romainguy.kotlin.explorer.Shortcut.Ctrl
import dev.romainguy.kotlin.explorer.Shortcut.CtrlShift
import dev.romainguy.kotlin.explorer.bytecode.ByteCodeTextArea
import dev.romainguy.kotlin.explorer.dex.DexTextArea
import dev.romainguy.kotlin.explorer.oat.OatTextArea
import kotlinx.coroutines.launch
Expand All @@ -44,6 +46,7 @@ import org.fife.rsta.ui.search.SearchEvent
import org.fife.rsta.ui.search.SearchListener
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea
import org.fife.ui.rsyntaxtextarea.SyntaxConstants
import org.fife.ui.rsyntaxtextarea.SyntaxConstants.SYNTAX_STYLE_NONE
import org.fife.ui.rsyntaxtextarea.Theme
import org.fife.ui.rtextarea.RTextScrollPane
import org.fife.ui.rtextarea.SearchEngine
Expand All @@ -55,7 +58,10 @@ import org.jetbrains.jewel.intui.window.decoratedWindow
import org.jetbrains.jewel.intui.window.styling.dark
import org.jetbrains.jewel.intui.window.styling.light
import org.jetbrains.jewel.ui.ComponentStyling
import org.jetbrains.jewel.ui.component.*
import org.jetbrains.jewel.ui.component.DefaultButton
import org.jetbrains.jewel.ui.component.Icon
import org.jetbrains.jewel.ui.component.Text
import org.jetbrains.jewel.ui.component.TextField
import org.jetbrains.jewel.window.DecoratedWindow
import org.jetbrains.jewel.window.TitleBar
import org.jetbrains.jewel.window.newFullscreenControls
Expand Down Expand Up @@ -114,20 +120,23 @@ private fun FrameWindowScope.KotlinExplorer(
}}

val sourceTextArea = remember { sourceTextArea(focusTracker, explorerState) }
val byteCodeTextArea = remember { byteCodeTextArea(explorerState, focusTracker) }
val dexTextArea = remember { dexTextArea(explorerState, focusTracker) }
val oatTextArea = remember { oatTextArea(explorerState, focusTracker) }

val findDialog = remember { FindDialog(window, searchListener).apply { searchContext.searchWrap = true } }
var showSettings by remember { mutableStateOf(!explorerState.toolPaths.isValid) }

val sourcePanel: @Composable () -> Unit = { SourcePanel(sourceTextArea, explorerState) }
val byteCodePanel: @Composable () -> Unit = { TextPanel("Byte Code", byteCodeTextArea, explorerState) }
val dexPanel: @Composable () -> Unit = { TextPanel("DEX", dexTextArea, explorerState) }
val oatPanel: @Composable () -> Unit = { TextPanel("OAT", oatTextArea, explorerState) }
var panels by remember { mutableStateOf(explorerState.getPanels(sourcePanel, dexPanel, oatPanel)) }
var panels by remember { mutableStateOf(explorerState.getPanels(sourcePanel, byteCodePanel, dexPanel, oatPanel)) }

MainMenu(
explorerState,
sourceTextArea,
{ byteCode -> updateTextArea(byteCodeTextArea, byteCode!!) },
{ dex ->
if (dex != null) {
updateTextArea(dexTextArea, dex)
Expand All @@ -141,7 +150,7 @@ private fun FrameWindowScope.KotlinExplorer(
{ findDialog.isVisible = true },
{ SearchEngine.find(activeTextArea, findDialog.searchContext) },
{ showSettings = true },
{ panels = explorerState.getPanels(sourcePanel, dexPanel, oatPanel) },
{ panels = explorerState.getPanels(sourcePanel, byteCodePanel, dexPanel, oatPanel) },
)

if (showSettings) {
Expand Down Expand Up @@ -169,11 +178,15 @@ private fun FrameWindowScope.KotlinExplorer(

private fun ExplorerState.getPanels(
sourcePanel: @Composable () -> Unit,
byteCodePanel: @Composable () -> Unit,
dexPanel: @Composable () -> Unit,
oatPanel: @Composable () -> Unit,
): List<@Composable () -> Unit> {
return buildList {
add(sourcePanel)
if (showByteCode) {
add(byteCodePanel)
}
if (showDex) {
add(dexPanel)
}
Expand Down Expand Up @@ -231,24 +244,26 @@ private fun sourceTextArea(focusTracker: FocusListener, explorerState: ExplorerS
}
}

private fun dexTextArea(explorerState: ExplorerState, focusTracker: FocusListener): DexTextArea {
return DexTextArea(explorerState).apply {
configureSyntaxTextArea(SyntaxConstants.SYNTAX_STYLE_NONE)
addFocusListener(focusTracker)
}
}
private fun byteCodeTextArea(explorerState: ExplorerState, focusTracker: FocusListener) =
ByteCodeTextArea(explorerState).configure(focusTracker)

private fun oatTextArea(explorerState: ExplorerState, focusTracker: FocusListener): RSyntaxTextArea {
return OatTextArea(explorerState).apply {
configureSyntaxTextArea(SyntaxConstants.SYNTAX_STYLE_NONE)
addFocusListener(focusTracker)
}
private fun dexTextArea(explorerState: ExplorerState, focusTracker: FocusListener) =
DexTextArea(explorerState).configure(focusTracker)

private fun oatTextArea(explorerState: ExplorerState, focusTracker: FocusListener) =
OatTextArea(explorerState).configure(focusTracker)

private fun CodeTextArea.configure(focusTracker: FocusListener): CodeTextArea {
configureSyntaxTextArea(SYNTAX_STYLE_NONE)
addFocusListener(focusTracker)
return this
}

@Composable
private fun FrameWindowScope.MainMenu(
explorerState: ExplorerState,
sourceTextArea: RSyntaxTextArea,
onByteCodeUpdate: (String?) -> Unit,
onDexUpdate: (String?) -> Unit,
onOatUpdate: (String) -> Unit,
onStatusUpdate: (String) -> Unit,
Expand All @@ -264,6 +279,7 @@ private fun FrameWindowScope.MainMenu(
disassemble(
explorerState.toolPaths,
sourceTextArea.text,
onByteCodeUpdate,
onDexUpdate,
onOatUpdate,
onStatusUpdate,
Expand All @@ -281,6 +297,7 @@ private fun FrameWindowScope.MainMenu(
}
Menu("View") {
val onShowPanelChanged: (Boolean) -> Unit = { onPanelsUpdated() }
MenuCheckboxItem("Show ByteCode", Ctrl(B), explorerState::showByteCode, onShowPanelChanged)
MenuCheckboxItem("Show DEX", Ctrl(D), explorerState::showDex, onShowPanelChanged)
MenuCheckboxItem("Show OAT", Ctrl(O), explorerState::showOat, onShowPanelChanged)
MenuCheckboxItem("Show Line Numbers", CtrlShift(L), explorerState::showLineNumbers) {
Expand Down
2 changes: 2 additions & 0 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import kotlin.io.path.readLines
private const val Optimize = "OPTIMIZE"
private const val Presentation = "PRESENTATION"
private const val ShowLineNumbers = "SHOW_LINE_NUMBERS"
private const val ShowByteCode = "SHOW_BYTE_CODE"
private const val ShowDex = "SHOW_DEX"
private const val ShowOat = "SHOW_OAT"

Expand All @@ -37,6 +38,7 @@ class ExplorerState(
var optimize by BooleanState(Optimize, true)
var presentationMode by BooleanState(Presentation, false)
var showLineNumbers by BooleanState(ShowLineNumbers, true)
var showByteCode by BooleanState(ShowByteCode, true)
var showDex by BooleanState(ShowDex, true)
var showOat by BooleanState(ShowOat, true)
var sourceCode: String = readSourceCode(toolPaths)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2023 Romain Guy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.romainguy.kotlin.explorer.bytecode

import dev.romainguy.kotlin.explorer.CodeTextArea
import dev.romainguy.kotlin.explorer.ExplorerState
import dev.romainguy.kotlin.explorer.getValue
import dev.romainguy.kotlin.explorer.jump.RegexJumpDetector

/**
* ByteCode examples:
*
* ```
* 4: if_icmpge 31
* 10: ifne 16
* 13: goto 25
* ```
*/
private val ByteCodeJumpRegex =
Regex("^ *\\d+: (goto|if[_a-z]*) +(?<address>\\d+)(?<direction>$)")
private val ByteCodeAddressedRegex = Regex("^ *(?<address>\\d+): .+$")

// TODO: Add embedded line-number information
internal class ByteCodeTextArea(explorerState: ExplorerState) : CodeTextArea(
explorerState, RegexJumpDetector(ByteCodeJumpRegex, ByteCodeAddressedRegex),
lineNumberRegex = null
)

fun main() {
println(ByteCodeAddressedRegex.matchEntire(" 4: if_icmpge 31")?.getValue("address"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ import dev.romainguy.kotlin.explorer.getValue
class RegexJumpDetector(private val jumpRegex: Regex, private val addressedRegex: Regex) : JumpDetector {
override fun detectJump(line: String): Jump? {
val match = jumpRegex.matchEntire(line) ?: return null
return Jump(match.getValue("address").toInt(16), if (match.getValue("direction") == "+") 1 else -1)

val direction = when (match.getValue("direction")) {
"+" -> 1
"-" -> -1
"" -> 0
else -> throw IllegalArgumentException("Unexpected direction in '$line'")
}
return Jump(match.getValue("address").toInt(16), direction)
}

override fun detectAddressed(line: String) = addressedRegex.matchEntire(line)?.getValue("address")?.toInt(16)
Expand Down

0 comments on commit 2c08c74

Please sign in to comment.