JVM memberikan portabilitas dan throughput tinggi, tetapi membawa biaya startup 200-2000 ms dan footprint memori 200 MB. Untuk CLI tool, serverless function, dan aplikasi embedded, biaya ini tidak dapat diterima. Kotlin/Native mengatasi masalah tersebut dengan mengompilasi kode Kotlin langsung ke binary native via LLVM — startup 5-50 ms, memori 10-50 MB, dan tidak memerlukan JVM di mesin target. Alternatif lain adalah GraalVM native-image yang mengompilasi bytecode Java/Kotlin menjadi binary via analisis closed-world. Pada chapter ini kita akan membedah target Kotlin/Native, membandingkan dengan GraalVM, serta mendiskusikan trade-off native vs JVM untuk membantu pemilihan strategi distribusi.

Target Kotlin/Native #

Kotlin/Native mendukung banyak target platform. Setiap target menghasilkan binary dalam format OS-spesifik: ELF untuk Linux, Mach-O untuk macOS, PE untuk Windows.

kotlin
// native_binaries_kotlin_native.kt — Demo konsep Kotlin/Native dan komparasi

fun main() {
    println("=== Native Binaries dengan Kotlin/Native ===")
    println()
    println("Kompilasi Kotlin ke binary native; tanpa JVM di target.")
    println()

    // === 1. Target Kotlin/Native ===
    println("=== 1. Target Kotlin/Native ===")
    val targets = listOf(
        Triple("linuxX64", "Linux x86_64", "ELF binary 64-bit"),
        Triple("linuxArm64", "Linux ARM 64-bit", "Raspberry Pi 4, server ARM"),
        Triple("macosX64", "macOS Intel", "Mach-O 64-bit (legacy)"),
        Triple("macosArm64", "macOS Apple Silicon", "Mach-O ARM64 (M1/M2)"),
        Triple("mingwX64", "Windows x86_64", "PE binary 64-bit (.exe)"),
        Triple("iosArm64", "iOS device", "ARM64 binary"),
        Triple("iosSimulatorArm64", "iOS simulator M-series", "ARM64 simulator"),
        Triple("tvosArm64", "tvOS device", "Set-top box Apple")
    )
    targets.forEach { (target, platform, binary) ->
        println("  " + target.padEnd(22) + " | " + platform.padEnd(28) + " | " + binary)
    }
    println()

    // === 2. Kotlin/Native vs GraalVM native-image ===
    println("=== 2. Kotlin/Native vs GraalVM native-image ===")
    val comparison = listOf(
        Pair("Bahasa input", Pair("Kotlin/Native: Kotlin langsung", "GraalVM: bytecode Java/Kotlin")),
        Pair("Compiler", Pair("LLVM backend", "Substrate VM (closed-world analysis")),
        Pair("Runtime", Pair("Sendiri (konkuren, GC)", "Java API subset (reflection terbatas)")),
        Pair("Binary size", Pair("~5-10 MB", "~50-100 MB (inklusif JDK)")),
        Pair("Startup time", Pair("~5-50 ms", "~10-100 ms")),
        Pair("Reflection", Pair("Terbatas; @ExportReflection", "Perlu konfigurasi reachability")),
        Pair("Cohabit JVM", Pair("Tidak (binary mandiri)", "Bisa (dari JAR)")),
        Pair("Best for", Pair("iOS, embedded, CLI", "Serverless, microservice, CLI Java"))
    )
    comparison.forEach { (aspek, pair) ->
        val (native, graal) = pair
        println("  " + aspek.padEnd(18) + " | " + native.padEnd(38) + " | " + graal)
    }
    println()

    // === 3. Konfigurasi Gradle Kotlin/Native ===
    println("=== 3. Konfigurasi Gradle Kotlin/Native ===")
    val gradle = listOf(
        "plugins { kotlin(\"multiplatform\") }",
        "",
        "kotlin {",
        "  linuxX64(\"native\") {",
        "    binaries {",
        "      executable {",
        "        entryPoint = \"com.example.main\"",
        "      }",
        "    }",
        "  }",
        "}"
    )
    gradle.forEach { println("  " + it) }
    println()

    // === 4. Perintah build ===
    println("=== 4. Perintah Build ===")
    val cmds = listOf(
        Pair("./gradlew linkReleaseExecutableNative", "Build optimized binary di build/bin/native/releaseExecutable/"),
        Pair("./gradlew linkDebugExecutableNative", "Build debug binary (lebih cepat compile)"),
        Pair("./gradlew assemble", "Build semua target termasuk native"),
        Pair("native-image -jar app.jar app", "GraalVM native-image dari JAR"),
        Pair("native-image --no-fallback -jar app.jar app", "GraalVM tanpa fallback ke JVM")
    )
    cmds.forEach { (cmd, desc) ->
        println("  " + cmd.padEnd(45) + " -> " + desc)
    }
    println()

    // === 5. Trade-off native vs JVM ===
    println("=== 5. Trade-off Native vs JVM ===")
    val trades = listOf(
        Triple("Startup time", "Native: 5-50 ms", "JVM: 200-2000 ms (warm-up)"),
        Triple("Peak throughput", "Native: lebih rendah (no JIT)", "JVM: lebih tinggi setelah JIT)"),
        Triple("Memory usage", "Native: 10-50 MB", "JVM: 200-500 MB (heap default)"),
        Triple("Binary size", "Native: 5-10 MB", "JVM: 0 (JAR) + JVM 200 MB"),
        Triple("Reflection", "Native: terbatas", "JVM: penuh"),
        Triple("Hot reload", "Native: rebuild binary", "JVM: hot swap / agent")
    )
    trades.forEach { (aspek, nativeRow, jvmRow) ->
        println("  " + aspek.padEnd(20) + " | " + nativeRow.padEnd(35) + " | " + jvmRow)
    }
    println()

    // === 6. Use cases ===
    println("=== 6. Use Cases ===")
    val useCases = listOf(
        "Kotlin/Native: iOS app, embedded, CLI tool tanpa JVM",
        "GraalVM: AWS Lambda, microservice, CLI Java",
        "JVM fat JAR: server app dengan puncak throughput",
        "jlink: custom runtime image untuk Docker minimal"
    )
    useCases.forEach { println("  " + it) }
    println()

    // === 7. Aturan praktis ===
    println("=== 7. Aturan Praktis ===")
    val rules = listOf(
        "1. CLI tool ringan -> Kotlin/Native atau GraalVM native-image",
        "2. Server throughput -> JVM dengan GC tuning",
        "3. iOS / Apple platform -> Kotlin/Native (wajib)",
        "4. Embedded / IoT -> Kotlin/Native (binary kecil)",
        "5. Serverless cold start -> GraalVM native-image",
        "6. Reflection ekstensif -> tetap di JVM"
    )
    rules.forEach { println("  " + it) }
}

