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’s
- Swift Standard Library’s
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
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
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 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
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
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 (
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.
let nsstring = NSString("À") print("nsstring: \(nsstring), length: \(nsstring.length)") let string = "À" print("string: \(string), length: \(string.count)")
nsstring: À, length: 2 string: À, length: 1
NSString in this case is composed of the UTF-16 code units
U+0041 LATIN CAPITAL LETTER Aand
U+0300 COMBINING GRAVE ACCENT
whereas the Swift
String contains a single Swift Character representing an extended grapheme cluster.
- “Strings in Swift 1”, by Ole Begemann
- “Strings in Swift 3”, by Airspeed Velocity and Ole Begemann
- “Strings in Swift 4”, by Ole Begemann
- “UTF-8 String”, by Michael Ilseman
- “Piercing the String Veil”, by Michael Ilseman
- “String Processing For Swift 4”, by Dave Abrahams, Ben Cohen
- “Static and Dynamic Libraries and Frameworks in iOS”, by Vadim Bulavin
- “Bridge”, by ridiculousfish