Kotlin Help

Kotlin/Native as an Apple framework – tutorial

Kotlin/Native provides bidirectional interoperability with Swift/Objective-C. You can both use Objective-C frameworks and libraries in Kotlin code, and Kotlin modules in Swift/Objective-C code.

Kotlin/Native comes with a set of pre-imported system frameworks; it's also possible to import an existing framework and use it from Kotlin. In this tutorial, you'll learn how to create your own framework and use Kotlin/Native code from Swift/Objective-C applications on macOS and iOS.

In this tutorial, you will:

You can use the command line to generate a Kotlin framework, either directly or with a script file (such as .sh or .bat file). However, this approach doesn't scale well for big projects that have hundreds of files and libraries. Using a build system simplifies the process by downloading and caching 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.

Create a Kotlin library

The Kotlin/Native compiler can produce a framework for macOS and iOS from the Kotlin code. The created framework contains all declarations and binaries needed to use it with Swift/Objective-C.

Let's first create a Kotlin library:

  1. In the src/nativeMain/kotlin directory, create the lib.kt file with the library contents:

    package example object Object { val field = "A" } interface Interface { fun iMember() {} } class Clazz : Interface { fun member(p: Int): ULong? = 42UL } fun forIntegers(b: Byte, s: UShort, i: Int, l: ULong?) { } fun forFloats(f: Float, d: Double?) { } fun strings(str: String?) : String { return "That is '$str' from C" } fun acceptFun(f: (String) -> String?) = f("Kotlin/Native rocks!") fun supplyFun() : (String) -> String? = { "$it is cool!" }
  2. Update your build.gradle(.kts) Gradle build file with the following:

    plugins { kotlin("multiplatform") version "2.1.0" } repositories { mavenCentral() } kotlin { iosArm64("native") { binaries { framework { baseName = "Demo" } } } } tasks.wrapper { gradleVersion = "8.10" distributionType = Wrapper.DistributionType.ALL }
    plugins { id 'org.jetbrains.kotlin.multiplatform' version '2.1.0' } repositories { mavenCentral() } kotlin { iosArm64("native") { binaries { framework { baseName = "Demo" } } } } wrapper { gradleVersion = "8.10" distributionType = "ALL" }

    The binaries {} block configures the project to generate a dynamic or shared library.

    Kotlin/Native supports the iosArm64, iosX64, and iosSimulatorArm64 targets for iOS, as well as macosX64 and macosArm64 targets for macOS. So, you can replace the iosArm64() with the respective Gradle function for your target platform:

    Target platform/device

    Gradle function

    macOS x86_64

    macosX64()

    macOS ARM64

    macosArm64()

    iOS ARM64

    iosArm64()

    iOS Simulator (x86_64)

    iosX64()

    iOS Simulator (ARM64)

    iosSimulatorArm64()

    For information on other supported Apple targets, see Kotlin/Native target support.

  3. Run the linkDebugFrameworkNative Gradle task in the IDE or use the following console command in your terminal to build the framework:

    ./gradlew linkDebugFrameworkNative

The build generates the framework into the build/bin/native/debugFramework directory.

Generated framework headers

Each framework variant contains a header file. The headers don't depend on the target platform. Header files contain definitions for your Kotlin code and a few Kotlin-wide declarations. Let's see what's inside.

Kotlin/Native runtime declarations

In the build/bin/native/debugFramework/Demo.framework/Headers directory, open the Demo.h header file. Take a look at Kotlin runtime declarations:

NS_ASSUME_NONNULL_BEGIN #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunknown-warning-option" #pragma clang diagnostic ignored "-Wincompatible-property-type" #pragma clang diagnostic ignored "-Wnullability" #pragma push_macro("_Nullable_result") #if !__has_feature(nullability_nullable_result) #undef _Nullable_result #define _Nullable_result _Nullable #endif __attribute__((swift_name("KotlinBase"))) @interface DemoBase : NSObject - (instancetype)init __attribute__((unavailable)); + (instancetype)new __attribute__((unavailable)); + (void)initialize __attribute__((objc_requires_super)); @end @interface DemoBase (DemoBaseCopying) <NSCopying> @end __attribute__((swift_name("KotlinMutableSet"))) @interface DemoMutableSet<ObjectType> : NSMutableSet<ObjectType> @end __attribute__((swift_name("KotlinMutableDictionary"))) @interface DemoMutableDictionary<KeyType, ObjectType> : NSMutableDictionary<KeyType, ObjectType> @end @interface NSError (NSErrorDemoKotlinException) @property (readonly) id _Nullable kotlinException; @end

Kotlin classes have a KotlinBase base class in Swift/Objective-C that extends the NSObject class there. There are also wrappers for collections and exceptions. Most of the collection types are mapped to similar collection types in Swift/Objective-C:

Kotlin

Swift

Objective-C

List

Array

NSArray

MutableList

NSMutableArray

NSMutableArray

Set

Set

NSSet

MutableSet

NSMutableSet

NSMutableSet

Map

Dictionary

NSDictionary

MutableMap

NSMutableDictionary

NSMutableDictionary

Kotlin numbers and NSNumber

The next part of the Demo.h file contains type mappings between Kotlin/Native number types and NSNumber. The base class is called DemoNumber in Objective-C and KotlinNumber in Swift. It extends NSNumber.

For each Kotlin number type, there is a corresponding predefined child class:

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 corresponding simple type. Also, there is an instance method to extract a simple value back. Schematically, all such declarations look like that:

__attribute__((swift_name("Kotlin__TYPE__"))) @interface Demo__TYPE__ : DemoNumber - (instancetype)initWith__TYPE__:(__CTYPE__)value; + (instancetype)numberWith__TYPE__:(__CTYPE__)value; @end;