Output yang Dihasilkan:

text
=== Native Binaries dengan Kotlin/Native ===

Kompilasi Kotlin ke binary native; tanpa JVM di target.

=== 1. Target Kotlin/Native ===
  linuxX64               | Linux x86_64                 | ELF binary 64-bit
  linuxArm64             | Linux ARM 64-bit             | Raspberry Pi 4, server ARM
  macosX64               | macOS Intel                  | Mach-O 64-bit (legacy)
  macosArm64             | macOS Apple Silicon          | Mach-O ARM64 (M1/M2)
  mingwX64               | Windows x86_64               | PE binary 64-bit (.exe)
  iosArm64               | iOS device                   | ARM64 binary
  iosSimulatorArm64      | iOS simulator M-series       | ARM64 simulator
  tvosArm64              | tvOS device                  | Set-top box Apple

=== 2. Kotlin/Native vs GraalVM native-image ===
  Bahasa input       | Kotlin/Native: Kotlin langsung         | GraalVM: bytecode Java/Kotlin
  Compiler           | LLVM backend                           | Substrate VM (closed-world analysis
  Runtime            | Sendiri (konkuren, GC)                 | Java API subset (reflection terbatas)
  Binary size        | ~5-10 MB                               | ~50-100 MB (inklusif JDK)
  Startup time       | ~5-50 ms                               | ~10-100 ms
  Reflection         | Terbatas; @ExportReflection            | Perlu konfigurasi reachability
  Cohabit JVM        | Tidak (binary mandiri)                 | Bisa (dari JAR)
  Best for           | iOS, embedded, CLI                     | Serverless, microservice, CLI Java

=== 3. Konfigurasi Gradle Kotlin/Native ===
  plugins { kotlin("multiplatform") }
  
  kotlin {
    linuxX64("native") {
      binaries {
        executable {
          entryPoint = "com.example.main"
        }
      }
    }
  }

=== 4. Perintah Build ===
  ./gradlew linkReleaseExecutableNative         -> Build optimized binary di build/bin/native/releaseExecutable/
  ./gradlew linkDebugExecutableNative           -> Build debug binary (lebih cepat compile)
  ./gradlew assemble                            -> Build semua target termasuk native
  native-image -jar app.jar app                 -> GraalVM native-image dari JAR
  native-image --no-fallback -jar app.jar app   -> GraalVM tanpa fallback ke JVM

=== 5. Trade-off Native vs JVM ===
  Startup time         | Native: 5-50 ms                     | JVM: 200-2000 ms (warm-up)
  Peak throughput      | Native: lebih rendah (no JIT)       | JVM: lebih tinggi setelah JIT)
  Memory usage         | Native: 10-50 MB                    | JVM: 200-500 MB (heap default)
  Binary size          | Native: 5-10 MB                     | JVM: 0 (JAR) + JVM 200 MB
  Reflection           | Native: terbatas                    | JVM: penuh
  Hot reload           | Native: rebuild binary              | JVM: hot swap / agent

