Kotlin/Native as an Apple Framework

Last Updated

15 April 2019

Compiling Kotlin/Native code and use it from Objective-C and Swift

Kotlin/Native provides bi-directional interoperability with Objective-C/Swift.
Objective-C frameworks and libraries can be used in Kotlin code.
Kotlin modules can be used in Swift/Objective-C code too.
Besides that, Kotlin/Native has
C Interop.
There is also the Kotlin/Native as a Dynamic Library
tutorial for more information.

In this tutorial, we will look at how to use Kotlin/Native code from
Objective-C and Swift applications on macOS and iOS.
We will build a framework from Kotlin code.

Creating a Kotlin Library

Kotlin/Native compiler can produce a framework for macOS and iOS
out of the Kotlin code. The created framework contains all declarations
and binaries needed to use it with Objective-C and Swift.
The best way to understand the techniques is to try it for ourselves.
Let's create a tiny Kotlin library first and use it from an Objective-C program.

While it is possible to use the command line, either directly or
by combining it with a script file (i.e., sh or bat file), we should notice,
that it does not scale well for big projects that have hundreds of files and libraries.
It is then better to use the Kotlin/Native compiler with a build system, as it
helps to download and cache the Kotlin/Native compiler binaries and libraries with
transitive dependencies and run the compiler and tests.
Kotlin/Native can use the Gradle build system through the
kotlin-multiplatform plugin.

We covered the basics of setting up an IDE compatible project with Gradle in the
A Basic Kotlin/Native Application
tutorial. Please check it out if you are looking for detailed first steps
and instructions on how to start a new Kotlin/Native project and open it in IntelliJ IDEA.
In this tutorial, we'll look at the advanced C interop related usages of Kotlin/Native
and
multiplatform
builds with Gradle.

First, let's create a project folder. All the paths in this tutorial will be relative to this folder. Sometimes
the missing directories will have to be created before any new files can be added.

We'll use the following
build.gradlebuild.gradle.kts
Gradle build file with the following contents:

The prepared project sources can be directly downloaded from
GitHub.
GitHub.

Let's move the sources file into the src/nativeMain/kotlin folder under
the project. That is the default path, where sources are located, when
the kotlin-multiplatform
plugin is used. We use the following block to instruct configure the project
to generate a dynamic or shared library for us:

binaries {
framework {
baseName = "Demo"
}
}

Along with macOS X64, Kotlin/Native supports iOS arm32, arm64 and X64
targets. We may replace the macosX64 with respective functions as shown
in the table:

Target platform/device

Gradle function

macOS x86_64

macosX64()

iOS ARM 32

iosArm32()

iOS ARM 64

iosArm64()

iOS Simulator (x86_64)

iosX64()

Let's run the linkNative Gradle task to build the library
in the IDE
or by calling the following console command:

./gradlew linkNative

./gradlew linkNative

gradlew.bat linkNative

Depending on the variant, the build generates the framework
into the
build/bin/native/debugFramework
and
build/bin/native/releaseFramework
folders.
Let's see what is inside

Generated Framework Headers

Each of the created frameworks contains the header file in <Framework>/Headers/Demo.h.
The headers do not depend on the target platform (at least with Kotlin/Native v.0.9.2).
It contains the definitions for our Kotlin code and a few Kotlin-wide declarations.

Note, the way Kotlin/Native exports symbols is subject to change without notice.

Kotlin classes have a KotlinBase base class in Objective-C, the class extends
the NSObject class there. We also have wrappers for collections and exceptions.
Most of the collection types are mapped to similar collection types from the other side:

Kotlin

Swift

Objective-C

List

Array

NSArray

MutableList

NSMutableArray

NSMutableArray

Set

Set

NSSet

Map

Dictionary

NSDictionary

MutableMap

NSMutableDictionary

NSMutableDictionary

Kotlin Numbers and NSNumber

The next part of the <Framework>/Headers/Demo.h contains number type mappings
between Kotlin/Native and NSNumber. We have the base class called DemoNumber in Objective-C
and KotlinNumber in Swift. It extends NSNumber.
There are also child classes per Kotlin number type:

Kotlin

Swift

Objective-C

Simple type

-

KotlinNumber

<Package>Number

-

Byte

KotlinByte

<Package>Byte

char

UByte

KotlinUByte

<Package>UByte

unsigned char

Short

KotlinShort

<Package>Short

short

UShort

KotlinUShort

<Package>UShort

unsigned short

Int

KotlinInt

<Package>Int

int

UInt

KotlinUInt

<Package>UInt

unsigned int

Long

KotlinLong

<Package>Long

long long

ULong

KotlinULong

<Package>ULong

unsigned long long

Float

KotlinFloat

<Package>Float

float

Double

