| # Swift Export Execution Model — standalone |
| |
| This document provides an overview of what `swift-export-standalone` runner does under the hood |
| and how it should be connected to the outside world. |
| |
| > ❗️ Swift export is in the very early stages of development. |
| > Things change very quickly, and documentation might sometimes not catch up. |
| |
| ## Terminology |
| |
| 1. `swift-export-standalone` runner, Standalone Executor — an actor, designed for Build Tooling consumption. Provides the ability to translate a set of interlinked klibs into a set of `Swift modules`. |
| 2. Output `Swift module` - represents a single Swift module and its dependencies. It will consist of a `.swift` file with the resulting API and may contain the additional bridges to call the compiled Kotlin code. The structure of the bridges and their purpose are described in the [Architecture section](architecture.md). |
| 3. `synthetic` module - Swift Module, that has no counterpart in the original sources. |
| |
| ## Preconditions |
| |
| Standalone Executor expects every module that ought to be translated to be precompiled. That allows the following: |
| 1. guarantees the validity of input |
| 2. unifies the input — everything is a klib |
| |
| Standalone Executor expects that every module involved in the public API will be passed to it. |
| See details on [Multimodule resolve](#multimodule-resolve). |
| |
| ## Execution |
| |
| As an input, Swift Standalone Executor receives a set of klibs. |
| This means that the dependency tree was flattened somewhere in the build tooling. |
| This fact adds another task for the execution: it has to reconstruct the dependency graph for the resulting Swift modules. |
| |
| Given that, the Runner solves two problems: |
| 1. It should execute a single module translation for every Kotlin module that was passed as an input. |
| 2. It should collect all the results from the previous step, enrich them with the synthetic modules, and construct a resulting dependency graph for them. |
| |
| ### Translation of every Kotlin module |
| |
| For each klib from the input, the Runner will construct its own Analysis API Session. |
| Analysis API Session requires having a source module as a root, |
| which means that we will create a synthetic source module and add every binary module to it as a dependency. |
| There is no need to reconstruct the actual dependency graph at this point, all the binary dependencies are passed as a flat list. |
| For the reasoning behind this step, please consult [Multimodule resolve](#multimodule-resolve). |
| |
| For each such Analysis API session, a single module translation will be performed. |
| For the details on this please consult [Architecture section](architecture.md). |
| |
| ### Construct a resulting dependency graph |
| |
| The resulting dependency graph can be found by analyzing the `imports` for each resulting SwiftModule. |
| Therefore, in this step, the Runner collects all the imports for each module and converts them into an actual graph. |
| |
| ## Packages |
| |
| The main approach with package translation can be found [here](language-mapping.md#packages). |
| However, with the introduction of multiple modules, there is a problem. |
| Consider the following code: |
| |
| ```kotlin |
| // MODULE: one |
| // FILE: one.kt |
| package namespace |
| |
| fun foo(): Unit = TODO() |
| ``` |
| ```kotlin |
| // MODULE: two |
| // FILE: two.kt |
| package namespace |
| |
| fun bar(): Unit = TODO() |
| ``` |
| |
| Naive translation for this will produce 2 separate enums on the Swift side for each module: |
| ```Swift |
| // MODULE: one |
| enum namespace {} |
| extension namespace { |
| func foo() { /**/ } |
| } |
| // MODULE: two |
| enum namespace {} |
| extension namespace { |
| func bar() { /**/ } |
| } |
| ``` |
| |
| Which will lead to strange constructions on the call site when both of those modules are imported: |
| ```Swift |
| import one |
| import two |
| |
| extension one.namespace { // <- we are forced to use module name, as there is more than one "namespace" |
| } |
| ``` |
| |
| To solve this problem, we are introducing a synthetic `ExportedKotlinPackages` module. |
| It stores a hierarchy of Swift enums, which represents a tree of Kotlin packages that were encountered during translation. |
| |
| With that addition, the previous example becomes this: |
| ```Swift |
| // MODULE: ExportedKotlinPackages |
| enum namespace {} |
| // MODULE: one |
| @_exported import ExportedKotlinPackages |
| |
| extension namespace { |
| func foo() { /**/ } |
| } |
| // MODULE: two |
| @_exported import ExportedKotlinPackages |
| |
| extension namespace { |
| func bar() { /**/ } |
| } |
| ``` |
| |
| This provides us with a nice common namespace between different modules, leaving the user with a feeling of a common package: |
| ```Swift |
| import one |
| import two |
| |
| extension namespace { |
| func test() { |
| foo() |
| bar() |
| } |
| } |
| ``` |
| |
| ## Multimodule resolve |
| |
| Consider the following code: |
| |
| ```kotlin |
| // MODULE: dep |
| // FILE: foo.kt |
| class Foo |
| ``` |
| ```kotlin |
| // MODULE: main(dep) |
| // FILE: main.kt |
| import Foo |
| val f = Foo() |
| ``` |
| |
| This code makes the type Foo part of the public API surface. |
| It is impossible to fully translate the module `main` without the information about `Foo`. |
| Therefore, we need two steps: |
| 1. The module `dep` should be translated by the Swift Export |
| 2. The module `main` should be translated with the knowledge of the existence of module `dep` |
| |
| Without those two steps, the module `main` translation will encounter an unknown type. |
| Depending on the Standalone Executor configuration, this will either lead to an error or be translated as `Swift.Never`. |
| Neither of those options is desirable to the user. |
| |
| For the first step, we cannot take every module and translate it – this will force every module in the compilation to become public. |
| This in turn will lead to an enormous public API surface, |
| which will render any possibility of DCE(dead code elimination) almost impossible. |
| Which will lead to an enlarged binary size. |
| Therefore, we need a mechanism to separate modules into two groups – the ones that do form a public API surface and those that are not. |
| And this separation cannot be made automatically, |
| because klib format does not make a distinction between `api` and `implementation` dependencies. |
| Therefore, |
| there is a Gradle DSL for declaring which modules should be exported, |
| and that is the responsibility of the end-user to mark all modules that are part of the public API surface as such. |
| |
| The second step is already done by the user, but that information is lost before the call of the Swift Export Runner. |
| This restriction can be overcome by the fact that we don't need it: |
| Analysis API does not require the actual dependency tree – it is allowed for the dependency list to be flattened. |
| Actually, Analysis API does not require even the dependencies between binary modules. |
| The absence of these restrictions allows us to do the following for the Standalone Executor: |
| |
| When translating a single module from the requested set, |
| Swift Export Runner will put every binary module as a dependency for the fake source module. |
| This will provide the Analysis API with the ability to find every necessary declaration during the translation. |
| |
| As a side effect: this will also extract all the transitive dependencies for the particular Swift Module and make them direct. |
| Also, this step accidentally cleans up any unused dependencies marked as `api` on the Gradle side, |
| forcing the dependency graph on the Swift side to be as accurate as possible. |
| |
| This behavior best be illustrated by an example. Consider the following code: |
| ```kotlin |
| // MODULE: one |
| class Foo |
| // MODULE: two |
| // build.gradle |
| dependencies { |
| api("one") |
| } |
| // two.kt |
| fun foo(): Foo = Foo() |
| |
| // MODULE: main |
| // build.gradle |
| dependencies { |
| api("two") |
| } |
| // main.kt |
| fun main(): Foo = foo() |
| ``` |
| Technically, from the user perspective, there is no direct dependency between main and one modules, |
| as this configuration can be represented visually like this: |
| ```mermaid |
| flowchart LR |
| main -->|depends| two |
| two --> |depends| one |
| ``` |
| |
| But, after the swift export is done with transforming this API, we will produce the following graph: |
| ```mermaid |
| flowchart LR |
| main -->|depends| one |
| two -->|depends| one |
| ``` |