Skip to content

Commit

Permalink
Add a Code Model
Browse files Browse the repository at this point in the history
This makes it easier to detect jumps and toggle show-line-number
  • Loading branch information
alonalbert committed May 5, 2024
1 parent da90411 commit ded97c2
Show file tree
Hide file tree
Showing 15 changed files with 512 additions and 289 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ kotlin {
implementation("net.java.dev.jna:jna:${extra["jna.version"] as String}")
implementation("androidx.collection:collection:${extra["collections.version"] as String}")
implementation("org.jetbrains.androidx.lifecycle:lifecycle-runtime:${extra["lifecycle.version"] as String}")
implementation("com.google.guava:guava:${extra["guava"]}")
runtimeOnly("org.jetbrains.skiko:skiko-awt-runtime-linux-x64:${extra["skiko.version"] as String}")
}
}
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ jewel.version=0.17.3
jna.version=5.13.0
collections.version=1.4.0
lifecycle.version=2.8.0-alpha01
guava=33.2.0-jre
99 changes: 19 additions & 80 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,27 @@

package dev.romainguy.kotlin.explorer

import dev.romainguy.kotlin.explorer.code.CodeContent
import dev.romainguy.kotlin.explorer.code.CodeContent.Error
import dev.romainguy.kotlin.explorer.dex.DexDumpParser
import kotlinx.coroutines.*
import dev.romainguy.kotlin.explorer.oat.OatDumpParser
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
import kotlin.io.path.extension

private val dexDumpParser = DexDumpParser()
private val oatDumpParser = OatDumpParser()

