Toll-free Bridging in Kotlin Native
Toll-free bridging is a concept in macOS and iOS development that allows developers to use data types interchangeably between the Core Foundation and Foundation Kit frameworks.
About Core Foundation & Foundation
Core Foundation belongs to the open-source framework created by Apple as a foundation to build libraries and applications for Mac OS. It is C-based and inspired by Foundation.
Foundation, on the other hand, was created by NeXT for NeXTSTEP for similar purposes, and it uses the Objective-C programming language. Allegedly, it was created for their EOF product.
Tools from the former kit typically start with CF
for Core Foundation in their names, while tools from Foundation start with NS
for NeXTSTEP.
Toll-free Bridging
Mac OS merged with NeXTSTEP to create Mac OS X and the need to merge the Mac Toolbox with OpenStep gave forth toll-free bridging of types between Core Foundation and Foundation. That is, conversion of equivalent types between both frameworks without the need for checking types and converting between them. There is no cost to cross the Core Foundation to Foundation and Foundation to Core Foundation bridge.
Toll-free bridging is used by Cocoa to interact with macOS APIs.
When using tools from Foundation, you are able to use CF
types in places where NS
types are expected. Similarly, when using Core Foundation, you’re able to pass in NS
objects to APIs that expect CF
types.
Click here to see how Core Foundation types map to Foundation classes below 👇
Core Foundation type | Foundation class |
---|---|
CFArrayRef |
NSArray |
CFAttributedStringRef |
NSAttributedString |
CFBooleanRef |
NSNumber |
CFCalendarRef |
NSCalendar |
CFCharacterSetRef |
NSCharacterSet |
CFDataRef |
NSData |
CFDateRef |
NSDate |
CFDictionaryRef |
NSDictionary |
CFErrorRef |
NSError |
CFLocaleRef |
NSLocale |
CFMutableArrayRef |
NSMutableArray |
CFMutableAttributedStringRef |
NSMutableAttributedString |
CFMutableCharacterSetRef |
NSMutableCharacterSet |
CFMutableDataRef |
NSMutableData |
CFMutableDictionaryRef |
NSMutableDictionary |
CFMutableSetRef |
NSMutableSet |
CFMutableStringRef |
NSMutableString |
CFNullRef |
NSNull |
CFNumberRef |
NSNumber |
CFReadStreamRef |
NSInputStream |
CFRunLoopTimerRef |
NSTimer |
CFSetRef |
NSSet |
CFStringRef |
NSString |
CFTimeZoneRef |
NSTimeZone |
CFURLRef |
NSURL |
CFWriteStreamRef |
NSOutputStream |
Kotlin Native
Kotlin Native uses the Kotlin programming language as a frontend and the LLVM compiler as a backend instead of targeting the JVM for compilation. Kotlin Native compiles down to native code for multiple platforms including Windows, Linux, macOS, iOS and Android.
Interoperability
Kotlin Native provides built-in C and Objective-C interoperability. Kotlin Native makes it easy to import and export C and Objective-C compatible ABIs.
As a result, Kotlin Native can be used for macOS and iOS development using both platforms’ native libraries.
Click here to see how Kotlin Native types map to Swift and Objective-C types below 👇
Kotlin | Swift | Objective-C |
---|---|---|
class |
class |
@interface |
interface |
protocol |
@protocol |
constructor /create |
Initializer | Initializer |
Property | Property | Property |
Method | Method | Method |
enum class |
class |
@interface |
suspend-> |
completionHandler: /async |
completionHandler: |
@Throws fun |
throws |
error:(NSError**)error |
Extension | Extension | Category member |
companion member <- |
Class method or property | Class method or property |
nan |
nil |
nil |
Singleton |
shared or companion property |
shared or companion property |
Primitive type | Primitive type / NSNumber |
nan |
Unit return type |
Void |
void |
String |
String |
NSString |
String |
NSMutableString |
NSMutableString |
List |
Array |
NSArray |
MutableList |
NSMutableArray |
NSMutableArray |
Set |
Set |
NSSet |
MutableSet |
NSMutableSet |
NSMutableSet |
Map |
Dictionary |
NSDictionary |
MutableMap |
NSMutableDictionary |
NSMutableDictionary |
Function type | Function type | Block pointer type |
Inline classes | Unsupported | Unsupported |
However, the language doesn’t abstract away the need to understand the underlying systems and APIs for the platforms you’re targeting. Kotlin Native does nothing to change the need to manually invoke toll-free bridging when working with Core Foundation and Foundation libraries. You still need to cross the bridge from C to Objective-C/Kotlin and the other way around.
Kotlin Native also doesn’t abstract away the need for manual memory management. It’s up to you to to know who owns your types and when they should be freed from memory.
How to use it
From Objective-C and C
You can use toll-free bridging just by casting NS
types as CF
types and vice versa. If you do that, however, you have to remember to release memory manually.
Here’s an example from Apple’s documentation:
NSLocale *gbNSLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_GB"];
CFLocaleRef gbCFLocale = (CFLocaleRef) gbNSLocale;
In this example, we create a Foundation object, a NSLocale
, and then cast it to its Core Foundation equivalent type.
Below, we then use that Core Foundation type to call a Core Foundation function, then we cast that object as a NSString
pointer to use it with a Foundation function, NSLog()
:
CFStringRef cfIdentifier = CFLocaleGetIdentifier (gbCFLocale);
NSLog(@"cfIdentifier: %@", (NSString *)cfIdentifier);
Finally, we have to release the memory used by the CF
type:
CFRelease((CFLocaleRef) gbNSLocale);
Toll-free bridging and ARC
What if you want to hand off ownership of a Core Foundation type to Objective-C’s ARC? And what if you want to hand ownership of an Objective-C object to C?
Core Foundation provides functions like CFBridgingRelease()
to move ownership of CF
types to Objective-C and CFBridgingRetain()
to give ownership of an NS
type to C.
// Bridge to Core Foundation
NSArray *foundationArray = @[@"Objective", @"C", @"Example"];
CFArrayRef coreFoundationArray = CFBridgingRetain(foundationArray);
The last line in the example above moves ownership of the NSArray
to C from ARC, re-casted as an equivalent CFArrayRef
.
Here we take a Core Foundation object and move its ownership to ARC:
CFMutableArrayRef mutableCoreFoundationArray = CFArrayCreateMutableCopy(kCFAllocatorDefault, 0, coreFoundationArray);
CFArrayAppendValue(mutableCoreFoundationArray, CFSTR("C"));
// Bridge to Foundation
NSArray *updatedFoundationArray = CFBridgingRelease(mutableCoreFoundationArray);
From Kotlin Native
Why toll-free bridging is necessary in Kotlin Native
Kotlin Native supports bidirectional compatibility with Objective-C. That means that Kotlin Native can import and export Objective-C libraries natively.
Kotlin Native also has its own memory management model that is unlike C and Objective-C’s ARC. The language uses garbage collection to manage memory instead of reference counting.
According to KN’s developers, integration between Kotlin’s garbage collector is seamless with Objective-C’s ARC. That means once an object crosses into Objective-C land, and ARC would be aware of it, Kotlin is, too.
In contrast, when working with C code and Kotlin Native, manual memory management is still needed for C objects.
With that in mind, we can let Kotlin’s garbage collector handle our NS
types and be vigilant about memory management when using Core Foundation types.
When we need to cross the Core Foundation to Foundation bridge, we don’t have to release memory manually. That’s because once crossed to the Objective-C side of things, ARC owns the object. Kotlin Native integrates with ARC and handles the memory for us.
When we need to cross the Foundation to Core Foundation bridge, neither ARC nor Kotlin Native’s garbage collector are managing our objects’ memory.
Toll-free bridging in Kotlin Native
While Kotlin Native supports Objective-C interoperability, it does not support simply casting CF
types as NS
types, and vice versa, in order to use them.
If you want to use a Foundation type with Core Foundation, or the other way around, you will need to manually cross that bridge.
Working with Core Foundation types
Core Foundation types derive from CFTypeRef
, in both C and Kotlin, and in Kotlin, CFTypeRef
is a type alias of COpaquePointer
, the supertype for other pointer types, which maps to void*
and is a type alias of CPointer<T>
:
public typealias COpaquePointer = CPointer<out CPointed>
public typealias CFTypeRef = kotlinx.cinterop.COpaquePointer
Here’s what a CPointer<T>
looks like:
public abstract class CPointed(rawPtr: NativePtr) : NativePointed(rawPtr.toNonNull())
public class CPointer<T : CPointed> internal constructor(@PublishedApi internal val value: NonNullNativePtr) : CValuesRef<T>() { /* */ }
CPointer
is a feature of Kotlin Native’s C interop, it isn’t unique to the platform, Core Framework, or Objective-C. Kotlin uses it for pointers and arrays in C.
If we want to use Core Foundation types in Kotlin, especially to cast them to types that Kotlin integrates like Foundation types, we’ll need to manually bridge it into Kotlin with CFBridgingRelease()
.
Here’s a naive example:
import platform.CoreFoundation.*
import platform.CoreGraphics.*
import platform.Foundation.*
fun getWindows(): CFArrayRef? =
CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID)
fun main() {
val cf = getWindows()
println(cf)
println(cf as? NSObject)
// -> CPointer(raw=0x7f7e5ec04080)
// -> CPointer(raw=0x7f7e5ec04080)
val released = CFBridgingRelease(cf)
println(released)
println(released!!::class.qualifiedName)
// -> [{kCGWindowLayer=0, kCGWindowMemoryUsage=186720, ...}]
// -> kotlin.native.internal.NSMutableArrayAsKMutableList
val asNsObject = released as? NSObject
println(asNsObject!!::class.qualifiedName)
// -> kotlin.native.internal.NSMutableArrayAsKMutableList
val asNative = released as? MutableList<Map<String, Any>>
println(asNative!!::class.qualifiedName)
// -> kotlin.native.internal.NSMutableArrayAsKMutableList
}
Above, we call the Core Foundation function CGWindowListCopyWindowInfo()
that returns a CFArrayRef?
, which would map to a NSArray
when using toll-free bridging from Objective-C. Using Objective-C, a simple cast is all we would need, unless we want to transfer ownership, but in Kotlin, we will need to use CFBridgingRelease()
to do our casting and ownership transfer. Then, casting with as
tells the compiler to treat the underlying data as a new type without changing the data itself. Casting released
to collections successfully compiles and runs at runtime.
To use the released
value in main()
from Kotlin or Foundation APIs, we don’t have to do anything. Looking at its class name, we see that it is a kotlin.native.internal.NSMutableArrayAsKMutableList
.
Here’s what that looks like in Kotlin:
public actual abstract class AbstractMutableList<E> protected actual constructor() : AbstractMutableCollection<E>(), MutableList<E> { /* */ }
internal class NSMutableArrayAsKMutableList : AbstractMutableList<Any?>(), RandomAccess, ObjCObjectWrapper { /* */ }
NSMutableArrayAsKMutableList
is an AbstractMutableList
from Kotlin’s standard library. That is the base class for Kotlin Native’s MutableList
.
As it stands, NSMutableArrayAsKMutableList
is equivalent to MutableList
and casts to it both compile and succeed at runtime.
With that said, there is no reason you need to manually cast any Foundation type to a Kotlin type, as Kotlin will handle the interop for you. That means you can use Kotlin types in place of NS
types without explicit casts:
val released: MutableList<Map<String, Any>> = CFBridgingRelease(cf)
We can now treat released
like any type from Kotlin’s standard library thanks to the language’s built-in Objective-C support.
Memory management
Creating C objects uses native memory and not the Kotlin application’s heap memory. To automatically handle native memory, you can use memScoped
blocks:
import kotlinx.cinterop.memScoped
fun main() = memScoped {
val cf = getWindows()
println(cf)
/* */
}
memScoped
blocks allocate memory via an arena and automatically free it when done.
Writing idiomatic extension functions
Let’s clean up our example into some extension functions that have better ergonomics.
fun <T> CFRefType.release(): T? = CFBridgingRelease(this) as? T
fun <T> CFRefType.cast(): T? = memScoped { release() as? T }
Now, we can do things like:
typealias Window = Map<String, Any>
typealias Windows = List<Window>
fun getWindows(): Windows? = memScoped {
CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID)
?.cast<Windows>()
?.map { window -> window.filterKeys { /* */ } }
?.filter { /* */ }
}
We can use the cast<T>()
extension function on CFRefType
to easily cast them to Kotlin types.
If we want to go in the other direction and cast a Kotlin type to a Core Foundation type, we can do this:
fun Any.retain(): CFTypeRef? = CFBridgingRetain(this)
fun <T: CFTypeRef> Any.cast(): T? = memScoped { retain() as? T }
fun asCoreFoundation(): CFRefType? = getWindows().cast()
Similarly, we can use the cast<T>()
extension function on Kotlin objects to easily cast and bridge them to Core Foundation types.
Conclusion
While Kotlin Native, and its related platforms like Kotlin Multiplatform, abstract away a lot of underlying system details, if you want to target multiple platforms with Kotlin, you still need to understand what Kotlin abstracts over.
For macOS development, that means understanding how Kotlin Native interops with Objective-C and C, how Objective-C and C interact with each other, and how Core Foundation and Foundation kit interact with one another.
Sometimes it also means being aware of memory management outside of Kotlin’s garbage collector, and how you can allocate and free memory from C without leaking memory.
With that said, Kotlin Native makes it easy to also abstract over those implementation details with using features like extension functions, generics and manual memory management.