KotlinDouble

<Package>Double

double

Boolean

KotlinBoolean

<Package>Boolean

BOOL/Bool

Every number type has a class method to create a new instance from the related simple type. Also, there is an instance method
to extract a simple value back. Schematically, declarations look like that:

The code is full of Objective-C attributes, which are intended to help
the use of the framework from both Objective-C and Swift languages.
DemoClazz, DemoInterface, and DemoObject are created for Clazz, Interface, and Object
respectively. The Interface is turned into @protocol, both a class and an object are represented as
@interface.
The Demo prefix comes from the -output parameter
of the kotlinc-native compiler and the framework name.
We see here that the nullable return type ULong? is turned into DemoLong* in Objective-C.

Global Declarations from Kotlin

All global functions from Kotlin
are turned into DemoLibKt in Objective-C and into LibKt in Swift, where Demo is the framework name and set by
the -output parameter of kotlinc-native.

We see that Kotlin String and Objective-C NSString* are mapped transparently.
Similarly, Unit type from Kotlin is mapped to void. We see primitive types
are mapped directly. Non-nullable primitive types are mapped transparently.
Nullable primitive types are mapped into Kotlin<TYPE>* types, as shown in the table above.
Both higher order functions acceptFunF and supplyFun are included,
and accept Objective-C blocks.

More information about all other types mapping details can be found in the
Objective-C Interop
documentation article

Garbage Collection and Reference Counting

Objective-C and Swift use reference counting. Kotlin/Native has its own garbage collection too.
Kotlin/Native garbage collection is integrated with Objective-C/Swift reference
counting. We do not need to use anything special to control the lifetime of Kotlin/Native instances
from Swift or Objective-C.

Using the Code from Objective-C

Let's call the framework from Objective-C. For that we create the main.m file with
the following content:

We call Kotlin classes directly from Objective-C code. A Kotlin object has the class method
function object, which allows us to get the only instance of the object and to call
Object methods on it.
The widespread pattern is used to create an instance of the Clazz class. We call
the [[ DemoClazz alloc] init] on Objective-C. We may also use [DemoClazz new]
for constructors without parameters.
Global declarations from the Kotlin sources are scoped under the DemoLibKt class in Objective-C.
All methods are turned into class methods of that class.
The strings function is turned into DemoLibKt.stringsStr function in Objective-C, we can
pass NSString directly to it. The return is visible as NSString too.

Using the Code from Swift

The framework that we compiled with Kotlin/Native has helper attributes to make it
easier to use with Swift. Let's convert the previous Objective-C example
into Swift. As a result, we'll have the following code in main.swift:

The Kotlin code is turned into very similar looking
code in Swift. There are some small differences, though. In Kotlin any object has
only one instance. Kotlin object Object now has a
constructor in Swift, and we use the Object() syntax to access the only instance of it.
The instance is always the same in Swift, so that
Object() === Object() is true.
Methods and property names are translated as-is. Kotlin String is turned into Swift String too.
Swift hides NSNumber* boxing from us too. We pass Swift closure to Kotlin and call a Kotlin
lambda function from Swift too.

Xcode and Framework Dependencies

We need to configure an Xcode project to use our framework. The configuration depends on the
target platform.

Xcode for MacOS Target

First, we need to include the framework in the General section of the target
configuration. There is the Linked Frameworks and Libraries section to include
our framework. This will make Xcode look at our framework and resolve imports both
from Objective-C and Swift.

The second step is to configure the framework search path of the produced
binary. It is also known as rpath or run-time search path.
The binary uses the path to look for the required frameworks. We do not recommend
installing additional frameworks to the OS if it is not needed. We should understand the layout
of our future application, for example,
we may have the Frameworks folder under the application bundle with all the frameworks we use.
The @rpath parameter can be configured in Xcode. We need to open
the project configuration and find the Runpath Search Paths section. Here we specify
the relative path to the compiled framework.

Xcode for iOS Targets

First, we need to include the compiled framework into the Xcode project. For
this we add the framework to the Embedded Binaries block of the General section of
the target configuration page.

The second step is to then include the framework path into the Framework Search Paths block
of the Build Settings section of the target configuration page. It is possible to use $(PROJECT_DIR)
macro to simplify the setup.

The iOS simulator requires a framework compiled for the ios_x64 target, the iOS_sim folder
in our case.

Next Steps

Kotlin/Native has bidirectional interop with Objective-C and Swift languages.
Kotlin objects integrate with Objective-C/Swift reference counting. Unused Kotlin
objects are automatically removed.
The Objective-C Interop
article contains more information on the interop implementation details.
Of course, it is possible to import an existing framework and use it from Kotlin. Kotlin/Native
comes with a good set of pre-imported system frameworks.