08 · Interop with C/Objective-C¶
Swift was designed to interoperate with C and Objective-C from day one —
most of Apple's platform frameworks are still Objective-C or C underneath.
This module covers calling C functions directly, working with raw
pointers, and the Objective-C runtime features (NSObject, @objc, KVC)
that Swift code can still opt into.
Calling C functions directly¶
C standard library and system functions are available in Swift without any
wrapping — Foundation (and the platform SDK generally) exposes them as
plain Swift function calls:
import Foundation
let random = arc4random_uniform(100)
print("Random C function call returned a value in [0,100):", random < 100)
Output:
arc4random_uniform is a C function from <stdlib.h> — Swift calls it
exactly as written in C, no bridging layer needed, because the Swift
toolchain auto-generates a Swift-callable interface from the system's C
headers (a Clang importer, working the opposite direction from a manual
bridging header).
C strings¶
C represents strings as char * (null-terminated byte buffers); Swift
bridges these through UnsafeMutablePointer<CChar>/UnsafePointer<CChar>
and String(cString:):
let cString = strdup("hello from C")
if let cString {
let swiftString = String(cString: cString)
print("Bridged C string:", swiftString)
free(cString) // strdup allocates with malloc; Swift won't free it for you
}
Output:
strdup allocates memory with C's malloc — Swift's ARC has no idea this
pointer exists, so it will never be freed automatically; the explicit
free(cString) is not optional cleanup, it's required to avoid a leak.
Working with raw buffers¶
withUnsafeBufferPointer gives temporary, safe-scoped access to a Swift
array's contiguous storage as a C-style pointer + count, useful for calling
C APIs that expect exactly that shape:
func sumBuffer(_ pointer: UnsafePointer<Int32>, count: Int) -> Int32 {
var total: Int32 = 0
for i in 0..<count {
total += pointer[i]
}
return total
}
let values: [Int32] = [1, 2, 3, 4, 5]
let total = values.withUnsafeBufferPointer { buffer -> Int32 in
sumBuffer(buffer.baseAddress!, count: buffer.count)
}
print("Sum via C-style pointer access:", total)
Output:
The pointer handed to the closure is only valid for the duration of that
closure call — storing it anywhere and using it after withUnsafeBufferPointer
returns is undefined behavior, since the array is free to move or
deallocate that storage afterward.
Objective-C interop: NSObject, @objc, dynamic dispatch¶
A Swift class that needs to participate in Objective-C runtime features
(KVC/KVO, target-action, NSNotificationCenter selectors) subclasses
NSObject and marks members @objc:
import ObjectiveC
class Greeter: NSObject {
@objc dynamic var name: String = "World"
@objc func greet() -> String {
"Hello, \(name)!"
}
}
let greeter = Greeter()
greeter.name = "Swift"
print(greeter.greet())
Output:
dynamic on name forces Objective-C-style dynamic dispatch (a message
send) for that property, which is what allows runtime features like KVO
to intercept reads and writes — a plain Swift var (even @objc, without
dynamic) uses Swift's normal static/vtable dispatch, which KVO can't hook
into.
NSString bridging¶
String and NSString bridge transparently in both directions — the same
underlying storage, exposed through whichever API you're calling:
let nsString: NSString = "Objective-C string"
let swiftFromNS: String = nsString as String
print("Bridged NSString:", swiftFromNS)
print("Uppercase via NSString API:", nsString.uppercased)
Output:
Key-Value Coding (KVC)¶
value(forKey:) is a purely Objective-C-runtime feature — it looks up a
property by its string name at runtime rather than through Swift's
compile-time member access, which is why it requires @objc/NSObject:
Output:
Swift-specific traps¶
@objcalone doesn't enable KVO — you needdynamictoo. A@objc varwithoutdynamicis still statically dispatched by the Swift compiler for calls it can see at compile time, which bypasses the message-send mechanism KVO relies on to intercept changes.- Memory from C allocators (
malloc,strdup) is never managed by ARC — everymalloc/strdup/similar needs an explicit matchingfree; forgetting one is a genuine leak, not a Swift-caught bug. Unsafe*Pointertypes provide zero bounds checking — indexing past the buffer's actual length (as insumBufferabove, ifcountwere wrong) is undefined behavior, potentially reading or corrupting unrelated memory, not a clean crash.value(forKey:)returnsAny?, so theas? Stringcast can fail silently (yieldingnil) if the property name is misspelled or the actual type doesn't match — unlike a compile-time-checked Swift property access, a typo here is only caught at runtime, if at all.
Cheat sheet¶
| Need | Approach |
|---|---|
| Call a C function | Just call it — the Clang importer exposes it directly |
| Bridge a C string | String(cString:), and free() if it was heap-allocated |
| Pass a Swift array to a C-style API | array.withUnsafeBufferPointer { ... } |
| Enable KVO/KVC on a property | @objc dynamic var on an NSObject subclass |
Bridge String ↔ NSString |
as String / as NSString, transparently |
How It Actually Works¶
- Swift/C interop works because Swift's calling convention is largely
compatible with C's at the ABI level for simple types — a C function
imported via a bridging header or module map is called almost directly, with
the compiler generating thin marshaling code only where representations
differ (e.g. Swift's
Stringmust be converted to/from a Cchar*via explicit UTF-8 encoding/decoding, since Swift'sStringis not null-terminated bytes internally). - Objective-C interop relies on the shared Objective-C runtime. A Swift
class marked
@objc/inheriting fromNSObjectgets an Objective-C-compatible class structure (isa pointer, method list) generated alongside its normal Swift vtable — this dual representation is what lets Objective-C code call Swift methods viaobjc_msgSend(Objective-C's dynamic, selector-based dispatch, distinct from Swift's own vtable/witness-table dispatch) while Swift code calling the same method uses the faster, direct Swift-native path. - Bridging value types (
NSString↔String,NSArray↔Array) is not always a real copy — Swift'sStringandArrayimplement "bridging" interfaces so that, in many cases, anNSString/NSArraycan be wrapped rather than deep-copied, deferring an actual copy until (or unless) it's genuinely needed, following the same copy-on-write philosophy used elsewhere in Swift's standard library. - Manual memory management at the boundary (
Unmanaged<T>,withUnsafePointer,UnsafeMutablePointer) exists specifically because ARC's automatic retain/release insertion only understands Swift-managed references — when a C API expects to own a pointer itself (e.g. a callback context pointer), you must explicitly opt an object out of ARC's automatic counting (Unmanaged.passRetained/.takeRetainedValue) and take responsibility for balancing the retain count yourself, exactly at the seam where ARC's compile-time analysis can no longer see how the reference is used.
Exercise¶
Write a small C-interop demo that uses UnsafeMutablePointer<Int32> (via
UnsafeMutablePointer<Int32>.allocate(capacity:)) to manually allocate a
buffer of 10 integers, fill it with squares (0, 1, 4, 9, ...), read them
back into a Swift [Int32], and then explicitly deallocate() the buffer.
Then write an NSObject subclass Counter with an @objc dynamic var
count: Int = 0 and a method that increments it; use addObserver/KVO (or
value(forKey:) before and after incrementing) to demonstrate the runtime
can observe the property change without any Swift-level callback wired up
by you directly.