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 CFStringRef
s:
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
andCFRelease
need to be used.
Calling Objective-C from Kotlin can require many casts. Consider converting a CFStringRef to Kotlin String:
— Tõnis Tiganik (@tonisives) August 3, 2021
CFBridgingRelease(serialNumberAsCFString) as String#Kotlin #KotlinTipshttps://t.co/H53QWBxHPH