Here, __TYPE__ is one of the simple type names, and __CTYPE__ is the corresponding Objective-C type, for example, initWithChar(char).

These types are used to map boxed Kotlin number types to Swift/Objective-C. In Swift, you can call the constructor to create an instance, for example, KotlinLong(value: 42).

Classes and objects from Kotlin

Let's see how class and object are mapped to Swift/Objective-C. The generated Demo.h file contains the exact definitions for Class, Interface, and Object:

__attribute__((swift_name("Interface"))) @protocol DemoInterface @required - (void)iMember __attribute__((swift_name("iMember()"))); @end __attribute__((objc_subclassing_restricted)) __attribute__((swift_name("Clazz"))) @interface DemoClazz : DemoBase <DemoInterface> - (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer)); + (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead"))); - (DemoULong * _Nullable)memberP:(int32_t)p __attribute__((swift_name("member(p:)"))); @end __attribute__((objc_subclassing_restricted)) __attribute__((swift_name("Object"))) @interface DemoObject : DemoBase + (instancetype)alloc __attribute__((unavailable)); + (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable)); + (instancetype)object __attribute__((swift_name("init()"))); @property (class, readonly, getter=shared) DemoObject *shared __attribute__((swift_name("shared"))); @property (readonly) NSString *field __attribute__((swift_name("field"))); @end

Objective-C attributes in this code help use the framework from both Swift and Objective-C languages. DemoInterface, DemoClazz, and DemoObject are created for Interface, Clazz, and Object, respectively.

The Interface is turned into @protocol, while both a class and an object are represented as @interface. The Demo prefix comes from the framework name. The nullable return type ULong? is turned into DemoULong 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 set by the -output parameter of kotlinc-native:

__attribute__((objc_subclassing_restricted)) __attribute__((swift_name("LibKt"))) @interface DemoLibKt : DemoBase + (NSString * _Nullable)acceptFunF:(NSString * _Nullable (^)(NSString *))f __attribute__((swift_name("acceptFun(f:)"))); + (void)forFloatsF:(float)f d:(DemoDouble * _Nullable)d __attribute__((swift_name("forFloats(f:d:)"))); + (void)forIntegersB:(int8_t)b s:(uint16_t)s i:(int32_t)i l:(DemoULong * _Nullable)l __attribute__((swift_name("forIntegers(b:s:i:l:)"))); + (NSString *)stringsStr:(NSString * _Nullable)str __attribute__((swift_name("strings(str:)"))); + (NSString * _Nullable (^)(NSString *))supplyFun __attribute__((swift_name("supplyFun()"))); @end

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

You can find more information about type mapping in Interoperability with Swift/Objective-C.

Garbage collection and reference counting

Swift and Objective-C use automatic reference counting (ARC). Kotlin/Native has its own garbage collector, which is also integrated with Objective-C/Swift ARC.

Unused Kotlin objects are automatically removed. You don't need to take additional steps to control the lifetime of Kotlin/Native instances from Swift or Objective-C.

Use code from Objective-C

Let's call the framework from Objective-C. In the framework directory, create the main.m file with the following code:

#import <Foundation/Foundation.h> #import <Demo/Demo.h> int main(int argc, const char * argv[]) { @autoreleasepool { [DemoObject.shared field]; DemoClazz* clazz = [[ DemoClazz alloc] init]; [clazz memberP:42]; [DemoLibKt forIntegersB:1 s:1 i:3 l:[DemoULong numberWithUnsignedLongLong:4]]; [DemoLibKt forIntegersB:1 s:1 i:3 l:nil]; [DemoLibKt forFloatsF:2.71 d:[DemoDouble numberWithDouble:2.71]]; [DemoLibKt forFloatsF:2.71 d:nil]; NSString* ret = [DemoLibKt acceptFunF:^NSString * _Nullable(NSString * it) { return [it stringByAppendingString:@" Kotlin is fun"]; }]; NSLog(@"%@", ret); return 0; } }

Here, you call Kotlin classes directly from Objective-C code. A Kotlin object uses the <object name>.shared class property, which allows you to get the object's only instance and call object methods on it.

The widespread pattern is used to create an instance of the Clazz class. You call the [[ DemoClazz alloc] init] on Objective-C. You can also use [DemoClazz new] for constructors without parameters.

Global declarations from the Kotlin sources are scoped under the DemoLibKt class in Objective-C. All Kotlin functions are turned into class methods of that class.

The strings function is turned into DemoLibKt.stringsStr function in Objective-C, so you can pass NSString directly to it. The return value is visible as NSString too.

Use code from Swift

The framework you generated has helper attributes to make it easier to use with Swift. Let's convert the previous Objective-C example into Swift.

In the framework directory, create the main.swift file with the following code:

import Foundation import Demo let kotlinObject = Object.shared let field = Object.shared.field let clazz = Clazz() clazz.member(p: 42) LibKt.forIntegers(b: 1, s: 2, i: 3, l: 4) LibKt.forFloats(f: 2.71, d: nil) let ret = LibKt.acceptFun { "\($0) Kotlin is fun" } if (ret != nil) { print(ret!) }

There are some small differences between the original Kotlin code and its Swift version. In Kotlin, any object declaration has only one instance. The Object.shared syntax is used to access this single instance.

Kotlin function and property names are translated as is. Kotlin's String is turned into Swift's String. Swift hides NSNumber* boxing too. You can also pass a Swift closure to Kotlin and call a Kotlin lambda function from Swift.

You can find more information about type mapping in Interoperability with Swift/Objective-C.

Connect the framework to your iOS project

Now you can connect the generated framework to your iOS project as a dependency. There are multiple ways to set it up and automate the process, choose the method that suits you best:

Choose iOS integration method

What's next

Last modified: 12 December 2024