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

Here is a good summary circa 2006 of the history and inner workings of toll-free bridging from someone who was involved with its design and implementation.

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.