docs/swift-export/compiler-bridges.md
This document gives an overview on how bridges between Swift wrappers and Kotlin declarations are implemented.
❗️ Swift export is in very early stages of development. Things change very quickly, and documentation might not catch up at times.
Swift does know nothing about Kotlin code, and how to call it. But Swift has a pretty good C/Objective-C interop, and we can exploit that by exporting Kotlin code as a C library.
By default, Kotlin/Native makes a few assumptions when it compiles the code:
This makes impossible calling Kotlin/Native binaries from the outside easily. Instead, we have to add a small C-compatible layer:
ExternalRCRef) that are tracked by the GC.suspend fun foo() are wrapped with foo_wrapper(cont: ExternalRCRef)So for the given Kotlin function
package pkg
public fun foo(a: Foo): Bar { ... }
We can automatically create the following wrapper:
@CWrapper("pkg_foo_wrapper")
public fun foo_wrapper(a: ExternalRCRef): ExternalRCRef {
val a_inner = unwrap(a)
val result_inner = pkg.foo(a_inner)
return wrap(result_inner)
}
and the corresponding C header declaration:
void* pkg_foo_wrapper(void* a);
…Is pretty simple. Swift supports clang modules, so we can wrap an arbitrary C header with module.modulemap, and import it in Swift.
header.h:
int foo();
module.modulemap
module Bridge {
umbrella header <path-to-header>
export *
}
main.swift
import Bridge
print(foo())
When Swift export encounters a Kotlin function fun foo(a: Foo): Bar, it creates 3 declarations:
@CWrapper("pkg_foo_wrapper")
public fun foo_wrapper(a: ExternalRCRef): ExternalRCRef {
val a_inner = unwrap(a)
val result_inner = pkg.foo(a_inner)
return wrap(result_inner)
}
void* pkg_foo_wrapper(void* a);
import KotlinBridges
public func foo(a: Foo) -> Bar {
let a_ref = convertToExternalRCRef(a)
let result_ref = pkg_foo_wrapper(a_ref)
return convertFromExternalRCRef(result_ref)
}
Integration of memory managers is a pretty complicated topic, especially when we need to consider things like weak refs, object identity,
multithreading and so on.
Luckily, we already solved this problem in Objective-C export. All exported Objective-C classes inherit from KotlinBase.
KotlinBase overrides retain and release implementations (yep, it is possible in Objective-C export thanks to its dynamic nature!), and
calls Kotlin/Native GC operations under the hood. But what should we do in Swift export, where such overrides are not possible?
Luckily for us, Swift allows inheriting from Objective-C classes!
This allows us to do an amazing thing: reuse existing integration of memory managers by inheriting
all generated Swift classes from the same old KotlinBase (with a few adjustments), and avoid solving the same problems over again.
We might consider implementing another integration scheme in the future, but at the current state of development, this approach is sufficient.