Skip to content

Compiling for "external" clients

Dmitry Savvinov edited this page Nov 9, 2022 · 5 revisions

In the end, the code you wrote in Kotlin Multiplatform will be launched somewhere: on an Android or iOS device, on local JVM, in web-browser, etc. In this case, we're concerned about launching code on an Android or iOS device.

This process roughly can be described as two steps:

  1. Assembling the raw platform-specific output of the Kotlin Compiler
  2. Transforming (usually packaging or linking) that raw output into a form, consumable by "external" clients

Assembling platform-specific Kotlin Compiler output

There are four important things to understand about platform-specific compilations of Kotlin Multiplatform:

  1. Common sources of the current "module" are passed as sources.

Platform-specific compilations don't use some binary output of common-sources compilation. This is an oddity in the design of Kotlin Multiplatform which holds at the time of writing this (Kotlin 1.7.20). There are plans to change that to a more conventional compilation scheme (where first common sources are compiled into some binary form - e.g., .klib, - and then the platform compiler receives that .klib as an input).

Example. Let's look at AssembleJvm task called on TransitiveDependency. You can see that the free args (sources to be compiled) are looking like this:

<...>/TransitiveDependency/src/jvmMain 
<...>/TransitiveDependency/src/commonMain

Note how commonMain-sources are passed just alongside jvmMain-sources.

  1. Produced artifact is "fat" in the sense that it includes a common API of the current "module",

Convenient way to think about this is that we provide a platform-specific view on the Kotlin Multiplatform code. This transforms the common code in an interesting way: not only all expects are substituted with actuals, but also some Kotlin-stdlib declarations are substituted with their platform counterparts (if any).

Example. Let's again look at the output of AssembleJvm called on TransitiveDependency:

barebones-kotlin-multiplatform/buildInfra/output/TransitiveDependency/assembly/jvmMain/org/jetbrains/kotlin/transitive/
|- JustCommonKt.class
|- TransitiveJvmKt.class
L TypealiasExpansion.class

First of all, we can see that the file commonMain/justCommon.kt with the simple function fun justCommon(): String got compiled into respective .class-file. This illustrates the point of platform-specific artifacts being "fat" and including the common API.

Second, let's observe that the file commonMain/TransitiveCommon.kt doesn't have a corresponding class-file. It's because the only declaration in this file is expect class TransitiveCommon, which got replaced by actual class TransitiveCommon, declared in transitiveJvm.kt (and thus it placed in TransitiveJvmKt.class). So, as we see, expect-classes are replaced with actuals

Finally, let's illustrate that some Kotlin-primitives are replaced with their platform counterparts as well. Let's look at bytecode of JustCommonKt:

...
public static final java.lang.String justCommon()
...

Just as a reminder, the function was initially declared in common code as fun justCommon(): String, where String is obviously a kotlin.String (as you wouldn't have a java.lang.String in common code). We can see that this type from common code was replaced by the platform-specific counterpart for kotlin.String during JVM compilation.

If you think about it, this is a pretty natural process: the purpose of those artifacts is to be exposed to "external" clients that don't know anything about Kotlin Multiplatform, so it makes perfect sense to put all common API there as well, and replace all traces of Kotlin Multiplatform with platform-specifics

  1. Platform artifacts compile against similar platform artifacts

It's pretty self-explanatory. Let's look at the example of AssembleJvm in DirectDependency:

-classpath <...>/buildInfra/output/TransitiveDependency/packed/jvm.jar
          :<... stdlib declarations ...>

So, we're compiling platform-specific output against the (packed) similar platform-specific output of dependencies.

Transforming Compiler's raw output into a form, consumable by "external" clients

JVM

The raw output of K/JVM compilation consists of the usual JVM .classfiles and .kotlin_module file in META-INF folder. This file, along with @kotlin.Metadata annotation in the .classfiles, carries Kotlin-specific information that is not expressible by mechanisms built-in into .classfiles (examples: Kotlin-specific modifiers like suspend or inline)

This output is usually distributed in form of the traditional .jar-file. The transformation, therefore, is the trivial process of packing .classfiles into .jar-file, and actually, it's not even that necessary, as external JVM clients can consume .classfiles just fine. For example, Kotlin Gradle Plugin uses exactly raw .classfiles when handling local dependencies from one local Kotlin Gradle Project to another Kotlin Gradle Project.

Native

The raw output of K/Native compilation is the Kotlin-specific format called .klib. It contains Kotlin-invented binary format of representing the code, called Kotlin IR.

As this format is Kotlin-specific, the process of transforming this output into something consumable by external Native clients (Xcode, for example), is much less trivial than .jar-ing files. This process is called "linkage", see the LinkNative tasks.