suspend fun disassemble(
toolPaths: ToolPaths,
source: String,
onDex: (String) -> Unit,
onOat: (String) -> Unit,
onDex: (CodeContent) -> Unit,
onOat: (CodeContent) -> Unit,
onStatusUpdate: (String) -> Unit,
optimize: Boolean
) = coroutineScope {
Expand All @@ -49,7 +58,7 @@ suspend fun disassemble(

if (kotlinc.exitCode != 0) {
launch(ui) {
onDex(kotlinc.output.replace(path.parent.toString() + "/", ""))
onDex(Error(kotlinc.output.replace(path.parent.toString() + "/", "")))
onStatusUpdate("Ready")
}
return@launch
Expand All @@ -72,7 +81,7 @@ suspend fun disassemble(

if (r8.exitCode != 0) {
launch(ui) {
onDex(r8.output)
onDex(Error(r8.output))
onStatusUpdate("Ready")
}
return@launch
Expand All @@ -89,14 +98,14 @@ suspend fun disassemble(

if (dexdump.exitCode != 0) {
launch(ui) {
onDex(dexdump.output)
onDex(Error(dexdump.output))
onStatusUpdate("Ready")
}
return@launch
}

launch(ui) {
onDex(DexDumpParser(dexdump.output).parseDexDump())
onDex(dexDumpParser.parse(dexdump.output))
onStatusUpdate("AOT compilation…")
}

Expand All @@ -110,7 +119,7 @@ suspend fun disassemble(

if (push.exitCode != 0) {
launch(ui) {
onOat(push.output)
onOat(Error(push.output))
onStatusUpdate("Ready")
}
return@launch
Expand All @@ -127,7 +136,7 @@ suspend fun disassemble(

if (dex2oat.exitCode != 0) {
launch(ui) {
onOat(dex2oat.output)
onOat(Error(dex2oat.output))
onStatusUpdate("Ready")
}
return@launch
Expand All @@ -143,7 +152,7 @@ suspend fun disassemble(
directory = directory
)

launch(ui) { onOat(filterOat(oatdump.output)) }
launch(ui) { onOat(oatDumpParser.parse(oatdump.output)) }

if (oatdump.exitCode != 0) {
launch(ui) { onStatusUpdate("Ready") }
Expand Down Expand Up @@ -251,73 +260,3 @@ private fun cleanupClasses(directory: Path) {

internal val BuiltInKotlinClass = Regex("^(kotlin|kotlinx|java|javax|org\\.(intellij|jetbrains))\\..+")

private val OatClassNamePattern = Regex("^\\d+: L([^;]+); \\(offset=[0-9a-zA-Zx]+\\) \\(type_idx=\\d+\\).+")
private val OatMethodPattern = Regex("^\\s+\\d+:\\s+(.+)\\s+\\(dex_method_idx=\\d+\\)")
private val OatCodePattern = Regex("^\\s+(0x[a-zA-Z0-9]+):\\s+[a-zA-Z0-9]+\\s+(.+)")

private fun filterOat(oat: String) = buildString {
val indent = " "
val lines = oat.lineSequence().iterator()

if (!lines.consumeUntil("OatDexFile:")) return@buildString

var insideClass = false
var insideMethod = false
var firstMethod = false
var firstClass = true

while (lines.hasNext()) {
val line = lines.next()

var match: MatchResult? = null

if (insideClass) {
if (insideMethod) {
match = OatCodePattern.matchEntire(line)
if (match != null && match.groupValues.isNotEmpty()) {
appendLine("$indent${match.groupValues[1]}: ${match.groupValues[2]}")
}
}

if (match === null) {
match = OatMethodPattern.matchEntire(line)
if (match != null && match.groupValues.isNotEmpty()) {
val name = match.groupValues[1]
if (!firstMethod) appendLine()
firstMethod = false

appendLine(" $name")

if (!lines.consumeUntil("CODE: ")) break
insideMethod = true
}
}
}

if (match === null) {
match = OatClassNamePattern.matchEntire(line)
if (match != null && match.groupValues.isNotEmpty()) {
val className = match.groupValues[1].replace('/', '.')

val suppress = className.matches(BuiltInKotlinClass)
if (!suppress) {
if (!firstClass) appendLine()
appendLine("class $className")
}

insideMethod = false
firstMethod = true
firstClass = false
insideClass = !suppress
}
}
}
}

internal fun Iterator<String>.consumeUntil(prefix: String): Boolean {
while (hasNext()) {
val line = next()
if (line.trim().startsWith(prefix)) return true
}
return false
}
67 changes: 39 additions & 28 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ 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.dex.DexTextArea
import dev.romainguy.kotlin.explorer.oat.OatTextArea
import dev.romainguy.kotlin.explorer.code.CodeBuilder.LineNumberMode.FixedWidth
import dev.romainguy.kotlin.explorer.code.CodeBuilder.LineNumberMode.None
import dev.romainguy.kotlin.explorer.code.CodeContent
import dev.romainguy.kotlin.explorer.code.CodeTextArea
import kotlinx.coroutines.launch
import org.fife.rsta.ui.search.FindDialog
import org.fife.rsta.ui.search.SearchEvent
Expand All @@ -55,7 +57,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 All @@ -67,6 +72,8 @@ import javax.swing.SwingUtilities

private const val FontSizeEditingMode = 12.0f
private const val FontSizePresentationMode = 20.0f
private const val LINE_NUMBER_WIDTH = 4
private const val CODE_INDENT = 4

@Composable
private fun FrameWindowScope.KotlinExplorer(
Expand Down Expand Up @@ -125,22 +132,23 @@ private fun FrameWindowScope.KotlinExplorer(
val oatPanel: @Composable () -> Unit = { TextPanel("OAT", oatTextArea, explorerState) }
var panels by remember { mutableStateOf(explorerState.getPanels(sourcePanel, dexPanel, oatPanel)) }

val updatePresentationMode: (Boolean) -> Unit = {
listOf(dexTextArea, oatTextArea).forEach { area -> area.presentationMode = it }
}
val updateShowLineNumbers: (Boolean) -> Unit = { dexTextArea.lineNumberMode = it.toLineNumberMode() }

MainMenu(
explorerState,
sourceTextArea,
{ dex ->
if (dex != null) {
updateTextArea(dexTextArea, dex)
} else {
dexTextArea.refreshText()
}
},
{ oat -> updateTextArea(oatTextArea, oat) },
{ dex -> dexTextArea.content = dex },
{ oat -> oatTextArea.content = oat },
{ statusUpdate -> status = statusUpdate },
{ findDialog.isVisible = true },
{ SearchEngine.find(activeTextArea, findDialog.searchContext) },
{ showSettings = true },
{ panels = explorerState.getPanels(sourcePanel, dexPanel, oatPanel) },
updateShowLineNumbers,
updatePresentationMode,
)

if (showSettings) {
Expand Down Expand Up @@ -232,15 +240,18 @@ 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 dexTextArea(state: ExplorerState, focusTracker: FocusListener) =
codeTextArea(state, focusTracker)

private fun oatTextArea(state: ExplorerState, focusTracker: FocusListener) =
codeTextArea(state, focusTracker, hasLineNumbers = false)

private fun oatTextArea(explorerState: ExplorerState, focusTracker: FocusListener): RSyntaxTextArea {
return OatTextArea(explorerState).apply {
private fun codeTextArea(
state: ExplorerState,
focusTracker: FocusListener,
hasLineNumbers: Boolean = true
): CodeTextArea {
return CodeTextArea(state.presentationMode, CODE_INDENT, hasLineNumbers.toLineNumberMode()).apply {
configureSyntaxTextArea(SyntaxConstants.SYNTAX_STYLE_NONE)
addFocusListener(focusTracker)
}
Expand All @@ -250,13 +261,15 @@ private fun oatTextArea(explorerState: ExplorerState, focusTracker: FocusListene
private fun FrameWindowScope.MainMenu(
explorerState: ExplorerState,
sourceTextArea: RSyntaxTextArea,
onDexUpdate: (String?) -> Unit,
onOatUpdate: (String) -> Unit,
onDexUpdate: (CodeContent) -> Unit,
onOatUpdate: (CodeContent) -> Unit,
onStatusUpdate: (String) -> Unit,
onFindClicked: () -> Unit,
onFindNextClicked: () -> Unit,
onOpenSettings: () -> Unit,
onPanelsUpdated: () -> Unit,
onShowLineNumberChanged: (Boolean) -> Unit,
onPresentationModeChanged: (Boolean) -> Unit,
) {
val scope = rememberCoroutineScope()

Expand Down Expand Up @@ -285,10 +298,12 @@ private fun FrameWindowScope.MainMenu(
MenuCheckboxItem("Show DEX", Ctrl(D), explorerState::showDex, onShowPanelChanged)
MenuCheckboxItem("Show OAT", Ctrl(O), explorerState::showOat, onShowPanelChanged)
MenuCheckboxItem("Show Line Numbers", CtrlShift(L), explorerState::showLineNumbers) {
onDexUpdate(null)
onShowLineNumberChanged(it)
}
Separator()
MenuCheckboxItem("Presentation Mode", CtrlShift(P), explorerState::presentationMode)
MenuCheckboxItem("Presentation Mode", CtrlShift(P), explorerState::presentationMode) {
onPresentationModeChanged(it)
}
}
Menu("Compilation") {
MenuCheckboxItem("Optimize with R8", CtrlShift(O), explorerState::optimize)
Expand Down Expand Up @@ -406,11 +421,7 @@ private fun RSyntaxTextArea.updateStyle(explorerState: ExplorerState) {
font = font.deriveFont(if (presentation) FontSizePresentationMode else FontSizeEditingMode)
}

private fun updateTextArea(textArea: RSyntaxTextArea, text: String) {
val position = textArea.caretPosition
textArea.text = text
textArea.caretPosition = minOf(position, textArea.document.length)
}
private fun Boolean.toLineNumberMode() = if (this) FixedWidth(LINE_NUMBER_WIDTH) else None

fun main() = application {
val explorerState = remember { ExplorerState() }
Expand Down
2 changes: 2 additions & 0 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Regex.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package dev.romainguy.kotlin.explorer

const val HexDigit = "[0-9a-fA-F]"

fun MatchResult.getValue(group: String): String {
return groups[group]?.value ?: throw IllegalStateException("Value of $group not found in $value")
}
36 changes: 36 additions & 0 deletions src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/String.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2024 The Android Open Source Project
*
* 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

fun Iterator<String>.consumeUntil(prefix: String): Boolean {
while (hasNext()) {
val line = next()
if (line.trim().startsWith(prefix)) return true
}
return false
}

fun Iterator<String>.consumeUntil(regex: Regex): MatchResult? {
while (hasNext()) {
val line = next()
val match = regex.matchEntire(line)
if (match != null) {
return match
}
}
return null
}
Loading

0 comments on commit ded97c2

Please sign in to comment.