Categories
Kotlin Kotlin Multiplatform MacOS

Kotlin native interoperability with Swift/Objective-C.

Under the Kotlin Native umbrella lie many platforms, like Linux, Windows, or even the Android NDK. Of course it is also available for Apple platforms, which run on Objective-C. How is this interoperability be implemented?

Call Swift/Objective-C functions from Kotlin

There are 2 options to call native functions from Kotlin:

  • Create a Kotlin function with a callback from Objective-C.
  • Write native code in Kotlin. Optionally use the expect/actual system.

For the first case, the native code will be written in the Xcode project in Swift/Objective-C, and returned to Kotlin via an object or interface callback. Projects who already have existing Objective-C code bases can prefer this method. Because conversion would take a considerable effort.

Whereas for the second case, the code will be written in pure Kotlin and in IntelliJ. This sounds great for developers familiar with the Jetbrains platform. Could there be any drawbacks for using this method?

Get the MacBook serial number

For iOS, there is a convenient method to access the unique id of the device.

let deviceId = UIDevice.current.identifierForVendor?.uuidString

Whereas in MacOS, there is no such method. And consequently it needs to be queried via a complicated C interface. While there is an Objective-C implementation example available and it could be queried via an interface to Xcode, surely converting this code to Kotlin would have the benefit of not convoluting the Kotlin/Swift interface.

To write this native implementation, there need to be expect/actual Kotlin functions when using a common module, or just a native one when only using a native module.

fun getUniqueUserId(): String {
	TODO()
}

Next, the Objective-C code needs to be converted to Kotlin. Swift code cannot be converted, so its native counterparts always need to be used.

Available MacOS frameworks

All of the MacOS frameworks are available to be imported into Kotlin. Furthermore, any C library can be used when a corresponding bridge is created. For our example, we need to import IOKit and Foundation libraries

#import <IOKit/IOKitLib.h>

>

import platform.IOKit.*
// Foundation needs to be imported separately
import platform.CoreFoundation.*
import platform.Foundation.*

Next, in the Objective-C code, there is a call to a C function that returns an int and a function that retrieves the MacBooks serial number

io_service_t platformExpert = IOServiceGetMatchingService(
	kIOMasterPortDefault,
  IOServiceMatching("IOPlatformExpertDevice")
);

CFStringRef serialNumberAsCFString = NULL;

if (platformExpert) {
    serialNumberAsCFString = IORegistryEntryCreateCFProperty(
       platformExpert,
       CFSTR(kIOPlatformSerialNumberKey),
       kCFAllocatorDefault,
       0
    );
    
    IOObjectRelease(platformExpert);
}

Consequently the question arises: how to write this code in Kotlin?

New Language: Kotlin C

The first IOServiceGetMatchingService can be converted easily, since it is just a function invocation. It is even converted automatically when pasted into IntelliJ. io_service_t is in that case converted to a generic val.

val platformExpert = IOServiceGetMatchingService(
    kIOMasterPortDefault,
    IOServiceMatching("IOPlatformExpertDevice")
)

On the contrary, the compiler completely fails converting the code regarding CFStringRefs:

Therefore, the developer needs to use Kotlin constructs to represent the CoreFoundation counterparts. This is not obvious at all, and there are no tutorials on how to use them. The developer needs to investigate Kotlin/C mappings or search relevant Objective-C examples.

For our problem, the constant kIOPlatformSerialNumberKey is a String, but the function expects a CFStringRef.

How to convert a Kotlin String to a CFStringRef?

When looking at the CreateNSStringFromKString method, it is noted that Kotlin String can be cast to an NSString with a plain Kotlin cast.

NSString could then be cast to CFStringRef with the __bridge parameter in Objective-C. Despite this apparent convenience, there is no __bridge cast in Kotlin. The developer needs to discover the CFBridgingRetain method as an alternative cast option.

val serialNumberKey = CFBridgingRetain(kIOPlatformSerialNumberKey as NSString) as CFStringRef

The returned value from IORegistryEntryCreateCFProperty method can be cast from CFTypeRef into CFStringRef. Additionally, it needs to be converted to a Kotlin String for our method to return.

How to convert CFStringRef to Kotlin String?

Despite there being a toll-free cast between CFStringRef and NSString, there is still no __bridge parameter in Kotlin.

To overcome this, the CFBridgingRelease function can be used for the final conversion.

val kotlinString = CFBridgingRelease(serialNumberAsCFString) as String

Cleanup

All retained and created objects need to be released before returning from the native function:

CFRelease(serialNumberKey)
CFRelease(serialNumberAsCFString)
IOObjectRelease(platformExpert)

Conclusion

To access Objective-C functions from Kotlin, considerable effort needs to be given to converting previously written snippets. The developer needs to write C in Kotlin, which almost feels like learning a new language.

Positives

  • Can use many of the Kotlin programming language benefits.
  • All of the Objective-C code is in the Kotlin project, not convoluting the Xcode one.
  • All of the Cocoa framework headers have a bridge variant in Kotlin.

Negatives

  • Writing C in Kotlin feels like using a new language. Additionally without any information about how to use C to Kotlin conversion methods, like CFStringRef to Kotlin string.
  • There are often casts required to convert Objective-C variables to Kotlin.
  • Debugging in IntelliJ is buggy, which causes resolving to just printing out the test data.
  • ARC is not available in Kotlin. CFBridgingRetain and CFRelease need to be used.

Leave a Reply