=== 6. Use Cases ===
  Kotlin/Native: iOS app, embedded, CLI tool tanpa JVM
  GraalVM: AWS Lambda, microservice, CLI Java
  JVM fat JAR: server app dengan puncak throughput
  jlink: custom runtime image untuk Docker minimal

=== 7. Aturan Praktis ===
  1. CLI tool ringan -> Kotlin/Native atau GraalVM native-image
  2. Server throughput -> JVM dengan GC tuning
  3. iOS / Apple platform -> Kotlin/Native (wajib)
  4. Embedded / IoT -> Kotlin/Native (binary kecil)
  5. Serverless cold start -> GraalVM native-image
  6. Reflection ekstensif -> tetap di JVM

Mari kita bedah ringkasan di atas:

  1. Target: Kotlin/Native mendukung 8+ target. Untuk iOS, target iosArm64 (device) dan iosSimulatorArm64 (simulator M-series) adalah yang paling umum dipakai di KMP.
  2. Perbandingan: Kotlin/Native mengompilasi langsung dari source Kotlin via LLVM; GraalVM mengompilasi bytecode yang sudah ada. Kotlin/Native menghasilkan binary lebih kecil (5-10 MB) karena tidak menyertakan JDK; GraalVM menghasilkan 50-100 MB karena mengemas sebagian JDK.
  3. Konfigurasi Gradle: plugin kotlin("multiplatform") mengekspos DSL kotlin { linuxX64("native") { binaries { executable { entryPoint = "..." } } } }. entryPoint menentukan function main() mana yang menjadi titik masuk.
  4. Perintah Build: linkReleaseExecutableNative menghasilkan binary optimized (mirip -O2 di C); linkDebugExecutableNative lebih cepat compile tetapi binary lebih besar dan lebih lambat.
  5. Trade-off: Native menang di startup dan memori; JVM menang di peak throughput karena JIT mengoptimalkan hot path berdasarkan profil runtime. Native tidak punya JIT — performa bytecode identik dari awal hingga akhir.
  6. Use Cases: Kotlin/Native wajib untuk iOS (Apple tidak mengizinkan JVM). GraalVM populer untuk serverless karena cold start yang cepat.

Konfigurasi Gradle Kotlin/Native #

Untuk proyek Kotlin Multiplatform yang ingin menghasilkan binary native, gunakan plugin kotlin("multiplatform") dan deklarasikan target dengan binaries.executable():

kotlin build.gradle.kts
plugins {
    kotlin("multiplatform") version "2.0.21"
}

kotlin {
    linuxX64("native") {
        binaries {
            executable {
                entryPoint = "com.example.main"
                // Optimasi binary
                optimization = true
            }
        }
    }
}

Build menghasilkan build/bin/native/releaseExecutable/native.kexe (Linux/macOS) atau native.exe (Windows).

GraalVM native-image #

Alternatif untuk distribusi tanpa JVM adalah GraalVM native-image. Tool ini mengompilasi bytecode Java/Kotlin menjadi binary native via closed-world analysis — semua path yang mungkin dieksekusi dianalisis statis. Karena analisis ini, reflection dinamis (mis. Class.forName(namaDariFile)) tidak otomatis terdeteksi dan butuh konfigurasi reachability manual via reflect-config.json.

bash GraalVM native-image
native-image -jar app.jar app
./app  # binary native, startup ~10 ms

Trade-off utama: binary GraalVM besar (50-100 MB) karena menyertakan subset JDK. Untuk CLI kecil, Kotlin/Native lebih ringkas.

Referensi Lanjutan #

Selanjutnya #

Native binary menyelesaikan masalah startup dan footprint, tetapi konfigurasi runtime tetap perlu dikelola terpisah. Bab berikutnya membahas konfigurasi runtime: env vars, system properties, dan file konfigurasi (HOCON, YAML, TOML) — topik pada chapter Runtime Configuration.