Platform API Layers on the Example of the String Type

This article describes different layers of platform APIs on Apple platforms.

We will use the string data type as an example, because string data types exist in three platform API layers:

Core Foundation

Core Foundation is a plain C framework.

Note: “Framework” in this context means “unit of shipment” – basically a folder structure that bundles a library and corresponding collection of header files. Find out more about Frameworks in the context of SDKs below.

Core Foundation’s string type looks like this (see CoreFoundation.framework/Headers/CFBase.h):

typedef const struct CF_BRIDGED_TYPE(NSString) __CFString * CFStringRef;

The public-facing type is CFStringRef, a reference (pointer) to an opaque __CFString type (the double underscore in the name hinting that this is a private type). Here’s some documentation pointers to Core Foundation Design Concepts and Core Foundation’s use of Opaque Types and corresponding handle types. Note that there’s also a hint in there that Core Foundation’s CFStringRef is somehow related to Foundation’s NSString. We’ll come to that.

Now, to create a Core Foundation string, you can use one of the CFStringCreate C functions, for example CFStringCreateWithBytes:

CFStringRef CFStringCreateWithBytes(CFAllocatorRef alloc,
                                    const UInt8 *bytes,
                                    CFIndex numBytes,
                                    CFStringEncoding encoding,
                                    Boolean isExternalRepresentation)

This C function is mapped to Swift like so:

func CFStringCreateWithBytes(_ alloc: CFAllocator!,
                             _ bytes: UnsafePointer<UInt8>!,
                             _ numBytes: CFIndex,
                             _ encoding: CFStringEncoding,
                             _ isExternalRepresentation: Bool) -> CFString!

Note that the CFStringRef C (pointer) type is exposed to Swift code as class CFString – a reference type. So be aware that Core Foundation strings behave different from Swift strings in terms of reference / value semantics.

Usage of this API would then look like this:

let bytes: [UInt8] = [104, 101, 108, 108, 111] // "hello" in ASCII
let cfstring = CFStringCreateWithBytes(nil,
                                       bytes,
                                       bytes.count,
                                       UInt32(kCFStringEncodingASCII),
                                       false)

For Core Foundation ownership rules, see this document. In essence, when you call a Create() function, you own the returned object, and it’s your your responsibility to relinquish ownership when you no longer need it.

In a non-ARC project (using Objective-C or C/C++), you would have to call CFRelease(cfstring) to release the allocated object. In an ARC project, however, an explicit call to CFRelease(cfstring) is prohibited by the compiler:

‘CFRelease’ is unavailable: Core Foundation objects are automatically memory managed

To release the string under ARC, what you do is nil out the strong reference (needs to be declared var cfstring: CFString? for this), or simply let the strong reference go out of scope.

Interlude: Frameworks and SDKs

Let’s take a closer look at frameworks in the context of SDKs. If you add a framework to the “Link Binary with Libraries” build phase of a target in Xcode, you are actually only adding a stub for the actual target platform framework.

For example, adding CoreFoundation.framework to an iOS framework or app target, the stub will be something like

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/CoreFoundation.framework

Inside this CoreFoundation.framework⁩ SDK stub, you will find all the header files as well as a CoreFoundation.tbd file, a “text-based .dylib stub” that provides a compact, textual representation of the library for use in SDKs. Note that there’s no actual library binary in this framework bundle.

The text-based .dylib stub contains information such as the target platform, supported architectures, list of exported symbols, and the install location of the framework on the target platform. For CoreFoundation.framework, this will be

System/Library/Frameworks/CoreFoundation.framework

This framework bundle then contains the actual shared library file (here showing the contents of the macOS CoreFoundation framework):

$ file /System/Library/Frameworks/CoreFoundation.framework/Versions/Current/CoreFoundation 
/System/Library/Frameworks/CoreFoundation.framework/Versions/Current/CoreFoundation: Mach-O universal binary with 3 architectures: [x86_64:Mach-O 64-bit dynamically linked shared library x86_64] [x86_64h]
/System/Library/Frameworks/CoreFoundation.framework/Versions/Current/CoreFoundation (for architecture x86_64):	Mach-O 64-bit dynamically linked shared library x86_64
/System/Library/Frameworks/CoreFoundation.framework/Versions/Current/CoreFoundation (for architecture i386):	Mach-O dynamically linked shared library i386
/System/Library/Frameworks/CoreFoundation.framework/Versions/Current/CoreFoundation (for architecture x86_64h):	Mach-O 64-bit dynamically linked shared library x86_64h

Foundation

Foundation is Core Foundation’s object-oriented big brother. It’s an Objective-C framework that provides a base layer of functionality for apps and frameworks. It offers a much broader set of functionality than Core Foundation.

Foundation’s string type is NSString, an Objective-C class that translates to Swift as class NSString (again, a reference type).

NSString belongs to the toll-free bridged types, a set of data types that are interchangeable between Core Foundation and Foundation. Simplifying a lot here, this is possible because Core Foundation strings basically have the same memory layout than their Foundation counterparts. What this means for clients is that CFStringRef and NSString can be mixed and matched without any performance penalty.

Sticking with our example of creating a string from some ASCII bytes:

let bytes: [UInt8] = [104, 101, 108, 108, 111] // "hello" in ASCII
let nsstring = NSString(bytes: bytes,
                        length: bytes.count,
                        encoding: String.Encoding.ascii.rawValue)

Swift Standard Library

Swift’s String type is provided by the Swift Standard Library.

Swift’s String is a value type (a struct), so at least conceptually, “assignment creates a copy”. Under the hood, however, strings use a copy-on-write strategy, so you don’t need to be worried about passing large strings around.

Again using our example of creating a string from some ASCII bytes:

let bytes: [CChar] = [104, 101, 108, 108, 111] // "hello" in ASCII
let string = String(cString: bytes)

Swift strings can be converted to Foundation strings by way of the type-cast operator (as):

string as NSString

Be aware that Swift strings and Foundation strings are not the same, though.

All Strings Are Not Equal

A Foundation string is a collection of UTF-16 code units, whereas a Swift string is a collection of Characters, each representing an extended grapheme cluster of one or more Unicode scalars. You may run into unexpected differences in behavior.

For example,

let nsstring = NSString("À")
print("nsstring: \(nsstring), length: \(nsstring.length)")
let string = "À"
print("string: \(string), length: \(string.count)")

will print

nsstring: À, length: 2
string: À, length: 1

because the NSString in this case is composed of the UTF-16 code units

whereas the Swift String contains a single Swift Character representing an extended grapheme cluster.

References