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
CFStringRef
- Foundation’s
NSString
- Swift Standard Library’s
String
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 Character
s, 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
U+0041 LATIN CAPITAL LETTER A
andU+0300 COMBINING GRAVE ACCENT
whereas the Swift String
contains a single Swift Character representing an extended grapheme cluster.
References
- “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