Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Disassembling Kotlin Features - DroidCon Italy ...

Disassembling Kotlin Features - DroidCon Italy 2024

Andrea Cioccarelli

November 29, 2024
Tweet

More Decks by Andrea Cioccarelli

Other Decks in Technology

Transcript

  1. Disassembling Kotlin Features A journey through Kotlin’s language compilation and

    Android’s runtime architecture @cioccarellia • November 29 •
  2. Introduction • Talk idea from working with lower level languages,

    where much more direct representation exists between source code and executable • Exploring what is between Kotlin source and Android execution • Developers model systems with <black|white> boxes • Throughline: adding to your mental model a general overview of those underlying systems we rely on when developing
  3. Heap Stack pc Constant Pool Method Area Specification Platform Language

    -> jvm spec, bytecode -> compulation, execution -> features & high-level Kotlin Android ART ELF when guards Nullability data classes conventions default arguments destructuring
  4. Specification 1. Abstract JVM 2. Data Types 3. Class File

    4. Runtime Data Areas 5. Comparison w/ Linux-like formats Heap Stack pc Constant Pool Method Area
  5. Abstract JVM From The JVM Specification: […], specifying the abstract

    Java Virtual Machine, serving as documentation for a concrete implementation only as blueprints document a house. An implementation of the JVM must embody this specification, but is constrained only when necessary. • Specification define an abstract computing machine • Stack-based virtual machine, 256 instruction (1-byte opcode) [1]
  6. Abstract JVM • JVM knows nothing about Java • Acts

    as intermediary between language and platform • Has its own ISA (bytecode), hardware indepentent JVM Java Scala Clojure Linux Kotlin Windows MacOS Languages targets JVM, Platforms implements JVM
  7. Abstract JVM Sometimes language restrictions aren’t because of constraints in

    the JVM, but because of the language itself • CPUs have their own machine code representation (assembly) • JVM bridges that gap with bytecode • Virtual execution environment for languages to target
  8. Abstract JVM - > Android Android targets different processors, devices

    and architectures This enables deploying the same code over different devices • Android uses a runtime similar to the JVM to run apps • Heavily modified and optimized • Allows for flexibility iOS targets a homogeneous set of devices, only one streamlined CPU architecture; its applications are compiled binaries
  9. Class File • Each Java/Kotlin class/interface is compiled into one

    .class file • Simple binary format, stores all information about the class (e.g. methods, fields, visibility, superclass, …) • May be packaged together (e.g. .jar file) • Output of the compiler, input to the JVM All your codebase eventually gets compiled to a bunch of classfiles
  10. Class File magic 0x00 0x03 minor_version 0x04 0x05 major_version 0x06

    0x07 Magic Magic number for .class files, 4 bytes, 0xcafebabe constant_pool_count 0x08 0x09 constant_pool
  11. Class File minor_version 0x04 0x05 major_version 0x06 0x07 Version(s) Contains

    version of the class file, specifying for which JVM specification it has been implemented e.g. Java 8 is 52.0, Java 11 is 55.0 constant_pool_count 0x08 0x09 constant_pool access_flags C + 0x01 C + 0x02 C 0x0A
  12. Class File Constant Pool Table of symbols, containing all constant

    values for the class i.e. strings, numbers, references, classes, internal structures constant_pool_count 0x08 0x09 constant_pool access_flags C + 0x01 C + 0x02 C 0x0A this_class super_class interfaces_count C + 0x03 C + 0x04 C + 0x05 C + 0x06 C + 0x07 C + 0x08
  13. Class File Access Flags Mask of flags, used to define

    properties about the class e.g. public, final, abstract super, interface, synthethic, annotation, enum access_flags C + 0x01 C + 0x02 this_class super_class interfaces_count C + 0x03 C + 0x04 C + 0x05 C + 0x06 C + 0x07 C + 0x08 interfaces C + 0x09 I
  14. Class File This & Super Offset in the constant_pool definining

    the name of the current and super class Can be zero if no superclass this_class super_class interfaces_count C + 0x03 C + 0x04 C + 0x05 C + 0x06 C + 0x07 C + 0x08 interfaces C + 0x09 I fields_count I + 0x01 I + 0x02
  15. Class File Interfaces Offsets in the constant_pool representing interface classes

    implemented by the current class interfaces_count C + 0x07 C + 0x08 interfaces C + 0x09 I fields_count I + 0x01 I + 0x02 fields I + 0x03 F
  16. Class File Fields Contains information for every field explicitly defined

    in the current class e.g. private/public, type fields_count I + 0x01 I + 0x02 fields I + 0x03 F methods_count methods F + 0x01 F + 0x02 F + 0x03 M
  17. Class File Methods Contains information for every method explicitly defined

    in the current class e.g. private/public, type methods_count methods F + 0x01 F + 0x02 F + 0x03 M attributes_count attributes M + 0x01 M + 0x02 M + 0x03 A
  18. Class File Attributes Contains all the meta- information about the

    whole class structure not specified in other fields e.g. annotations, debugging, exceptions, deprecation, line numbers attributes_count attributes M + 0x01 M + 0x02 M + 0x03 size-1 Fields are like class variables Attributes are meta-info Attributes only encode data defined by the JVM Spec
  19. Data Types Divied into either primitive or reference types Primitives

    References Basic data types, representing raw values No methods, references or any other special function Faster to work with Types referring to objects or arrays They hold a reference to an object on the heap @NotNull @Nullable
  20. Data Types / Primitives • Rationale: basic data types should

    have an explicit, value-based representation instead of being a refrence • Many bytecode instructions are tailored per type - > Addition: iadd, ladd, fadd, dadd - > Mutliply: imul, lmul, fmul, dmul - > Increment: iinc - > Conversion: l2i, f2i, d2f, …
  21. Runtime Data Areas •Load the .class file into memory, moving

    information from storage into an address space •Each class section stored in different memory area •Runtime relies on this memory division
  22. Method Area Method Area Contains in-memory fields, methods and constructors

    for the current class Loaded form the class file Created on JVM startup
  23. Method Area Runtime Constant Pool Runtime Constant Pool Runtime representation

    of the constant_pool for the class file Loaded form the class file Created on class creation
  24. Method Area Runtime Constant Pool Heap Heap Memory region used

    to store instances of objects Dynamically allocated as bytecode requests new object instantiations Created on JVM startup, GC’d The JVM bytecode instruction new is used to allocate heap memory and invoke the constructor for a certain class
  25. Method Area Call Stack “The stack in stacktrace” Contains the

    list of all the calls to past functions On function call -> stack pushed On return -> stack popped Runtime Constant Pool Heap Stack LIFO data structure, holds the activation record (aka frame) for the current method call
  26. Method Area Program Counter Per-thread register, containing address of current

    instruction being executed (either in method area or native) Runtime Constant Pool Heap Stack pc Only explicit register that the JVM specs mandate
  27. Method Area Multi-threaded model Many threads may concurrently access and

    execute a given class The only per-thread elements are the stack and the pc Runtime Constant Pool Heap Thread #1 Thread #2 Stack pc Stack pc
  28. Thread #2 Stack pc Local Variables Operand Stack One frame

    per function Frame on top of the stack is the one associated with the currently executing instruction Frame
  29. Thread #2 Stack pc Local Variables Operand Stack Initially contains

    list of parameters passed to the method currently positioned on the stack <stack+n> Frame fun pow(base: Int, exp: Int) Tstore instructions save values to local variables
  30. Thread #2 Stack pc Local Variables Operand Stack Initially empty

    Used by instructions to store operands for successive instructions taking them as argument <stack+n> Frame Tload instructions push values onto operand stack
  31. Bytecode analysis: Adder.kt Let’s now analyze a program performing a

    sum of two numbers in its decompiled form fun add() { val i = 15 val j = 10 return i + j } Adder.kt bipush 15 istore_0 bipush 10 istore_1 iload_0 iload_1 iadd ireturn kotlinc Adder.kt javap -c Adder.class Classfile
  32. bipush 15 istore_0 bipush 10 istore_1 iload_0 iload_1 iadd ireturn

    Pre-first instruction Frame for add() method just pushed on stack Variables and operand stack empty Local Variables Operand Stack frame <add>
  33. bipush 15 istore_0 bipush 10 istore_1 iload_0 iload_1 iadd ireturn

    Store 15 on local variable Push 15 on operand stack via bipush Then pop it via istore_0, saving it in local variable, index 0 Local Variables Operand Stack frame <add> 15
  34. bipush 15 istore_0 bipush 10 istore_1 iload_0 iload_1 iadd ireturn

    Store 10 on local variable Push 10 on operand stack via bipush Then pop it via istore_1, saving it in local variable, index 1 Local Variables Operand Stack frame <add> 15 10
  35. bipush 15 istore_0 bipush 10 istore_1 iload_0 iload_1 iadd ireturn

    Load local variables back into operand stack Push variables at index 0 and 1 (respectively, 15 and 10) onto the operand stack Local Variables Operand Stack frame <add> 15 10 10 15
  36. bipush 15 istore_0 bipush 10 istore_1 iload_0 iload_1 iadd ireturn

    Perform addition The iadd instruction takes the top two operands from the stack, sums them, and pushes the result in the stack Local Variables Operand Stack frame <add> 15 10 10 15 25
  37. bipush 15 istore_0 bipush 10 istore_1 iload_0 iload_1 iadd ireturn

    Return from add() The ireturn instruction returns the value on top of the operand stack Local Variables Operand Stack frame <add> 15 10 10 15 25
  38. Calling Convention • Method parameters are copied in the callee

    function’s frame, inside the local variables - > Primitive types are just copied over The JVM always handles method calls as pass-by-value - > Reference types are copied as well, but for their nature, they provide a way to access and modify the original value Pass by value the reference’s memory address
  39. Comparison w/ C-like model • C on Linux compiles programs

    in ELF format (Executable and Linkable Format), used throughout the Kernel • ELF is a very flexible format; maps to the machine architecture
  40. Comparison w/ C-like model Heap Stack pc Runtime Constant Pool

    Method Area Heap Stack .rodata .text .bss 0x00008000 high addresses
  41. Assembly analysis: adder.c int main() { int a = 15;

    int b = 10; return a + b; } adder.c gcc -S -masm=intel -O0 .section __ TEXT, __ text .globl _main _main: ## @main push rbp mov rbp, rsp mov dword ptr [rbp - 4], 0 mov dword ptr [rbp - 8], 15 mov dword ptr [rbp - 12], 10 mov eax, dword ptr [rbp - 8] add eax, dword ptr [rbp - 12] pop rbp ret ELF executable
  42. Platform 1. Kotlin Compilation Process 2. Android APK contents 3.

    ART Runtime structure 4. On-device optimization 5. OAT files & PGO Kotlin Android ART ELF+dex
  43. Frontend Backend *.kt *.class kotlinc • Platform independent • Takes

    source code (.kt), tokenizes, builds the PSI tree, checks for errors • Produces a form of IR* to pass to the backend • Responsible for checking syntax correctness • Can target different platforms • Takes the frontend* IR, generates IR, then turns it into platform- specific (lowered) IR • Produces desired platform binaries * w/ K2
  44. Android APKs Android • Android applications packaged as .apk file

    • APK is just a ZIP archive (unzip a.apk to see contents) • Contains all resources needed to install an application • On installation, application is placed in /data and processed • On execution, application classes are loaded into memory and run
  45. - > AndroidManifest.xml Android • XML file providing a complete

    description and requirements for the Android application • Contains data needed by the OS at installation time (e.g. package name) • Declares permissions, SDK versions, icons, language, and internal application components (activities, services, broadcast receivers, content providers)
  46. - > /res / * Android • Processed resources (drawables,

    layouts, strings, icons) • Indexed in RESOURCES.ARSC
  47. - > /lib / * Android • Contains native libraries

    (compiled binaries / JNI) • Most common: • arm64-v8a (64-bit ARM aarch64) • armeabi-v7a (32-bit ARM) • x86 (32-bit Intel 80386) • x86_64 (64-bit x86-64) • MIPS
  48. - > classesN.dex Android • Contains application bytecode in Dalvik-Executable

    Format • Used by Android Runtime (ART) to execute application • Different than a *.class file Android doesn’t run the JVM • Why not *.class?
  49. Android Runtime (ART) • Optimized VM executing the classes.dex .dex

    files are generated from .class files via specific tools. They are the result of a transpilation & lowering process • Register-based machine (not stack-based like JVM) • File format similar to .class files • Completely different bytecode than JVM ART * • Designed for memory and battery constrained devices
  50. ART (Ex. Dalvik) • Register-based VM • Executes .dex files

    • Slow evolution, fragmented ecosystem JVM • Stack-based VM • “Frequent”-er releases • Executes .class files Dalvik bytecode (.dex) needs to be generated from .class files • Optimized for mobile devices • General purpuse computing
  51. Android Runtime (ART) • On KitKat and before, runtime was

    Dalvik VM A .dex file can hold at most 2^16 = 65,536 methods. Multidex applications have multiple dex files. • Up until Lollipop, ART was introduced • Latest ART versions have sophisticated structure and post-processing steps ART
  52. ART Internals • So, Android executes .dex files via ART

    • Not exactly • Android uses a mix of Just-In-Time and Ahead-Of-Time compilation to optimize applications ELF+dex • Intuition: running a native binary is much faster than running a virtual machine (ART/Dalvik) executing bytecode
  53. Compilation Types ELF+dex Techniques used to convert .dex classes into

    a lower level Ahead Of Time Just in Time Before application execution, classes.dex is compiled into native code While the app is executing, the runtime profiles frquently used methods, [5]
  54. Re-compilation is tricky ELF+dex •Intuitively, compiling an application from dex

    to native code should make it run faster •In practice it’s tricky to choose how to implement this type of optimization - > When do I re-compile the app? Install-time or run-time? - > Do I recompile all the app? Or just some parts? - > Impact on battery, storage, CPU usage, device life TLDR: Difficult implementation; not strictly relevant for developers
  55. dex2oat • Every Android release since Lollipop has /system/bin/dex2oat •

    Takes in .dex, produces .oat • OAT files are ELF binaries! • OAT format changes every android release, triggering recompilation of all installed apps ELF+dex “Optimizing app $X” means “executing dex2oat on app $X” [6]
  56. OAT files • Implemented in ELF container (used throughout android:

    .so files are ELF), enables using /system/bin/linker64 • OAT files contain a mix of compiled native code (for the device architecture) and some dex Don’t trust extensions, .dex can be DEX/OAT, .oat is OAT, .odex N/A OAT [7]
  57. OAT files > PGO • Compilation can be aided by

    profiles, instructing dex2oat on which code paths to optimize • Distributed by google play with app install/updates App-version specific [8, 9]
  58. *.kt *.java kotlinc javac *.class Compilation Compile the source files

    with the java and kotlin compilers, generating standard *.class files
  59. *.dex d8/r8 kotlinc javac *.class D8 & R8 The D8

    and R8 tools convert class files into optimized dex files. * •D8 is the dexer+desugarer: it takes in JVM classes and converts them into dex file(s), while lowering the target JVM version for the dex output •R8 is an optimizer Allows writing code using higher language features, while retaining compatibility with the dex format [10]
  60. *.dex d8/r8 Joining The *.dex classes are joined with resources

    and native libraries into an apk file Resources Native Libs aapt + .so .apk
  61. .apk Packaging The files are finalized, signed and zip-aligned into

    the final APK signer zipalign .apk keystore COMPILE TIME INSTALL TIME
  62. *.dex d8/r8 *.kt *.java kotlinc javac *.class Resources Native Libs

    aapt + .so .apk signer zipalign .apk keystore
  63. *.so ELF .dex .oat dex2oat INSTALL TIME EXECUTION TIME ART

    Native /apex/com.android.art/bin/dalvikvm /system/bin/linker profile
  64. Language 1. @Metadata 2. Nullability 3. When Expressions w/ Guards

    4. Properties 5. Extensions 6. Conventions 7. Destricturing Declarations 8. Data Classes 9. Default Arguments when guards Nullability data classes conventions default arguments destructuring
  65. Preface • Language Features can be implemented by: -> Compiler

    (code generation / rearranging) -> Type system (compile-time errors) • Language design constrained by: - > JVM rules (or platform) -> Backward compatibility • Useful thinking: imagine how you would implement something, given the JVM constraints
  66. Preface • We’ll assume Kotlin 2.0 • Targeting JVM •

    Non-optimized bytecode • Cleaned-up decompilation
  67. class Counter { private var count: Int = 0 fun

    increment() { count += 1 } }
  68. @Metadata( mv = {2, 0, 0}, k = 1, xi

    = 48, d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u000 d2 = {"LK00_meta/Counter;", "", "<init>", "()V", "count", "", "inc ) public final class CounterKt { private int count; public final void increment() { ++ this.count; } }
  69. @Metadata • Kotlin needs to persist additional information to the

    .class Recall: JVM class files can’t store any Kotlin-specific information • It adds a @Metadata annotation to each class, containing those information -> Nullability, Declaration-site variance, Reflection, [ . .. ] • Also, kotlin class names are suffixed with “Kt”, so that it can avoid naming conflicts with Java classes
  70. Nullability • Type system handles nullability explicitly; safe by design

    Recall: JVM allows any reference type to be nullable Idea: It’s not semantically OK for reference types to be null in some points • Code correctness checked at compile time • Partitions types in two universes: {T} ∩ {T?} = ∅ Converts potential runtime errors (due to the JVM’s unreliability w.r.t. null references) to compile time errors
  71. Nullability • Creates null-safe environment within Kotlin code • Provides

    way to operate with null values gracefully -> Safe-Call Operator ? . -> Elvis Operator ? : -> Double-bang Operator !! -> Ternary Operator ? • They all have a clean compilation representations
  72. /* Kotlin */ val maybe: A? = [ ... ]

    print(maybe ?. accessor()) // Java (decompiled) String maybe = [ ... ] String var2 = maybe != null ? maybe.accessor() : null; System.out.print(var2); Safe Call Operator
  73. /* Kotlin */ val maybe: A? = [ ... ]

    print(maybe ?: ”Default”) // Java (decompiled) String maybe = [ ... ] String var2 = maybe != null ? maybe : ”Default”; System.out.print(var2); Elvis Operator
  74. /* Kotlin */ val maybe: A? = [ ... ]

    print(maybe !! ) // Java (decompiled) String maybe = [ ... ] Intrinsics.checkNotNull(maybe); System.out.print(maybe); Double-Bang Operator
  75. Nullability > Intrinsics • Part of kotlin.jvm.internal • Injected by

    the compiler to enforce null-safety • Contains checker methods and assertions -> checkNotNullExpressionValue() -> checkNotNull() -> checkReturnedValueIsNotNull() -> throwNpe() -> checkFieldIsNotNull() -> checkParameterIsNotNull()
  76. Nullability > Platform types • What about Java interoperability? •

    Kotlin supports flexible types: -> Types coming from other languages are treated as Platfrom Types -> Keyword dynamic (for JS) disables type checking altogether • Flexible types are defined as having a varying (gradual) type, among a subtype and supertype • Practically, non-annotated Java types are assumed to have no nullability information (T! is among T and T?)
  77. // Java public class TypicalJavaClass { String bad() { return

    null; } String good() { return "null"; } }
  78. /* Kotlin */ fun nn() { val x: String =

    TypicalJavaClass().good() val y: String = TypicalJavaClass().bad() } fun nullable() { val x: String? = TypicalJavaClass().good() val y: String? = TypicalJavaClass().bad() } We’re telling the compiler “I know this is not null” We’re telling the compiler “I know this may be null”
  79. // Java (decompiled) public static final void nn() { String

    x = (new TypicalJavaClass()).good(); Intrinsics.checkNotNullExpressionValue(x, “good( .. . )"); String y = (new TypicalJavaClass()).bad(); Intrinsics.checkNotNullExpressionValue(y, "bad( . .. )"); } public static final void nullable() { String x = (new TypicalJavaClass()).good(); String y = (new TypicalJavaClass()).bad(); }
  80. Nullability > Platform types • Why don’t we treat them

    as nullable by default? Again, since JVM allows reference types to be null: -> Among Kotlin classes, type system enforces null safety -> In Kotlin+Java, compiler inserts intrinsic checks matching your type declarations (to ensure runtime safety) -> Painful, verbose and most of the times redundant Kotlin relinquishes some type-safety (by requiring the developer to handle values coming from non-kotlin with care) to achieve better interoperability
  81. When statements w/ Enums • When expressions are mapped to

    switch statements • Kotlin requires when to be exhaustive -> Nice to use with enums, for representing closed domains
  82. /* Kotlin */ enum class PathDir(val bearing: Int) { NORTH(0),

    SOUTH(180), EAST(90), WEST(270) } fun printDirection(direction: PathDir) { when (direction) { PathDir.NORTH -> print("You are heading North.") PathDir.SOUTH -> print("You are heading South.") PathDir.EAST -> print("You are heading East.") PathDir.WEST -> print("You are heading West.") } }
  83. /* Java (disassembled, PathDir) */ public enum PathDir { private

    final int bearing; NORTH(0), SOUTH(180), EAST(90), WEST(270); // $FF: synthetic field private static final EnumEntries $ENTRIES = EnumEntriesKt.enumEntries($VALUES); private PathDir(int bearing) { this.bearing = bearing; } public final int getBearing() { return this.bearing; }
  84. private PathDir(int bearing) { this.bearing = bearing; } public final

    int getBearing() { return this.bearing; } @NotNull public static EnumEntries getEntries() { return $ENTRIES; } // $FF: synthetic method private static final PathDir[] $values() { PathDir[] var0 = new PathDir[]{NORTH, SOUTH, EAST, WEST}; return var0; } }
  85. /* Java (disassembled, WhenMappings) */ // $FF: synthetic class public

    class WhenMappings { // $FF: synthetic field public static final int[] $EnumSwitchMapping$0; static { int[] var0 = new int[PathDir.values().length]; try { var0[PathDir.NORTH.ordinal()] = 1; } catch (NoSuchFieldError var5) {} try { var0[PathDir.SOUTH.ordinal()] = 2; } catch (NoSuchFieldError var4) {} try { Maps enum ordinals to their number
  86. try { var0[PathDir.NORTH.ordinal()] = 1; } catch (NoSuchFieldError var5) {}

    try { var0[PathDir.SOUTH.ordinal()] = 2; } catch (NoSuchFieldError var4) {} try { var0[PathDir.EAST.ordinal()] = 3; } catch (NoSuchFieldError var3) {} try { var0[PathDir.WEST.ordinal()] = 4; } catch (NoSuchFieldError var2) {} $EnumSwitchMapping$0 = var0; } }
  87. /* Java (disassembled, when) */ public static final void printDirection(@NotNull

    PathDir direction) { Intrinsics.checkNotNullParameter(direction, "direction"); int var2 = WhenMappings.$EnumSwitchMapping$0[direction.ordinal()]; switch (var2) { case 1: String var6 = "You are heading North."; System.out.print(var6); break; case 2: String var5 = "You are heading South."; System.out.print(var5); break; case 3: String var4 = "You are heading East."; System.out.print(var4); break; case 4: String var3 = "You are heading West.";
  88. switch (var2) { case 1: String var6 = "You are

    heading North."; System.out.print(var6); break; case 2: String var5 = "You are heading South."; System.out.print(var5); break; case 3: String var4 = "You are heading East."; System.out.print(var4); break; case 4: String var3 = "You are heading West."; System.out.print(var3); break; default: throw new NoWhenBranchMatchedException(); }
  89. When guards • Mixes when conditions with additional check Introduced

    in Kotlin 2.1 (2 days ago) • Must maintain exhaustiveness
  90. / * Kotlin */ enum class PathDir(val bearing: Int) {

    NORTH(0), SOUTH(180), EAST(90), WEST(270) } fun printDirection(direction: PathDir) { when (direction) { PathDir.NORTH if direction.bearing > 10 - > print("North") PathDir.SOUTH - > print("South") PathDir.EAST -> print("East") PathDir.WEST -> print("West") else - > print("Else") } }
  91. / * Java (disassembled, when) */ public static final void

    printDirection(@NotNull PathDir direction) { Intrinsics.checkNotNullParameter(direction, "direction"); int var1 = WhenMappings.$EnumSwitchMapping$0[direction.ordinal()]; if (var1 = = 1 & & direction.getBearing() > 10) { System.out.print("North"); } else if (var1 == 2) { System.out.print("South"); } else if (var1 == 3) { System.out.print("East"); } else if (var1 == 4) { System.out.print("West"); } else { System.out.print("Else"); } }
  92. Properties • JVM classes support fields for storing information ->

    Need to write accessors (getX() / setX()) • Kotlin introduces properties, as first-class language feature -> Declared as normal variables -> Compiler can generate getter and/or setter Produces cleaner high-level code by grouping together underlying JVM statements -> May omit backing field
  93. / * Kotlin */ class ClassWithProps { var mutableProperty: Int

    = 10 val immutableProperty: Int = 20 }
  94. // Java (disassembled) public final class ClassWithProps { private int

    mutableProperty = 10; private final int immutableProperty = 20; public final int getMutableProperty() { return this.mutableProperty; } public final void setMutableProperty(int var1) { this.mutableProperty = var1; } public final int getImmutableProperty() { return this.immutableProperty; } }
  95. / * Kotlin */ class Rectangle(val l1: Int, val l2:

    Int) { var isSquareWrong: Boolean = l1 == l2 val isSquare: Boolean get() = l1 == l2 }
  96. // Java (decompiled) public final class Rectangle { private final

    int l1; private final int l2; private boolean isSquareWrong; public Rectangle(int l1, int l2) { this.l1 = l1; this.l2 = l2; this.isSquareWrong = this.l1 == this.l2; } public final int getL1() { return this.l1; } public final int getL2() { return this.l2; }
  97. public final int getL1() { return this.l1; } public final

    int getL2() { return this.l2; } public final boolean isSquareWrong() { return this.isSquareWrong; } public final void setSquareWrong(boolean var1) { this.isSquareWrong = var1; } public final boolean isSquare() { return this.l1 == this.l2; } }
  98. Properties / Backing Field • Properties may or may not

    have a field behind it -> e.g. if getter and setter depend on other variables “The way you access a property doesn’t depend on whether it has a backing field: the compiler will generate a backing field if you use it explicitly, or use the default accessor implementation” Kotlin in Action, 2nd Edition • If everything can be derived from get()/set(), no field is needed (and also no initialization)
  99. Extension Functions • Simple mechanism: creates a static method accepting

    the receiver object as the first type • Allows to integrate classes you didn’t write: it’s enough to have access to the classfile • Can’t access private fields/methods of the object
  100. // Java (decompiled) @NotNull public static final String lastChar(@NotNull String

    $this$lastChar) { Intrinsics.checkNotNullParameter($this$lastChar, "<this>"); return String.valueOf($this$lastChar.charAt($this$lastChar.length() - 1)); }
  101. Conventions • Java integrates classes implementing certain interfaces with language

    features -> Iterable in for loops, AutoCloseable in try-with-resources • Kotlin formalizes this notion into conventions • Achieved by declaring functions with specific names -> Brings operator overloading cleanly into the syntax Conventions enable language features through certain functions, instead of relying on magical ad-hoc statements
  102. Conventions w/ Extension Functons By implementing conventions via function declaration,

    we can enrich existing classes with extension functions! • Self-contained: Kotlin extension functions work on top of already existing platform code • Most of the kotlin collections are implemented this way • Java can’t enrich already existing code, since its syntax requires interface implementation -> e.g. T implementing java.lang.Iterable -> e.g. defining T.iterator(), for any T
  103. Conventions for ((k,v) in map) { [ ... ] }

    val a = 0xCA and 0xFE val b = 0xBA xor 0xBE list += Element() V[x,y,z]
  104. Destructuring Declarations • Another convention! • Implemented by defining T.componentN()

    functions • Allows for extracting multiple data fields all at once -> (F1, …,FN ) = T • Destructuring declarations are positional • Can be ignored by supplying _ as the variable name
  105. / * Kotlin */ class Point(val x: Int, val y:

    Int) { operator fun component1(): Int = x operator fun component2(): Int = y }
  106. // Java (decompiled) public final class Point { private final

    int x; private final int y; public Point(int x, int y) { this.x = x; this.y = y; } public final int getX() { return this.x; } public final int getY() { return this.y; }
  107. } public final int getX() { return this.x; } public

    final int getY() { return this.y; } public final int component1() { return this.x; } public final int component2() { return this.y; } }
  108. // Java (decompiled) Point p = new Point(10, 20); int

    px = p.component1(); int py = p.component2(); /* Kotlin */ val p = Point(10, 20) val (px, py) = p
  109. // Java (decompiled) Point p = new Point(10, 20); int

    px = p.component1(); int py = p.component2(): /* Kotlin */ val p = Point(10, 20) val (px, _) = p
  110. Data Classes • Designed to hold data • Contain autogenerated

    boilerplate methods -> e.g. equals(), hashCode(), toString() componentN(), copy()
  111. / * Kotlin */ data class Beer( var volumeMm: Int,

    val priceEur: Float ) { val approxCost get() = volumeMm * priceEur fun priceLog() = ln(priceEur.toDouble()) }
  112. // Java (decompiled) public final class Beer { private int

    volumeMm; private final float priceEur; public Beer(int volumeMm, float priceEur) { this.volumeMm = volumeMm; this.priceEur = priceEur; } public final int getVolumeMm() { return this.volumeMm; } public final void setVolumeMm(int var1) { this.volumeMm = var1; }
  113. return this.volumeMm; } public final void setVolumeMm(int var1) { this.volumeMm

    = var1; } public final float getPriceEur() { return this.priceEur; } public final float getApproxCost() { return (float)this.volumeMm * this.priceEur; } public final double priceLog() { return Math.log((double)this.priceEur); }
  114. return this.priceEur; } public final float getApproxCost() { return (float)this.volumeMm

    * this.priceEur; } public final double priceLog() { return Math.log((double)this.priceEur); } public final int component1() { return this.volumeMm; } public final float component2() { return this.priceEur; }
  115. @NotNull public final Beer copy(int volumeMm, float priceEur) { return

    new Beer(volumeMm, priceEur); } // $FF: synthetic method public static Beer copy$default(Beer var0, int var1, float var2, int var3, Object var4) { if ((var3 & 1) != 0) { var1 = var0.volumeMm; } if ((var3 & 2) != 0) { var2 = var0.priceEur; } return var0.copy(var1, var2); }
  116. @NotNull public String toString() { return "Beer(volumeMm=" + this.volumeMm +

    ", priceEur=" + this.priceEur + ')'; } public int hashCode() { int result = Integer.hashCode(this.volumeMm); result = result * 31 + Float.hashCode(this.priceEur); return result; } public boolean equals(@Nullable Object other) { if (this == other) { return true; } else if (!(other instanceof Beer)) { return false; } else {
  117. public boolean equals(@Nullable Object other) { if (this == other)

    { return true; } else if (!(other instanceof Beer)) { return false; } else { Beer var2 = (Beer)other; if (this.volumeMm ! = var2.volumeMm) { return false; } else { return Float.compare(this.priceEur, var2.priceEur) = = 0; } } } }
  118. Equality Checks • Kotlin: -> Equality operator always converted in

    .equals() -> If implemented, provides value-based comparison • Java: -> “ == “ on primitive types compares value -> “ == “ on reference types compares reference value (same object) -> Informal “convention” of using .equals() for value comparison == in kotlin ! = = = in java
  119. Default Arguments • The JVM doesn’t support default arguments for

    methods • Kotlin implements default parameters on top of that DefaultArgs.kt fun padding(top: Int = 10, bottom: Int = 10, left: Int = 24, right: Int = 24) { println("$top $bottom $left $right") } • Design challenge: think about how you would implement this
  120. Default Arguments • Key points: -> Should replace missing values

    with their default -> Should preserve introperability with java • Challenges: -> Any value combination can be missing -> Can’t generate as many functions as possible inputs
  121. /* Kotlin */ fun padding(top: Int = 10, bottom: Int

    = 10, left: Int = 24, right: Int = 24) { println("$top $bottom $left $right") }
  122. // Java (decompiled) public final class DefaultKt { public static

    final void padding(int top, int bottom, int left, int right) { String var4 = "" + top + ' ' + bottom + ' ' + left + ' ' + right; System.out.println(var4); } // /* Kotlin */ fun padding(top: Int = 10, bottom: Int = 10, left: Int = 24, right: Int = 24) { println("$top $bottom $left $right") }
  123. // $FF: synthetic method public static void padding$default(int var0, int

    var1, int var2, int var3, int var4, Object var5) { if ((var4 & 1) != 0) { var0 = 10; } if ((var4 & 2) != 0) { var1 = 10; } if ((var4 & 4) != 0) { var2 = 24; } if ((var4 & 8) != 0) { var3 = 24; } padding(var0, var1, var2, var3); }
  124. Default Arguments • Two methods are generated: -> Normal method

    and body (same as declared, no defaults) -> Helper synthetic method (method name appended with “$default”) • Any call f with any default argument will result in the compiler emitting a method call to the method’s f$default helper Basically, a call without all arguments isn’t considered complete and will need to go through the default-enhancing method Java code will be able to call the standard method, no defaults
  125. Default Arguments • Two+ parameters are added to the default-enhanching

    method: -> Bitmask, flagging which parameters haven’t been passed -> DefaultConstructorMarker for signature collision • If parameter 0 is absent, bitmask += 20=1 • If parameter 1 is absent, bitmask += 21=2 • If parameter 2 is absent, bitmask += 22=4 var0 absent var1 present 0 0 0 0 1 1 0 1
  126. fun fun_max_arg_nodfu( arg000: Int=0, arg001: Int=0, arg002: Int=0, arg003: Int=0,

    arg004: Int=0, arg005: Int=0, arg006: Int=0, arg007: I arg010: Int=0, arg011: Int=0, arg012: Int=0, arg013: Int=0, arg014: Int=0, arg015: Int=0, arg016: Int=0, arg017: I arg020: Int=0, arg021: Int=0, arg022: Int=0, arg023: Int=0, arg024: Int=0, arg025: Int=0, arg026: Int=0, arg027: I arg030: Int=2, arg031: Int=4, arg032: Int=8, arg033: Int=0, arg034: Int=0, arg035: Int=0, arg036: Int=0, arg037: I arg040: Int=0, arg041: Int=0, arg042: Int=0, arg043: Int=0, arg044: Int=0, arg045: Int=0, arg046: Int=0, arg047: I arg050: Int=0, arg051: Int=0, arg052: Int=0, arg053: Int=0, arg054: Int=0, arg055: Int=0, arg056: Int=0, arg057: I arg060: Int=0, arg061: Int=0, arg062: Int=0, arg063: Int=0, arg064: Int=0, arg065: Int=0, arg066: Int=0, arg067: I arg070: Int=0, arg071: Int=0, arg072: Int=0, arg073: Int=0, arg074: Int=0, arg075: Int=0, arg076: Int=0, arg077: I arg080: Int=0, arg081: Int=0, arg082: Int=0, arg083: Int=0, arg084: Int=0, arg085: Int=0, arg086: Int=0, arg087: I arg090: Int=0, arg091: Int=0, arg092: Int=0, arg093: Int=0, arg094: Int=0, arg095: Int=0, arg096: Int=0, arg097: I arg100: Int=0, arg101: Int=0, arg102: Int=0, arg103: Int=0, arg104: Int=0, arg105: Int=0, arg106: Int=0, arg107: I arg110: Int=0, arg111: Int=0, arg112: Int=0, arg113: Int=0, arg114: Int=0, arg115: Int=0, arg116: Int=0, arg117: I arg120: Int=0, arg121: Int=0, arg122: Int=0, arg123: Int=0, arg124: Int=0, arg125: Int=0, arg126: Int=0, arg127: I arg130: Int=0, arg131: Int=0, arg132: Int=0, arg133: Int=0, arg134: Int=0, arg135: Int=0, arg136: Int=0, arg137: I arg140: Int=0, arg141: Int=0, arg142: Int=0, arg143: Int=0, arg144: Int=0, arg145: Int=0, arg146: Int=0, arg147: I arg150: Int=0, arg151: Int=0, arg152: Int=0, arg153: Int=0, arg154: Int=0, arg155: Int=0, arg156: Int=0, arg157: I arg160: Int=0, arg161: Int=0, arg162: Int=0, arg163: Int=0, arg164: Int=0, arg165: Int=0, arg166: Int=0, arg167: I arg170: Int=0, arg171: Int=0, arg172: Int=0, arg173: Int=0, arg174: Int=0, arg175: Int=0, arg176: Int=0, arg177: I arg180: Int=0, arg181: Int=0, arg182: Int=0, arg183: Int=0, arg184: Int=0, arg185: Int=0, arg186: Int=0, arg187: I arg190: Int=0, arg191: Int=0, arg192: Int=0, arg193: Int=0, arg194: Int=0, arg195: Int=0, arg196: Int=0, arg197: I arg210: Int=0, arg211: Int=0, arg212: Int=0, arg213: Int=0, arg214: Int=0, arg215: Int=0, arg216: Int=0, arg217: I arg220: Int=0, arg221: Int=0, arg222: Int=0, arg223: Int=0, arg224: Int=0, arg225: Int=0, arg226: Int=0, arg227: I arg230: Int=0, arg231: Int=0, arg232: Int=0, arg233: Int=0, arg234: Int=0, arg235: Int=0, arg236: Int=0, arg237: I arg240: Int=0, arg241: Int=0, arg242: Int=0, arg243: Int=0, arg244: Int=0, arg245: Int=0, arg246: Int=0, arg247: I arg250: Int=0, arg251: Int=0, arg252: Int=0, arg253: Int=0, arg254: Int=0, arg255: Int=0, arg256: Int=0, arg257: I arg260: Int=0, arg261: Int=0, arg262: Int=0, arg263: Int=0, arg264: Int=0, arg265: Int=0, arg266: Int=0, arg267: I arg270: Int=0, arg271: Int=0, arg272: Int=0, arg273: Int=0, arg274: Int=0, arg275: Int=0, arg276: Int=0, arg277: I arg280: Int=0, arg281: Int=0, arg282: Int=0, arg283: Int=0, arg284: Int=0, arg285: Int=0, arg286: Int=0, arg287: I
  127. @Jvm* • Annotations for controlling bytecode compilation -> @JvmStatic: converts

    function to static java method -> @JvmInline: marks an inline class -> @JvmRecord: makes a data class a record -> @JvmName(“f”): sets the name of the method/field
  128. Thank you! • Slides on my Twitter / Bsky •

    For playing around with disassembly/DEX/OAT, check out kotlin- explorer