Back to Compose Multiplatform

Swift export architecture

docs/swift-export/architecture.md

2.3.204.0 KB
Original Source

Swift export architecture

This document provides a high-level overview on how Swift export fits into the bigger "Kotlin" picture.

❗️ Swift export is in very early stages of development. Things change very quickly, and documentation might not catch up at times.

A bit of history: Objective-C export

Objective-C export was developed back in 2017 as a part of the Kotlin/Native compiler. It was pretty fine at the moment, given that Kotlin/Native was developed in a separate repository and the API surface of different Kotlin parts was pretty cumbersome.
Effectively, it consists of several global compiler passes:

  1. Generate Objective-C declarations from a tree of Kotlin declaration descriptors.
  2. Tie these Objective-C declarations to the corresponding backend IR nodes.
  3. Generate LLVM IR for Objective-C <-> Kotlin bridges.
  4. Pack everything into an Apple framework.

And it works pretty well! However, this approach has a number of drawbacks:

  1. It integrates poorly with an IDE.

    Objective-C export was written as a global "batch" tool while good IDE integration requires the local-first approach.

  2. Effectively, Kotlin/Native has 2 LLVM IR generators: one for Kotlin IR, and another for Objective-C bridges. Keeping these things in sync requires additional efforts.

  3. Objective-C export is tied to descriptors that are a K1 legacy, and moving it to K2 requires additional efforts.

  4. Integration with artifact kinds other than framework/xcframework might be tricky.

Swift export

Given Objective-C experience, we wanted to try something else in Swift export. Luckily, when we started Swift export in 2023, we had more tools at our disposal:

  1. Mighty Analysis API. While still a work in progress, Analysis API has a goal of creating a stable public API for Kotlin compiler frontend. It is developed with the IDE use-case in mind, given that it is used for the K2 IDE plugin. Moreover, it is suitable for standalone cases (KSP2, Dokka, Kapt 4) as well.
  2. The infrastructure around klibs and IR is much more mature. We can add a few unstable compiler intrinsics or stdlib declarations here and there if needed instead of relying on the compiler internals.

So after a few approaches, we settled down with a relatively simple architecture of Swift export: Swift export is a separate tool that acts as a lazy Analysis API to Swift translator. Additionally, it generates thin Kotlin bridges with a C header which effectively provides C API to Kotlin binary. These bridges are compiled by the Kotlin/Native compiler like any other sources and require little support in the compiler.

mermaid
---
title: Swift export overview
---
flowchart TD
    Klib["Kotlin Module (klib)"] --> AA("Analysis API")
    subgraph Swift export
        AA --> SwiftSession["SirSession"]
        SwiftSession --> SIR["Swift IR"]
        SIR --> SirPrinter("SIR Printer")
        SIR --> SirBridges("SIR compiler bridges generator")
    end
    SirPrinter  --> SwiftSources[Swift sources]
    SirBridges --> HeaderBridges[C header for Kotlin bridges]
    SirBridges --> KotlinBridges[Kotlin bridges]

    SwiftSources -.-> HeaderBridges
    HeaderBridges -.-> KotlinBridges

    Klib --> Compiler(Kotlin/Native compiler)
    KotlinBridges --> Compiler
    Compiler  --> Binary[Kotlin Dynamic/Static library]
    
    
    SwiftSources --> ExternalTooling("Gradle / Other build system")
    HeaderBridges --> ExternalTooling
    Binary --> ExternalTooling
    ExternalTooling --> Artifact["Framework / XCFramework / Swift Package / ..."]

We consider this approach nice for a few reasons:

  • Separate components of Swift export might be embedded into other environments (IDE, Kotlin Playground) as well.
  • There is a pretty explicit ABI border between compiler/runtime and Swift export, which simplifies the evolution of both counterparts.
    • Moreover, it would be possible to separate release cycles of these counterparts if needed.
  • Potentially, we can reuse the same ABI border for other exports (C/Objective-C, for example).