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:
- All references are tracked by Kotlin/Native garbage collector and nothing else.
- It can mangle declaration names freely.
- Kotlin functions are not called from other binaries, and unused declarations can be removed.
- It can use its own calling convention. For example, almost all Kotlin/Native functions have an additional parameter for a shadow stack.
This makes impossible calling Kotlin/Native binaries from the outside easily. Instead, we have to add a small C-compatible layer:
- This layer should not pass arbitrary objects. Only special wrappers (
ExternalRCRef
) that are tracked by the GC. - Function names in this layer should be predictable.
- Functions are excluded from dead code elimination.
- Non-trivial function signatures are converted in a predicatable way. For example,
suspend fun foo()
are wrapped withfoo_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:
- Kotlin declaration
@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)
}
- C declaration
void* pkg_foo_wrapper(void* a);
- Swift function
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.