Language Atlas (mini-courses for more languages) Module 7 – Objective-C for maintainers and Swift developers
Objective-C ARC, blocks and Swift interop
Find retain cycles under ARC, write blocks with __block and a weak self, annotate headers for Swift, and catch format-string and other C hazards.
What you will learn
- Explain strong, weak and copy references under ARC and find the retain cycle that keeps objects alive
- Write blocks that capture values, change a __block variable and use a weak self to break a cycle
- Annotate a header with nullability and lightweight generics so that Swift imports optionals and typed arrays
- Find format-string bugs and other C-level hazards in Objective-C code and build with the flags that catch them
Before you start
What you’ll learn · Before you start
What you will learn
- Explain strong, weak and copy references under ARC and find the retain cycle that keeps objects alive
- Write blocks that capture values, change a __block variable and use a weak self to break a cycle
- Annotate a header with nullability and lightweight generics so that Swift imports optionals and typed arrays
- Find format-string bugs and other C-level hazards in Objective-C code and build with the flags that catch them
Before you start
On this page
cycle.m, a recorded program of this lesson, creates seven objects that it counts, and four of them are still alive
when it ends: each is held, through a property or a block, in a ring of references that
ARC (automatic reference counting): Objective-C memory management in which the compiler inserts the retain and release calls, so that an object lives while a strong reference to it exists and is freed after the last one goes. It does not collect reference cycles. never breaks. clang warned about one
of those rings and said nothing about another that leaks in exactly the same way.
This lesson covers the parts of Objective-C where Swift habits help least. You will follow ARC as it frees objects, find and break a retain cycle, write blocks and see exactly what they capture, annotate a header so that Swift imports optionals and typed arrays instead of crashing, and spot the C hazards Objective-C keeps: format strings from outside the program, formats too narrow for their values, and integer overflow.
ARC frees an object when its last strong reference goes
lifetimes.m creates five tickets, each of which prints a line when it is created and when it is freed, and lets go of
them in different ways. Apple’s ARC release notes describe the mechanism as code the compiler adds, so that each
object lives for as long as something needs it and no longer. Clang’s specification makes it exact: storing an object in a
strong reference: A reference that keeps its object alive: under ARC an object lives while at least one strong variable, property or captured block points at it. Object pointers are strong unless declared otherwise. variable or property retains it, the value it replaced is released, and an
object is freed when the last strong reference to it goes. Strong is the default for object pointers, so plain
Ticket *a is strong.
// lifetimes.m: when ARC frees an object, and what a weak reference holds afterwards. Every ticket says when it is
// created and when it is freed.
#import <Foundation/Foundation.h>
#include <stdio.h>
@interface Ticket : NSObject
@property (nonatomic, copy) NSString *name;
- (instancetype)initWithName:(NSString *)name;
@end
@implementation Ticket
- (instancetype)initWithName:(NSString *)name {
self = [super init];
if (self) {
_name = [name copy];
printf(" + %s created\n", name.UTF8String);
}
return self;
}
- (void)dealloc {
printf(" - %s freed\n", self.name.UTF8String);
}
@end
int main(void) {
@autoreleasepool {
printf("1. one strong reference, then none\n");
Ticket *first = [[Ticket alloc] initWithName:@"T1"];
printf(" first holds %s\n", first.name.UTF8String);
first = nil;
printf("2. two strong references to one ticket\n");
Ticket *a = [[Ticket alloc] initWithName:@"T2"];
Ticket *b = a;
a = nil;
printf(" a is nil, b still holds %s\n", b.name.UTF8String);
b = nil;
printf("3. a weak reference\n");
Ticket *strong = [[Ticket alloc] initWithName:@"T3"];
__weak Ticket *weak = strong;
printf(" weak sees %s\n", weak.name.UTF8String);
strong = nil;
printf(" weak is now %s\n", weak == nil ? "nil" : "still an object");
printf("4. a new ticket stored only in a weak variable\n");
__weak Ticket *lost = [[Ticket alloc] initWithName:@"T4"];
printf(" lost is %s\n", lost == nil ? "nil" : "an object");
printf("5. a strong variable at the end of its scope\n");
Ticket *last = [[Ticket alloc] initWithName:@"T5"];
printf(" last holds %s\n", last.name.UTF8String);
}
printf("after the autorelease pool\n");
return 0;
} from session import run
# When ARC frees an object: build lifetimes.m, read its one warning, and run it.
run("""
cd arc
clang -fobjc-arc -Wall -Wextra -Wformat-security -framework Foundation lifetimes.m -o lifetimes
./lifetimes
""") Output
$ cd arc
$ clang -fobjc-arc -Wall -Wextra -Wformat-security -framework Foundation lifetimes.m -o lifetimes
lifetimes.m:44:24: warning: assigning retained object to weak variable; object will be released after assignment [-Warc-unsafe-retained-assign]
44 | __weak Ticket *lost = [[Ticket alloc] initWithName:@"T4"];
| ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.
$ ./lifetimes
1. one strong reference, then none
+ T1 created
first holds T1
- T1 freed
2. two strong references to one ticket
+ T2 created
a is nil, b still holds T2
- T2 freed
3. a weak reference
+ T3 created
weak sees T3
- T3 freed
weak is now nil
4. a new ticket stored only in a weak variable
+ T4 created
- T4 freed
lost is nil
5. a strong variable at the end of its scope
+ T5 created
last holds T5
- T5 freed
after the autorelease pool
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lifetimes_session.py
Every - … freed line sits exactly where the last strong reference went. T2 survived a = nil because b still held
it, and went at b = nil. A weak reference: A reference, declared __weak or with the weak attribute, that does not keep its object alive. ARC sets it to nil when the object is freed, so it never points at freed memory. reference does not count: T3 was freed as soon as
strong let go, and weak then read nil. Step through those two cases with the counts beside them:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- A new ticket. a = [[Ticket alloc] initWithName:@"T2"]. The new object has one strong reference, a, and the run printed + T2 created.
- A second strong reference. b = a. ARC retains the object for b, so two strong references now keep it alive.
- a lets go. a = nil releases one reference. One is left, so the ticket lives on, and the run printed a is nil, b still holds T2.
- The last strong reference goes. b = nil releases the last one. The count reaches 0, ARC frees the ticket, and its dealloc printed - T2 freed.
- A ticket with a weak reference. Case 3: strong holds a new ticket, and weak points at it too. A weak reference does not count, so the ticket still has one strong reference.
- Reading through weak. While the ticket lives, weak reads it like any reference: the run printed weak sees T3.
- The strong reference goes. strong = nil releases the only strong reference. ARC frees the ticket, and the run printed - T3 freed.
- weak is nil. Clang's specification says a weak reference to an object being freed is set to nil, so weak never points at freed memory: the run printed weak is now nil.
The two other lines are the edges of the rule. T4 was stored only in a weak variable, so it had no strong reference
from its first moment; clang warned at build time, assigning retained object to weak variable; object will be released after assignment, and the run printed + T4 created and - T4 freed back to back. T5 was freed at the
closing brace of the @autoreleasepool block, where its variable’s scope ended, before after the autorelease pool.
Key idea
Under ARC an object lives exactly as long as at least one strong reference points at it; weak references do not count, and ARC sets them to nil when the object goes.
Checkpoint
Check: When ARC frees an object
A ring of strong references is never freed
A strong reference keeps its object alive, so two objects that hold each other strongly keep each other alive for
ever. Clang’s specification says it plainly: ARC does not provide a cycle collector, and breaking cycles is the
programmer’s job, by hand or with weak references. Apple’s release notes call such a ring a strong reference cycle,
the name for what was earlier known as a retain cycle: A ring of strong references, such as two objects that hold each other, which keeps every object in it alive after the rest of the program lets go. Apple also calls it a strong reference cycle; ARC never collects one.. cycle.m counts its live
objects: each init adds one and each dealloc subtracts one, the dealloc counter a debug build can keep:
// cycle.m: three ways to keep objects alive by accident, each beside its fix. A counter of live objects says how many
// are left at the end of each case.
#import <Foundation/Foundation.h>
#include <stdio.h>
static int live = 0;
@class Order;
@interface Customer : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) Order *order;
@end
@interface Order : NSObject
@property (nonatomic, strong) Customer *buyer; // a strong reference back: the cycle
@property (nonatomic, weak) Customer *weakBuyer; // a weak reference back: the fix
@end
@implementation Customer
- (instancetype)init { if ((self = [super init])) { live++; } return self; }
- (void)dealloc { live--; printf(" - customer %s freed\n", self.name.UTF8String); }
@end
@implementation Order
- (instancetype)init { if ((self = [super init])) { live++; } return self; }
- (void)dealloc { live--; printf(" - order freed\n"); }
@end
@interface Feed : NSObject
@property (nonatomic, copy) NSString *label;
@property (nonatomic, copy) void (^onUpdate)(void);
@property (nonatomic, strong) NSMutableArray *handlers;
- (void)watch;
- (void)watchWeakly;
- (void)watchThroughAList;
@end
@implementation Feed
- (instancetype)init { if ((self = [super init])) { live++; _handlers = [NSMutableArray array]; } return self; }
- (void)watch {
self.onUpdate = ^{
printf(" update for %s\n", self.label.UTF8String);
};
}
- (void)watchWeakly {
__weak Feed *weakSelf = self;
self.onUpdate = ^{
Feed *strongSelf = weakSelf;
if (strongSelf) {
printf(" update for %s\n", strongSelf.label.UTF8String);
}
};
}
- (void)watchThroughAList {
NSMutableArray *list = self.handlers;
[list addObject:^{
printf(" update for %s\n", self.label.UTF8String);
}];
}
- (void)dealloc { live--; printf(" - feed %s freed\n", self.label.UTF8String); }
@end
int main(void) {
@autoreleasepool {
printf("1. customer and order hold each other strongly\n");
{
Customer *c = [[Customer alloc] init];
c.name = @"Asha";
c.order = [[Order alloc] init];
c.order.buyer = c;
}
printf(" live objects: %d\n", live);
printf("2. the order points back weakly\n");
{
Customer *c = [[Customer alloc] init];
c.name = @"Ravi";
c.order = [[Order alloc] init];
c.order.weakBuyer = c;
}
printf(" live objects: %d\n", live);
printf("3. a block property that uses self\n");
{
Feed *f = [[Feed alloc] init];
f.label = @"news";
[f watch];
f.onUpdate();
}
printf(" live objects: %d\n", live);
printf("4. the block uses a weak self\n");
{
Feed *f = [[Feed alloc] init];
f.label = @"sport";
[f watchWeakly];
f.onUpdate();
}
printf(" live objects: %d\n", live);
printf("5. the block reaches self's array through a local variable\n");
{
Feed *f = [[Feed alloc] init];
f.label = @"weather";
[f watchThroughAList];
}
printf(" live objects: %d\n", live);
}
return 0;
} from session import run
# Three cycles and their fixes: build cycle.m, read which of the cycles clang warned about, and run it.
run("""
cd arc
clang -fobjc-arc -Wall -Wextra -Wformat-security -framework Foundation cycle.m -o cycle
./cycle
""") Output
$ cd arc
$ clang -fobjc-arc -Wall -Wextra -Wformat-security -framework Foundation cycle.m -o cycle
cycle.m:43:39: warning: capturing 'self' strongly in this block is likely to lead to a retain cycle [-Warc-retain-cycles]
43 | printf(" update for %s\n", self.label.UTF8String);
| ^~~~
cycle.m:42:5: note: block will be retained by the captured object
42 | self.onUpdate = ^{
| ^~~~
1 warning generated.
$ ./cycle
1. customer and order hold each other strongly
live objects: 2
2. the order points back weakly
- customer Ravi freed
- order freed
live objects: 2
3. a block property that uses self
update for news
live objects: 3
4. the block uses a weak self
update for sport
- feed sport freed
live objects: 3
5. the block reaches self's array through a local variable
live objects: 4
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cycle_session.py
Case 1 ended with 2 live objects: the customer held its order and the order held the customer, so when the variable
c went at the closing brace, each still had one strong reference, from the other. Case 2 is the same pair with one
change, @property (nonatomic, weak) Customer *weakBuyer, and both were freed: the customer first, because only c
held it, then the order, which the customer had held. The count stayed at 2, the leak of case 1.
The rule that prevents most cycles is about ownership: a parent holds its children strongly, and a child points back to its parent weakly. Choosing the attribute of a property is mostly answering that question, and two more:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Attribute | Keeps the object alive | After the object is freed | If the caller changes the object | Typical use |
|---|---|---|---|---|
| strong Written as: @property (strong) | Good yes: each strong reference counts, so the object lives while this one holds it | Good cannot happen while this property holds it | Poor sees the change: nickname became Asha Rao in the second lesson | Good objects this one owns, such as a customer's order |
| weak Written as: @property (weak) | Mixed no: a weak reference does not count | Good reads nil: ARC sets it, as weak and weakSelf showed | Mixed sees the change: it is the same object | Good a reference back to an owner, or self inside a block |
| copy Written as: @property (copy) | Good yes, its own copy, which is strong | Good its copy lives as long as the property holds it | Good keeps the value it was given: owner stayed Asha | Good NSString, NSArray, NSDictionary and block properties |
| unsafe_unretained Written as: @property (unsafe_unretained) | Poor no: it does not count | Poor still points at the freed memory; reading it is undefined | Mixed sees the change: it is the same object | Poor rare: code from before weak references, where nothing else works |
How the answers decide
- “Does this object own the other one, or does it point back to the object that owns it?” It points back to its owner: rules out strong and copy. A strong reference back to the owner closes a cycle, like the customer and order of case 1, which were never freed.
- “Does this object own the other one, or does it point back to the object that owns it?” It owns the other object: rules out weak and unsafe_unretained. Nothing else keeps an owned object alive, so a reference that does not count would let it be freed at once, like ticket T4.
- “Is the value a string, a collection or a block that the caller could change or reuse later?” Yes, such as an NSMutableString: rules out strong and adds a caveat to weak. A string or collection passed in may be mutable and change later, as nickname did; copy keeps the value it was given.
- “Can the other object be freed while this one still exists?” Yes, it can go first: rules out unsafe_unretained. If the other object can be freed first, unsafe_unretained points at freed memory, while weak reads nil.
In short: strong for what an object owns, weak for a reference back to its owner or anything that may go first,
copy for strings, collections and blocks that a caller might change, and unsafe_unretained almost never, because
it keeps pointing at freed memory.
Checkpoint
Check: Retain cycles and weak references
A block captures values when it is created
A block: A piece of code written ^(arguments) { … } that can be stored and called later, with the variables it uses captured: the Objective-C and C counterpart of a Swift closure. Blocks are Objective-C objects. is a piece of code with the variables it uses, written ^(int price) { … }, the
Objective-C form of a Swift closure. Clang’s Blocks specification fixes what it captures: local variables are copied
into the block as constants at the moment the block literal is evaluated, and a variable marked
__block variable: A local variable declared with __block, which the blocks that use it share with its scope instead of copying, so that a block can change it and the code around it sees the change. is shared between the block and its scope instead, so that both see changes.
Blocks are also Objective-C objects, which can be stored in collections and properties:
// blocks.m: what a block captures and when, a __block variable, a block that keeps an object alive, and blocks
// stored in a collection.
#import <Foundation/Foundation.h>
#include <stdio.h>
@interface Ticket : NSObject
@end
@implementation Ticket
- (void)dealloc { printf(" - ticket freed\n"); }
@end
int main(void) {
@autoreleasepool {
int discount = 10;
int (^priceAfter)(int) = ^(int price) {
return price - discount;
};
discount = 50;
printf("1. priceAfter(200) = %d, with discount now %d\n", priceAfter(200), discount);
__block int sum = 0;
[@[@3, @1, @2] enumerateObjectsUsingBlock:^(NSNumber *mark, NSUInteger index, BOOL *stop) {
(void)index;
(void)stop;
sum += mark.intValue;
}];
printf("2. sum of 3, 1 and 2 with __block: %d\n", sum);
printf("3. a block that captured a ticket\n");
void (^show)(void);
{
Ticket *ticket = [[Ticket alloc] init];
show = ^{
printf(" the block still has %s\n", ticket == nil ? "nothing" : "the ticket");
};
ticket = nil;
printf(" the variable is nil\n");
}
show();
show = nil;
printf(" the block is gone\n");
NSArray *steps = @[^{ printf("4. step one\n"); }, ^{ printf(" step two\n"); }];
for (void (^step)(void) in steps) {
step();
}
}
return 0;
} from session import run
# What blocks capture: build and run blocks.m, then compile a block that assigns to a variable without __block.
run("""
cd blocks
clang -fobjc-arc -Wall -Wextra -Wformat-security -framework Foundation blocks.m -o blocks
./blocks
!clang -fobjc-arc -Wall -Wextra -Wformat-security -framework Foundation blocks_bad.m -o blocks_bad
""") Output
$ cd blocks
$ clang -fobjc-arc -Wall -Wextra -Wformat-security -framework Foundation blocks.m -o blocks
$ ./blocks
1. priceAfter(200) = 190, with discount now 50
2. sum of 3, 1 and 2 with __block: 6
3. a block that captured a ticket
the variable is nil
the block still has the ticket
- ticket freed
the block is gone
4. step one
step two
$ clang -fobjc-arc -Wall -Wextra -Wformat-security -framework Foundation blocks_bad.m -o blocks_bad
blocks_bad.m:10:17: error: variable is not assignable (missing __block type specifier)
10 | sum += mark.intValue;
| ~~~ ^
1 error generated.
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 blocks_session.py
priceAfter(200) gave 190, not 150: the block had copied discount when it was 10, and changing the variable
afterwards did not reach the copy. The sum of 3, 1 and 2 reached 6 only because sum was __block; without it,
blocks_bad.m did not compile: variable is not assignable (missing __block type specifier). The third case is the
one that matters for memory: the ticket survived ticket = nil, because the block captured the object strongly, and it
was freed only when the block itself went at show = nil. Apple’s guide states the rule: blocks keep strong
references to any objects they capture.
| What the block uses | What it captures | Can it change it? |
|---|---|---|
a local int |
a constant copy, made when the block is created | no |
a __block local |
the shared variable itself | yes, and the scope sees it |
| an object pointer | a strong reference to the object | the object, yes; the variable, no |
an instance variable or self.label |
self, strongly |
through self |
The last row hides a trap: the specification says that a block using an instance variable, such as _label, imports
self, so the block holds the whole object even though the word self never appears in it.
Checkpoint
Check: What a block captures
A block that uses self can keep its object alive
Cases 3 to 5 of cycle.m are the same ring as case 1, with a block in it. In case 3 the feed keeps the block in its
onUpdate property, and the block uses self.label, so it keeps the feed. Step through it, and then through the fix:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- A feed. Case 3 creates a Feed and keeps it in the local variable f: one strong reference.
- The block uses self. watch creates a block that prints self.label. Apple's guide says a block keeps strong references to what it captures, self included, so the block holds the feed.
- The feed keeps the block. self.onUpdate = ^{ … } stores the block in a copy property, so the feed holds the block. clang warned here: capturing self strongly in this block is likely to lead to a retain cycle.
- The scope ends. f goes away at the closing brace and releases its reference. The feed still has one, from the block, and the block one, from the feed.
- A cycle. Nothing outside the pair points at it, yet each keeps the other alive. ARC has no cycle collector, so neither is ever freed: the run counted live objects: 3.
- Case 4, a weak self. watchWeakly first stores self in weakSelf, a __weak variable, and the block captures weakSelf instead of self. The feed again has one strong reference, f.
- The scope ends again. f goes away. Now the feed has no strong reference at all, because the block holds only the weak one.
- Freed in order. ARC frees the feed, which printed - feed sport freed; freeing the feed releases its block, and weakSelf would read nil. Inside the block, strongSelf keeps the feed alive while one update runs.
The fix of case 4 is the pattern Apple’s guide recommends. The method first stores self in a weak variable,
__weak Feed *weakSelf = self;, and the block uses weakSelf, so it holds nothing strongly. Inside the block,
Feed *strongSelf = weakSelf; takes a strong reference for the length of one run and tests it for nil, because the
feed may already be gone by the time the block runs; Apple’s guide says the weak pointer is then simply nil. The run
printed - feed sport freed, and the live count did not grow.
Case 5 is the reason not to rely on the compiler. clang flagged case 3 with capturing 'self' strongly in this block is likely to lead to a retain cycle, but case 5, where the block reaches the feed’s array through a local variable,
NSMutableArray *list = self.handlers;, made the same ring with no warning at all, and the count rose to 4. The warning
sees a block stored straight into self; it cannot follow every path to it.
Using self, or an instance variable, in a stored block
A block kept by self, in a property, an array or a handler that self owns, must not hold self strongly:
self.onUpdate = ^{ [self reload]; }; leaks, and so does ^{ [_table reload]; }, which captures self too. Write
__weak typeof(self) weakSelf = self; before the block and use weakSelf inside it, testing a strong copy of it for
nil before use.
Checkpoint
Check: Blocks that use self
Nullability and generics decide what Swift sees
Swift imports every Objective-C header, and what it sees depends on what the header says. Apple’s page on designating
nullability lists the rules: a pointer marked nonnull comes in as a plain type, one marked nullable as an optional,
and one with no annotation as an implicitly unwrapped optional: A Swift type written with !, such as String!, that may hold nil but can be used without unwrapping; if it is nil when used, the program stops. Swift imports unannotated Objective-C pointers this way., a
value Swift lets you use as if it could not be nil. Lightweight generics, such as NSArray<NSString *> *, keep the
type of a collection’s elements; without them, Swift sees [Any]. Between NS_ASSUME_NONNULL_BEGIN and
NS_ASSUME_NONNULL_END every declaration without an annotation counts as nonnull, so only the nullable ones need a
word.
Shelf.h is written the old way. TypedShelf.h declares the same class with annotations, one nullable result and an
element type, plus a method that reports a failure through NSError **:
swift/Shelf.h
// Shelf.h: a header written the old way, with no nullability annotations and no element types.
#import <Foundation/Foundation.h>
@interface Shelf : NSObject
@property (nonatomic, copy) NSArray *titles;
- (NSString *)titleAtIndex:(NSUInteger)index; // nil when there is no book at that index
- (void)addTitle:(NSString *)title;
@end swift/TypedShelf.h
// TypedShelf.h: the same class, annotated: nonnull unless marked, one nullable result, the element type of the
// array, and a method that reports a failure through NSError.
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TypedShelf : NSObject
@property (nonatomic, copy) NSArray<NSString *> *titles;
- (nullable NSString *)titleAtIndex:(NSUInteger)index;
- (void)addTitle:(NSString *)title;
- (BOOL)removeTitle:(NSString *)title error:(NSError **)error;
@end
NS_ASSUME_NONNULL_END swift/plain.swift
// plain.swift: Swift using the unannotated Shelf. Both lines with String compile.
let shelf = Shelf()
shelf.addTitle("Gitanjali")
print("titles:", shelf.titles.count)
let first: String = shelf.title(at: 0)
print("first:", first)
let sixth: String = shelf.title(at: 5)
print("sixth:", sixth) swift/typed_fixed.swift
// typed_fixed.swift: TypedShelf used as Swift wants it: the optional unwrapped, and the NSError ** method called
// with try.
let shelf = TypedShelf()
shelf.addTitle("Gitanjali")
for title in shelf.titles {
print(title.uppercased())
}
if let sixth = shelf.title(at: 5) {
print("sixth:", sixth)
} else {
print("no book at index 5")
}
do {
try shelf.removeTitle("Kabir")
} catch {
print("removeTitle failed:", error.localizedDescription)
} The session compiles each class with clang and builds Swift programs against its header with -import-objc-header,
which hands swiftc a header to import, the job a bridging header does in an Xcode target, as Apple’s page on importing
Objective-C describes it:
from session import run
# Swift and Objective-C in one program: compile each Objective-C class to an object file, then build Swift programs
# that import its header (-import-objc-header, the command-line form of a bridging header) and link the object file.
run("""
cd swift
swiftc --version
clang -fobjc-arc -Wall -Wextra -c Shelf.m -o Shelf.o
clang -fobjc-arc -Wall -Wextra -c TypedShelf.m -o TypedShelf.o
swiftc -import-objc-header Shelf.h plain.swift Shelf.o -o plain
!./plain
!swiftc -import-objc-header Shelf.h plain_loop.swift Shelf.o -o plain_loop
!swiftc -import-objc-header TypedShelf.h typed.swift TypedShelf.o -o typed
swiftc -import-objc-header TypedShelf.h typed_fixed.swift TypedShelf.o -o typed_fixed
./typed_fixed
""") Output
$ cd swift
$ swiftc --version
swift-driver version: 1.168.6 Apple Swift version 6.4 (swiftlang-6.4.0.34.1 clang-2100.3.34.1)
Target: arm64-apple-macosx26.0
$ clang -fobjc-arc -Wall -Wextra -c Shelf.m -o Shelf.o
$ clang -fobjc-arc -Wall -Wextra -c TypedShelf.m -o TypedShelf.o
$ swiftc -import-objc-header Shelf.h plain.swift Shelf.o -o plain
$ ./plain
titles: 1
first: Gitanjali
plain/plain.swift:7: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
[exit status 133]
$ swiftc -import-objc-header Shelf.h plain_loop.swift Shelf.o -o plain_loop
plain_loop.swift:5:17: error: value of type 'Any' has no member 'uppercased'
3 | shelf.addTitle("Gitanjali")
4 | for title in shelf.titles {
5 | print(title.uppercased())
| |- error: value of type 'Any' has no member 'uppercased'
| `- note: cast 'Any' to 'AnyObject' or use 'as!' to force downcast to a more specific type to access members
6 | }
7 |
[exit status 1]
$ swiftc -import-objc-header TypedShelf.h typed.swift TypedShelf.o -o typed
typed.swift:7:27: error: value of optional type 'String?' must be unwrapped to a value of type 'String'
5 | print(title.uppercased())
6 | }
7 | let sixth: String = shelf.title(at: 5)
| |- error: value of optional type 'String?' must be unwrapped to a value of type 'String'
| |- note: coalesce using '??' to provide a default when the optional value contains 'nil'
| `- note: force-unwrap using '!' to abort execution if the optional value contains 'nil'
8 | print("sixth:", sixth)
9 |
[exit status 1]
$ swiftc -import-objc-header TypedShelf.h typed_fixed.swift TypedShelf.o -o typed_fixed
$ ./typed_fixed
GITANJALI
no book at index 5
removeTitle failed: no book called Kabir on the shelf
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 swift_session.py
| Objective-C declaration | Swift sees | What happened |
|---|---|---|
- (NSString *)titleAtIndex: |
String! |
let sixth: String = … compiled, then stopped with exit status 133 |
- (nullable NSString *)titleAtIndex: |
String? |
the same line did not compile: the optional must be unwrapped |
NSArray *titles |
[Any] |
title.uppercased() did not compile: Any has no member |
NSArray<NSString *> *titles |
[String] |
the loop compiled and printed GITANJALI |
- (BOOL)removeTitle:error: |
func removeTitle(_:) throws |
try caught no book called Kabir on the shelf |
The first row is the expensive one. With no annotation, the Swift compiler had no way to know that the method can
return nil, accepted the program, and the program stopped at run time with Unexpectedly found nil while implicitly unwrapping an Optional value. The annotated header moved the same mistake to compile time, where it costs nothing.
The last row shows Apple’s rule for error parameters at work: the NSError ** became throws.
Key idea
Annotate every header Swift will read: NS_ASSUME_NONNULL_BEGIN and END around it, nullable where nil can
come back, and element types on collections, so that Swift sees optionals and typed arrays instead of String! and
[Any].
Checkpoint
Check: How Swift imports a header
C’s hazards stay: format strings, widths and overflow
Objective-C is C underneath, so C’s ways to go wrong come with it. The most serious is a format string that comes from
outside the program. note_unsafe.m passes a note from its command line to stringWithFormat: as the format itself;
note_safe.m passes it as the argument of @"%@":
format/note_unsafe.m
// note_unsafe.m: prints a note that comes from outside the program, using the note itself as the format.
#import <Foundation/Foundation.h>
#include <stdio.h>
int main(int argc, const char *argv[]) {
@autoreleasepool {
if (argc != 2) {
fprintf(stderr, "usage: note_unsafe <note>\n");
return 2;
}
NSString *note = @(argv[1]);
NSString *line = [NSString stringWithFormat:note];
printf("note: %s\n", line.UTF8String);
}
return 0;
} format/note_safe.m
// note_safe.m: the same program with a fixed format; the note is only ever an argument.
#import <Foundation/Foundation.h>
#include <stdio.h>
int main(int argc, const char *argv[]) {
@autoreleasepool {
if (argc != 2) {
fprintf(stderr, "usage: note_safe <note>\n");
return 2;
}
NSString *note = @(argv[1]);
NSString *line = [NSString stringWithFormat:@"%@", note];
printf("note: %s\n", line.UTF8String);
}
return 0;
} from session import run
# A note from outside the program used as a format, then used as an argument; and the check that tells the two apart
# at build time, -Werror=format-security.
run("""
cd format
clang -fobjc-arc -Wall -Wextra -Wformat-security -framework Foundation note_unsafe.m -o note_unsafe
./note_unsafe 'Paid in full: 100%'
!./note_unsafe 'Refund %@ %@'
clang -fobjc-arc -Wall -Wextra -Wformat-security -framework Foundation note_safe.m -o note_safe
./note_safe 'Paid in full: 100%'
./note_safe 'Refund %@ %@'
!clang -fobjc-arc -Wall -Wextra -Werror=format-security -framework Foundation note_unsafe.m -o note_unsafe
clang -fobjc-arc -Wall -Wextra -Werror=format-security -framework Foundation note_safe.m -o note_safe
""") Output
$ cd format
$ clang -fobjc-arc -Wall -Wextra -Wformat-security -framework Foundation note_unsafe.m -o note_unsafe
note_unsafe.m:12:53: warning: format string is not a string literal (potentially insecure) [-Wformat-security]
12 | NSString *line = [NSString stringWithFormat:note];
| ^~~~
note_unsafe.m:12:53: note: treat the string as an argument to avoid this
12 | NSString *line = [NSString stringWithFormat:note];
| ^
| @"%@",
1 warning generated.
$ ./note_unsafe 'Paid in full: 100%'
note: Paid in full: 100
$ ./note_unsafe 'Refund %@ %@'
[exit status 139]
$ clang -fobjc-arc -Wall -Wextra -Wformat-security -framework Foundation note_safe.m -o note_safe
$ ./note_safe 'Paid in full: 100%'
note: Paid in full: 100%
$ ./note_safe 'Refund %@ %@'
note: Refund %@ %@
$ clang -fobjc-arc -Wall -Wextra -Werror=format-security -framework Foundation note_unsafe.m -o note_unsafe
note_unsafe.m:12:53: error: format string is not a string literal (potentially insecure) [-Werror,-Wformat-security]
12 | NSString *line = [NSString stringWithFormat:note];
| ^~~~
note_unsafe.m:12:53: note: treat the string as an argument to avoid this
12 | NSString *line = [NSString stringWithFormat:note];
| ^
| @"%@",
1 error generated.
[exit status 1]
$ clang -fobjc-arc -Wall -Wextra -Werror=format-security -framework Foundation note_safe.m -o note_safe
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 format_session.py
clang warned at once: format string is not a string literal (potentially insecure), with the fix spelled out, treat
the string as an argument. The two runs of the unsafe program show why. A note ending in 100% lost its percent sign
without a word, because % started a conversion. A note with two %@ made stringWithFormat: read two objects that
were never passed, and the program died with exit status 139, 128 plus signal 11, a segmentation fault. The safe
program printed both notes exactly. MITRE lists this weakness as CWE-134, use of an externally controlled format
string, and its mitigations include passing format functions only fixed strings and building with high warning
levels.
Make the check fail the build
-Werror=format-security turns the warning into an error: the session’s build of note_unsafe.m failed with
[-Werror,-Wformat-security], and note_safe.m built. Put the flag in the build script of every target, so a
format string that is not a literal can never reach a release, and write NSLog(@"%@", text), never NSLog(text).
Two quieter hazards come from C’s integer types. A format must match the size of its argument, and an NSInteger is
8 bytes on a 64-bit Mac while %d reads 4, so 5,000,000,000 paise printed as 705,032,704; clang’s -Wformat
warning even offered the fix, %ld with a (long) cast. And signed overflow is undefined in C: 2147483647 + 1 in
an int printed −2,147,483,648 with no complaint, until the program was built with -fsanitize=undefined, whose
check reported the line and the values: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int':
// widths.m: two C hazards that Objective-C keeps: a format that is too narrow for its argument, and a signed
// overflow, which C leaves undefined.
#import <Foundation/Foundation.h>
#include <stdio.h>
int main(void) {
@autoreleasepool {
NSInteger paise = 5000000000;
NSString *wrong = [NSString stringWithFormat:@"%d paise", paise];
NSString *right = [NSString stringWithFormat:@"%ld paise", (long)paise];
printf("with %%d: %s\nwith %%ld: %s\n", wrong.UTF8String, right.UTF8String);
int count = 2147483647;
count = count + 1;
printf("2147483647 + 1 in an int: %d\n", count);
}
return 0;
} from session import run
# A format too narrow for its argument and a signed overflow: the warning clang gives, what the program prints, and
# what the undefined-behaviour sanitizer reports when the same program is built with -fsanitize=undefined.
run("""
cd format
clang -fobjc-arc -Wall -Wextra -Wformat-security -framework Foundation widths.m -o widths
./widths
clang -fobjc-arc -Wall -Wextra -Wno-format -fsanitize=undefined -framework Foundation widths.m -o widths_checked
./widths_checked
""") Output
$ cd format
$ clang -fobjc-arc -Wall -Wextra -Wformat-security -framework Foundation widths.m -o widths
widths.m:9:67: warning: values of type 'NSInteger' should not be used as format arguments; add an explicit cast to 'long' instead [-Wformat]
9 | NSString *wrong = [NSString stringWithFormat:@"%d paise", paise];
| ~~ ^~~~~
| %ld (long)
1 warning generated.
$ ./widths
with %d: 705032704 paise
with %ld: 5000000000 paise
2147483647 + 1 in an int: -2147483648
$ clang -fobjc-arc -Wall -Wextra -Wno-format -fsanitize=undefined -framework Foundation widths.m -o widths_checked
$ ./widths_checked
with %d: 705032704 paise
with %ld: 5000000000 paise
widths.m:13:23: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior widths.m:13:23
2147483647 + 1 in an int: -2147483648
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 widths_session.py
Checkpoint
Check: Format strings and other C hazards
Practise: predict which objects ARC never frees
The exercise turns the counting rule of this lesson into a function. Given the strong and weak references between a
program’s objects and the variables that hold some of them, it returns the objects that are still alive after those
variables let go, which is what cycle.m’s counter measured. Its sample tests are the five cases of cycle.m, the
lost ticket T4, the ticket T2 that two variables held, and a chain that ends in a ring:
Exercise · Medium · Python
Predict which objects ARC never frees
cycle.m counted its live objects to find leaks. Write survivors(objects, strong, weak, held) in survivors.py, which predicts the same count before the program runs: it returns the names of the objects that are still alive once the program's variables have let go, sorted alphabetically.
objectslists every object by name, such as["customer", "order"].stronglists the strong references between objects as pairs:("customer", "order")means the customer holds the order strongly, through a property, an array or a captured block.weaklists the weak references the same way. They never keep an object alive.heldlists the objects that local variables hold strongly; a name may appear twice when two variables hold the same object. All of these variables let go at the end.
Count the way ARC does, not the way a garbage collector does. An object's count is the number of strong references to it, from held and from strong. When the variables let go, each count falls by one for each of them, and an object whose count reaches 0 is freed. Freeing an object releases every strong reference it held, which may free more objects in turn. An object that no strong reference holds from the start is freed at once, like the ticket T4. What is left at the end is the answer.
The sample tests are the five cases of cycle.m, the lost ticket of lifetimes.m, a ticket held by two variables, and a chain that ends in a ring.
Starter code · survivors.py
def survivors(objects, strong, weak, held):
"""Return, sorted, the names of the objects still alive after every variable in held lets go."""
# A garbage collector frees everything that no variable can reach any more, so this first try returns nothing.
# ARC counts references instead: replace this with the counting the prompt describes.
return [] The sample tests · test_survivors.py
from survivors import survivors
def test_case_1_strong_both_ways():
"""case 1: customer and order hold each other strongly, so both stay"""
assert survivors(["customer", "order"], [("customer", "order"), ("order", "customer")], [], ["customer"]) == ["customer", "order"]
def test_case_2_weak_back_reference():
"""case 2: the order points back weakly, so both are freed"""
assert survivors(["customer", "order"], [("customer", "order")], [("order", "customer")], ["customer"]) == []
def test_cases_3_and_4_block_and_self():
"""case 3: the feed keeps its block and the block keeps the feed; case 4: the block holds a weak self"""
assert survivors(["block", "feed"], [("feed", "block"), ("block", "feed")], [], ["feed"]) == ["block", "feed"]
assert survivors(["block", "feed"], [("feed", "block")], [("block", "feed")], ["feed"]) == []
def test_case_5_ring_through_an_array():
"""case 5: feed, its array and the block in it form a ring of three"""
strong = [("feed", "list"), ("list", "block"), ("block", "feed")]
assert survivors(["block", "feed", "list"], strong, [], ["feed"]) == ["block", "feed", "list"]
def test_object_held_by_nothing():
"""T4: an object stored only in a weak variable is freed at once"""
assert survivors(["T4"], [], [], []) == []
def test_two_variables_one_object():
"""T2: two variables hold one ticket, and both let go"""
assert survivors(["T2"], [], [], ["T2", "T2"]) == []
def test_chain_into_a_ring():
"""a holds b, b holds c, and c and d hold each other: a and b are freed, the ring of c and d stays"""
strong = [("a", "b"), ("b", "c"), ("c", "d"), ("d", "c")]
assert survivors(["a", "b", "c", "d"], strong, [], ["a"]) == ["c", "d"] A hint
Keep a dictionary of counts, one entry per object, and start it from strong and held. Then subtract one for each name in held, and put every object whose count is 0 on a list of objects to free. Freeing one means subtracting one from each object it holds strongly; any that reach 0 join the list.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then take the self-check on the six ideas of the lesson. Ideas you have answered wrongly come back first, and every option you choose is explained straight away:
Practice that adapts to you
Practise this lesson
12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Organisations named on this page
MySmartCoPilot is independent of Apple, the LLVM Project and MITRE. Apple and the LLVM Project are named for their compilers and documentation, MITRE for its list of software weaknesses.
Interview questions
Warm-up (fresher to mid-level): What is a retain cycle, and how does a weak reference break it?
A retain cycle is a ring of strong references: object A holds B strongly and B holds A, directly or through other
objects. Under ARC an object is freed when its last strong reference goes, so when the rest of the program lets go of
the ring, every object in it still has one strong reference, from its neighbour, and none is ever freed: Clang’s ARC
specification says ARC has no cycle collector. A weak reference does not keep its object alive, and ARC sets it to
nil when the object goes, so making one link weak, usually the one from a child back to its parent or from a block
back to self, lets the whole ring be freed. In the lesson, a customer and an order that held each other strongly
stayed alive; with a weak reference back, both were freed.
Three questions of the kind asked about memory, Swift boundaries and safe printing:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
In an app with Objective-C screens, memory grows by a few hundred kilobytes every time a user opens and closes the order screen, and its controller's dealloc never runs. The controller keeps blocks for a timer and for network callbacks. How do you find the cycle and fix it?
-
Fresher to senior
Swift code that calls an Objective-C method from your module crashes with "Unexpectedly found nil while implicitly unwrapping an Optional value", though it compiled cleanly. What does the crash tell you about the header, and how do you fix it so that the compiler catches the next one?
-
Mid to staff
Reading a colleague's change, you find NSLog(message) in a module that logs text received from a server, and the same pattern with stringWithFormat: in a few other files. The author says the messages are always plain text. What do you say, and what do you change in the code and in the build?
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Under ARC, when is an object freed?
As soon as its last strong reference goes. Object pointers are strong unless declared otherwise; weak references do not count and read nil once the object is gone.
-
What becomes of a new object that is stored only in a __weak variable?
It never has a strong reference, so it is freed straight away. clang warned that it will be released after assignment, and the run printed T4 created and T4 freed back to back.
-
A customer and an order hold each other strongly. Why are they never freed?
Each keeps one strong reference from the other when the program lets go, and ARC has no cycle collector. A weak reference from the order back to its customer freed both.
-
A block uses a local int discount, 10 at the time. The variable is then set to 50. What does the block see?
Still 10: a block copies the local values it uses, as constants, at the moment it is created. Only a variable marked __block is shared with the scope around it.
-
Why can a block that only uses the instance variable _label keep its object alive?
Using an instance variable captures self, so the block holds the whole object strongly although self is never written. If self stores that block, the two form a ring.
-
How should a block that self keeps refer to self?
Through __weak typeof(self) weakSelf = self;, written before the block. Inside, take a strong copy of weakSelf for one run and test it for nil, since the object may already be gone.
-
What does Swift see for an unannotated method returning NSString *, and for the same method marked nullable?
String!, an implicitly unwrapped optional, which stopped with exit status 133 on nil; and String?, which the compiler makes you unwrap. NSArray<NSString *> * comes in as [String], a plain NSArray as [Any].
-
Why is [NSString stringWithFormat:note] dangerous when note comes from outside the program?
The note becomes the format: a % swallowed text, and a note holding two %@ made it die with exit status 139 (CWE-134). Pass the note as an argument of @"%@", and build with -Werror=format-security.
Key takeaways
- Under ARC an object is freed when its last strong reference goes; weak references do not count and read
nilafterwards, and an object stored only in a weak variable is freed at once. - ARC has no cycle collector, so two objects that hold each other strongly are never freed; a child’s reference back
to its parent is weak, and properties are
strong,weakorcopyby ownership. - A block copies the local values it uses when it is created, shares only
__blockvariables, and keeps every object it captures alive,selfincluded, even when it only uses an instance variable. - A block stored by
selfmust use a weak self; clang’s warning caught the direct case and missed one that went through a local variable, so the live count is the real test. - Unannotated headers give Swift
String!and[Any];NS_ASSUME_NONNULL_BEGIN,nullableand element types give it optionals and typed arrays, and anNSError **method becomes one that throws. - A format string from outside the program crashed it with exit status 139 and silently dropped a
%; build with-Werror=format-security, match formats to their types, and use-fsanitize=undefinedto catch overflow.
Terms in this lesson
- __block variable
- A local variable declared with __block, which the blocks that use it share with its scope instead of copying, so that a block can change it and the code around it sees the change.
- ARC (automatic reference counting)
- Objective-C memory management in which the compiler inserts the retain and release calls, so that an object lives while a strong reference to it exists and is freed after the last one goes. It does not collect reference cycles.
- block
- A piece of code written ^(arguments) { … } that can be stored and called later, with the variables it uses captured: the Objective-C and C counterpart of a Swift closure. Blocks are Objective-C objects.
- implicitly unwrapped optional
- A Swift type written with !, such as String!, that may hold nil but can be used without unwrapping; if it is nil when used, the program stops. Swift imports unannotated Objective-C pointers this way.
- retain cycle
- A ring of strong references, such as two objects that hold each other, which keeps every object in it alive after the rest of the program lets go. Apple also calls it a strong reference cycle; ARC never collects one.
- strong reference
- A reference that keeps its object alive: under ARC an object lives while at least one strong variable, property or captured block points at it. Object pointers are strong unless declared otherwise.
- weak reference
- A reference, declared __weak or with the weak attribute, that does not keep its object alive. ARC sets it to nil when the object is freed, so it never points at freed memory.
References
- Objective-C Automatic Reference Counting (ARC) (LLVM Project)
- Language Specification for Blocks (LLVM Project)
- Transitioning to ARC Release Notes (Apple)
- Programming with Objective-C: Encapsulating Data (Apple)
- Programming with Objective-C: Working with Blocks (Apple)
- Designating Nullability in Objective-C APIs (Apple)
- Using Imported Lightweight Generics in Swift (Apple)
- Importing Objective-C into Swift (Apple)
- About Imported Cocoa Error Parameters (Apple)
- CWE-134: Use of Externally-Controlled Format String (MITRE)
- UndefinedBehaviorSanitizer (LLVM Project)
- Diagnostic flags in Clang: -Wformat-security (LLVM Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress