tennarrates.

KMP: The Missing Introduction

What is Multiplatform

7 min read

Multiplatform means one source and many platforms, but the word hides a difference that matters. A developer who wants to share code hears the word and assumes it means the same thing. It does not.

1. The developer who wants cross-platform

A developer who wants cross-platform meets three doors. The doors are Flutter, React Native, and Kotlin Multiplatform. Kotlin Multiplatform is KMP for short. It is an open-source technology from JetBrains.

Flutter is a UI toolkit from Google. It ships a Flutter engine with the app. The engine is written in C++. React Native is a framework from Meta. It ships a JavaScript runtime with the app. The runtime is often Hermes.

The developer comes from a place. The place can be Java runtime, Apple, system programming, or web. The developer is not familiar with Kotlin. The developer wants to reuse code without rewriting it for each platform. The three doors are not the same kind of answer. The notes mark Flutter and React Native as not truly multiplatform. KMP is the true one. The article earns that mark by looking at what runs the code.

Cross-platform is a need. Multiplatform is a claim. The claim needs a definition. The definition starts with the platform.

2. Common ground: machine, virtual machine, target, artifact

A platform is what runs the code. A platform can be a machine or a virtual machine.

A machine is a CPU plus OS combination. A machine is named as an architecture-OS pair. Examples are aarch64-apple-macos and x86_64-unknown-linux-gnu. The name tells the build where to aim.

A virtual machine is an abstract computing machine with its own instruction set. The Java Virtual Machine is JVM. It runs .class bytecode. Android Runtime is ART. It runs DEX bytecode. V8 is a JavaScript engine. It runs JavaScript. Node.js is a runtime environment that uses V8. WebAssembly is a binary instruction format for a stack-based virtual machine. It is another virtual machine the web runs.

A target is a platform named so a build can aim at it. A target names the machine or virtual machine. An artifact is what a target’s compilation produces. An artifact is the file the platform can load and run.

The four words give common ground. The reader can now name where code runs and what it becomes. The reader can separate the platform from the code. A machine platform is physical. A virtual machine platform is abstract. Both are platforms because both run code. Naming them as targets makes builds repeatable. Artifacts make the relation concrete.

Two cells — machine (CPU+OS, named as arch-OS pairs) and virtual machine (JVM, ART, V8, wasm, each with its own bytecode) — under the line Two cells — machine (CPU+OS, named as arch-OS pairs) and virtual machine (JVM, ART, V8, wasm, each with its own bytecode) — under the line

The picture shows machine and virtual machine under the line platform equals what runs the code. It also defines target and artifact.

3. The four lenses: who is doing KMP

A developer doing KMP can come from four lenses. Each lens asks the same question: what will my code run on, and what does it become?

The Java runtime lens runs on JVM. It becomes a .jar. A .jar is a zip of .class files. The .class files are JVM bytecode. The JVM already understands bytecode. The artifact is the platform’s own.

text
  $ unzip -l build/libs/what-is-multiplatform-jvm.jar
  Archive:  build/libs/what-is-multiplatform-jvm.jar
    Length      Date    Time    Name
  ---------  ---------- -----   ----
           0  02-01-1980 00:00   META-INF/
          25  02-01-1980 00:00   META-INF/MANIFEST.MF
        1220  02-01-1980 00:00   GreetKt.class
         499  02-01-1980 00:00   Platform_jvmKt.class
          53  02-01-1980 00:00   META-INF/what-is-multiplatform.kotlin_module
  ---------                     -------
        1797                     5 files

  $ unzip -l build/outputs/aar/what-is-multiplatform-release.aar
  Archive:  build/outputs/aar/what-is-multiplatform-release.aar
    Length      Date    Time    Name
  ---------  ---------- -----   ----
           0  02-01-1980 00:00   R.txt
         217  02-01-1980 00:00   AndroidManifest.xml
        1652  02-01-1980 00:00   classes.jar
         156  02-01-1980 00:00   META-INF/com/android/build/gradle/aar-metadata.properties
  ---------                     -------
        2025                     4 files

The Android lens runs on ART. It becomes an .aar. An .aar is a zip of AndroidManifest.xml and classes.jar. The classes.jar contains DEX bytecode. ART already understands DEX.

The Apple lens runs on macOS or iOS machines. It becomes a .dylib, a .a, an .xcframework, a .framework, or an .ipa. A .dylib is a Mach-O shared library. A .a is an archive of object files. An .xcframework is a bundle with one slice per architecture. An .ipa is a zip of an app bundle. The Apple toolchain already understands Mach-O.

text
  $ file libdemo.dylib libdemo.a
  libdemo.dylib: Mach-O 64-bit dynamically linked shared library arm64
  libdemo.a:     current ar archive random library

  $ xcodebuild -create-xcframework -library libdemo.dylib -output demo.xcframework
  xcframework successfully written out to: /private/tmp/apple-artifacts/demo.xcframework

  $ find demo.xcframework -type f
  demo.xcframework/macos-arm64/libdemo.dylib
  demo.xcframework/Info.plist

  $ file Demo.framework/Demo        # dylib placed in the bundle layout
  Demo.framework/Demo: Mach-O 64-bit dynamically linked shared library arm64

  $ file MyApp.ipa                  # zip of a .app bundle
  MyApp.ipa: Zip archive data, at least v1.0 to extract, compression method=store

The system programmer lens runs on Windows, Linux, or Darwin machines. It becomes a .dll, a .so, or a .dylib. A .dll is a PE executable. A .so is an ELF shared object. The OS loader already understands PE and ELF.

text
  $ file build/bin/mingwX64/debugShared/what_is_multiplatform.dll \
         build/bin/linuxX64/debugShared/libwhat_is_multiplatform.so
  build/bin/mingwX64/debugShared/what_is_multiplatform.dll:      PE32+ executable (DLL) (GUI) x86-64, for MS Windows
  build/bin/linuxX64/debugShared/libwhat_is_multiplatform.so:    ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=23f87243221f7b4022ca208d9c08046fe320342e, with debug_info, not stripped

The web lens runs on a JS engine. It becomes .js or wasm. The kitchen build produced JavaScript and ran it under Node. The JS engine already understands JavaScript. WebAssembly is a binary format the browser already understands.

text
  > Task :jsNodeDevelopmentRun
  Hello from JavaScript
  BUILD SUCCESSFUL in 9s

Every row was built by one Gradle build from one source. The question is the same from each place. The answer is different because the platform is different. The developer does not change the question. The platform changes the answer. This is why the lenses matter.

A five-row table — Java runtime, Android, Apple, System, Web — each with what it runs on (JVM, ART, machines, JS engine) and the artifact it becomes (.jar, .aar, .framework/.xcframework/.ipa, .dll/.so/.dylib, .js/wasm). A five-row table — Java runtime, Android, Apple, System, Web — each with what it runs on (JVM, ART, machines, JS engine) and the artifact it becomes (.jar, .aar, .framework/.xcframework/.ipa, .dll/.so/.dylib, .js/wasm).

The table shows five rows. Each row shows lens, runs on, and becomes. The rows prove the same source can target many platforms.

4. What KMP actually is

KMP turns one source into many artifacts. The source is Kotlin code. The source uses expect and actual to separate common code from platform code.

kotlin
// src/commonMain/kotlin/Greet.kt
expect fun platformName(): String

fun greet(): String = "Hello from ${platformName()}"

fun main() {
    println(greet())
}

The build file names the targets. The targets are JVM, Android, macOS, Linux, Windows, and JS.

kotlin
// build.gradle.kts (targets)
kotlin {
    jvm()
    androidTarget()
    macosArm64()        // binaries { executable() }
    linuxX64()          // binaries { sharedLib() }
    mingwX64()          // binaries { sharedLib() }
    js(IR) { nodejs(); binaries.executable() }
}

The Kotlin compiler has a language frontend and language backends. The frontend is K2. It does semantic analysis, call resolution, and type inference. The frontend produces IR. IR is Intermediate Representation. IR is one for all backends.

Each backend emits what that platform already speaks. The JVM backend emits .class bytecode. The JS backend emits JavaScript. The Native backend emits native binaries via LLVM. Each backend speaks the platform’s own language.

