5-Dec

Kotlin

Using the Bytecode Viewer and Decompiler tools in IntelliJ to lift the veil of Kotlin's magic

IDE's commonly include a lot of useful tools that can make our lives as developers easier. Some are pure productivity tools to speed up our development, while others may actually help our understanding of the code we are implementing. The toolset included with the IntelliJ IDEA IDE is no exception. In this article we will take a look at the Bytecode Viewer and Java Bytecode Decompiler integrated tools in IntelliJ, and how they may help us getting a better understanding of how our Kotlin code is represented on the JVM, and how it could potentially look in Java.

4 min read

·

By Sondre Larsen Ovrid

·

December 5, 2020

How to use the tools

First of all it is required that you have IntelliJ IDEA installed. You can use the Community or Ultimate edition, since both come bundled with the necessary tools. Then simply create or open a Kotlin file of your chosing and select Tools > Kotlin > Show Kotlin Bytecode from the menu. A new side panel will open and, given that you have a Kotlin file currently selected, will display the corresponding bytecode generated for that specific file. From here you can study the actual bytecode, or jump to another Kotlin file of your liking. The Bytecode Viewer will update automatically with the Kotlin file currently being viewed:

Now, the bytecode itself might not make much sense, unless you're already adept in interpreting it. However, if you're keen on making sense of the what the different statements actually mean, take a look at this excellent overview of the different bytecode instructions. Where things really start getting interesting, though, is when you utilize the decompilation functionality provided by clicking the Decompile button. Upon clicking the button, a file with a .decompiled.java extension will be generated for the currently selected Kotlin file. Behind the scenes, IntelliJ uses the Fernflower Java Decompiler to, in a sense, "reconstruct" Java code from the compiled bytecode. Essentially, we're able to compare our Kotlin code to a corresponding Java representation of the same code, on-the-fly. How cool is that!

A practical example

Kotlin expands upon Java by offering additional language features, while at the same time providing interoperability. It's easy to take this fact for granted, and it might seem as a bit of magic sometimes. By combining the Bytecode Viewer and Decompiler tools we now have the ability to compare Kotlin source code side-by-side with Java. If you're coming from a Java background, and you've just started with Kotlin, this can be very convenient if you quickly want to compare your Kotlin code to something more familiar.

Let's take a look at a concrete example with top-level functions in Kotlin, which Java currently do not support. Let's say we want to get an idea of how this could be represented in Java. We'll define a Kotlin file, TopLevel.kt, and declare the following top-level function:

fun myTopLevelFunction() = print("I am a top level function!") 

Let's open the Bytecode Viewer and then decompile the bytecode that IntelliJ produces. The Java Decompiler will serve us the following generated Java source code:

import kotlin.Metadata; @Metadata( mv = {1, 1, 16}, bv = {1, 0, 3}, k = 2, xi = 2, d1 = {"\u0000\b\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a\u0006\u0010\u0000\u001a\u00020\u0001¨\u0006\u0002"}, d2 = {"myTopLevelFunction", "", "kotlin-reified.main"} ) public final class TopLevelKt { public static final void myTopLevelFunction() { String var0 = "I am a top level function!"; boolean var1 = false; System.out.print(var0); } } 

The @Metadata annotation is generated by the Kotlin compiler upon compiling to bytecode. If you want to study what the different metadata-parameters mean, you can take a look at the official source code here. If we disregard the metadata information though, we will see something more familiar. The TopLevel.kt Kotlin file we defined, has been decompiled as a Java class named TopLevelKt, containing a static method named myTopLevelFunction which corresponds to the top-level function we declared earlier. This means that, in this particular case, if we wanted to use this function from Java we would be able to reference it as a static function on TopLevelKt and invoke it as any other static function in Java:

public class Main { public static void main(String[] args) { TopLevelKt.myTopLevelFunction(); } } >>> I am a top-level function! 

We've barely scratched the surface here with this simple example, but hopefully this will have shed some light on how useful these tools can be! Consider trying them out for example the next time you quickly want to compare some concept in Kotlin to Java.

A note on decompilers

An important thing to remember about decompilers is the fact that they make presumptions about the underlying bytecode. More specifically, they may assume that the bytecode was produced through very specific compilation patterns. Generally this means that the source code produced by a decompiler, i.e. the Java Decompiler that IntelliJ uses, should be treated as indicative only. For use-cases such as the one in this article, it can give us a pinpoint as to how a concept in Kotlin may be represented in Java. However there is no guarantee that every Kotlin concept can be represented entirely by native Java features.