The claim is: Kotlin becomes one more language the platform understands. The platform does not need a new runtime. The platform already has a toolchain. The frontend is shared. The backends are separate. The IR is the bridge. The bridge is stable. The backends evolve with the platform.

The kitchen build turned one Greet.kt into a jar, an aar, a Mach-O executable, an ELF shared object, a PE DLL, and a JS module. The same source ran under JVM, macOS native, and Node. The run proves the claim.

text
  $ file build/bin/macosArm64/debugExecutable/what-is-multiplatform.kexe
  build/bin/macosArm64/debugExecutable/what-is-multiplatform.kexe: Mach-O 64-bit executable arm64

  $ java -cp build/libs/what-is-multiplatform-jvm.jar:<kotlin-stdlib-2.2.0.jar> GreetKt
  Hello from JVM

  $ ./build/bin/macosArm64/debugExecutable/what-is-multiplatform.kexe
  Hello from Kotlin/Native

  $ gradle jsNodeDevelopmentRun
  > Task :jsNodeDevelopmentRun
  Hello from JavaScript

The JVM artifact contains ordinary JVM bytecode. javap decodes it. The bytecode is not KMP-specific. It is JVM bytecode.

text
  Compiled from "Greet.kt"
  public final class GreetKt {
    public static final java.lang.String greet();
    public static final void main();
    public static void main(java.lang.String[]);
      Code:
         0: invokestatic  #43                 // Method main:()V
         3: return
  }

KMP adds no platform layer of its own. The artifact is the platform’s own. The source is shared. The artifacts are native.

A pipeline — Kotlin source into the K2 language frontend into a shared IR, forking into four backends (JVM, JS, Native/LLVM, wasm) that emit .class, .js, .kexe/.so/.dll and .wasm; caption: each backend emits what the platform already speaks. A pipeline — Kotlin source into the K2 language frontend into a shared IR, forking into four backends (JVM, JS, Native/LLVM, wasm) that emit .class, .js, .kexe/.so/.dll and .wasm; caption: each backend emits what the platform already speaks.

The picture shows source into frontend into IR, then forking into four backends. Each backend emits what the platform already speaks.

5. Why KMP is multiplatform in the true sense

Flutter and React Native introduce their own layer of platform. The app ships with the Flutter engine and Dart runtime, or with a bundled Hermes JS engine. The app boots that layer before it runs. The code runs on the layer, not on the platform. The layer is carried inside the app.

KMP borrows the platform. The artifact runs on the platform’s own runtime. The JVM artifact runs on JVM. The native artifact runs on the OS. The JS artifact runs on a JS engine. The platform is not carried. It is borrowed.

The difference is visible inside the artifacts. The KMP native binary links only against system libraries. It links libSystem, libc++, libobjc, Foundation, and CoreFoundation. It does not ship a KMP runtime. The binary is the platform’s own. Flutter’s binary links against Flutter engine libraries. React Native’s binary links against Hermes. The layer is carried. The layer is visible.

text
  build/bin/macosArm64/debugExecutable/what-is-multiplatform.kexe:
  	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1359.0.0)
  	/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 2200.27.0)
  	/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 2280.0.0)
  	/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 5027.0.69)
  	/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 5027.0.69)

Whose platform runs the code? Flutter and React Native run on a platform they ship. KMP runs on the platform the user already has. That is the one-sentence difference the reader can state.

The series can now start. The reader has a working vocabulary. The reader can define target and artifact. The reader can map their background to one of the four lenses. The reader can explain why KMP is true multiplatform and Flutter and React Native are not.

Two boxes side by side — the carried platform (app plus Flutter engine, Dart runtime, or bundled Hermes) that boots its own layer, versus the borrowed platform (app carrying only the artifact, running on the platform's own runtime); footer: the KMP binary links only system libraries. Two boxes side by side — the carried platform (app plus Flutter engine, Dart runtime, or bundled Hermes) that boots its own layer, versus the borrowed platform (app carrying only the artifact, running on the platform's own runtime); footer: the KMP binary links only system libraries.