Advanced Pointer and Casting Patterns

Master the advanced pointer patterns that show up at the boundary between Objective-C and C: pointer-to-pointer parameters, the universal void * pointer, function pointers as callbacks, void *context smuggling of Objective-C objects through C APIs, unsafe reinterpret casts versus safe runtime-checked casts, and the object-lifetime hazards that arise when a C API holds onto a context pointer longer than the Objective-C runtime expects.

Most everyday Objective-C code never needs to think hard about pointers beyond NSString *name or id delegate — ARC handles memory, and the compiler enforces reasonable type safety. But the moment you interact with a C API — and in Objective-C, you always eventually do, because Objective-C is built on C and large parts of the system frameworks (Core Foundation, Core Audio, Core Graphics, POSIX, libdispatch's lower layers) expose plain C interfaces — you run into a different, older, more dangerous set of pointer idioms. C APIs pass results back through pointer-to-pointer output parameters, they describe 'any type' with void *, they accept callback functions as raw function pointers, and they let you attach an arbitrary piece of context data to a callback via a void *context parameter that you must cast back to something meaningful yourself, by hand, with no help from the compiler. This lesson looks closely at that C-flavored corner of pointer usage, building directly on 'Advanced Objective-C Type System' (which covered safe, runtime-checked casting) and 'Toll-Free Bridging' (which covered __bridge casts between Objective-C and Core Foundation objects), and setting up 'C Interoperability' and the sibling lesson 'Objective-C + C Callback Interoperability' for the full callback story.

Pointer-to-Pointer: A Pointer's Address, Not Its Value

A pointer-to-pointer is exactly what it sounds like: a pointer whose pointed-to value is itself another pointer. You've almost certainly already used one without necessarily naming it that way — the familiar NSError **error output parameter, as in - (BOOL)writeToFile:(NSString *)path error:(NSError **)error;, is a pointer-to-pointer. The method doesn't receive an NSError * directly; it receives the *address of a variable that holds* an NSError *, so that it can write a new NSError * value into that variable and have the change visible back at the call site, even though C (and Objective-C, which is C underneath) passes all arguments by value.

This is the fundamental reason pointer-to-pointer exists at all: C has no 'pass by reference' the way some languages do. When you pass an int to a function, the function gets a copy — modifying it inside the function does not change the caller's variable. The same is true for a plain pointer: if a function took NSError *error (a single star) and tried to do error = [NSError errorWithDomain:...] inside it, that would only reassign the function's local copy of the pointer, and the caller's variable would be completely unaffected once the function returned. To let a function reach back and modify a variable that lives in the caller's stack frame, you must instead give the function the *address* of that variable — a pointer to it — so it can dereference that address and write through it. That's precisely what NSError ** gives writeToFile:error:: the address of your local NSError *error variable, so the method can do the equivalent of *error = newErrorObject; and have error in your own code now hold that new value after the call returns.

This same double-indirection pattern shows up constantly in plain C beyond NSError , wherever a function needs to hand back more than one thing (its return value, plus one or more 'out' values) or needs to modify a pointer variable that belongs to the caller. A generic C example: void allocate_buffer(char outBuffer, size_t size) { *outBuffer = malloc(size); } — called as char *buf = NULL; allocate_buffer(&buf, 128);. Here outBuffer is a char **; inside the function, *outBuffer = malloc(size) writes a freshly allocated char * through that pointer-to-pointer, directly into the caller's buf variable. Without the extra level of indirection, there would be no way for allocate_buffer to hand a newly allocated pointer back to its caller other than as an ordinary return value — which is exactly what you do when you only need to hand back one thing, but pointer-to-pointer becomes necessary the moment the return value slot is already used for something else (like writeToFile:error:'s BOOL success/failure result), and the error needs a separate out-channel.

• A pointer-to-pointer (T **) is the address of a variable that itself holds a T * — it lets a function write a new pointer value back into the caller's variable. • C always passes arguments by value; a single-star pointer parameter only lets you modify what it *points to*, not reassign the caller's own pointer variable. • NSError **error is the most common Objective-C example: the method writes a new NSError * through the pointer-to-pointer so it's visible to the caller after the call. • Generic C output parameters (like a buffer-allocating function writing back a freshly malloc'd pointer) use the exact same T ** pattern for the same reason. • Pointer-to-pointer becomes necessary specifically when a function's own return value slot is already used for something else, so an extra value must come back through an out-parameter instead.

void *: C's Universal 'Any Type' Pointer

C is a statically typed language, but it has one deliberate escape hatch built into its type system: void *, a pointer that carries no type information about what it points to at all. Every other pointer type — int *, char *, MyStruct * — tells the compiler how many bytes the pointed-to value occupies and how to interpret those bytes; a void * tells the compiler nothing beyond 'this is an address.' You cannot dereference a void * directly (the compiler has no idea how many bytes to read or how to interpret them), and you cannot do pointer arithmetic on one, because arithmetic on a pointer is defined in terms of the size of the type it points to, which void * doesn't have.

What void * is good for is exactly what its lack of type information suggests: writing C functions that need to work generically, across any type the caller wants, without the function itself needing to know what that type is. The classic example is void *memcpy(void *dst, const void *src, size_t n); — memcpy copies raw bytes and genuinely does not care whether those bytes represent an int, a struct, or an array of NSString * pointers (well, pointers to Objective-C objects specifically, but from memcpy's perspective it's just bytes); it takes void * parameters so it can be called with a pointer to absolutely anything. malloc returns void * for the same reason in the other direction: it allocates raw, untyped memory, and it's up to the caller to decide what type of data will live there and to use the pointer accordingly (implicitly, or via an explicit cast in stricter code or in Objective-C++).

The other place void * is indispensable — and the one this lesson cares about most — is as a generic 'carry anything' slot in a C API's callback registration function, conventionally named context, userInfo, userData, or info depending on the framework. Because the C API itself has no idea what kind of data the caller might want available inside the callback (it could be a C struct, a plain integer disguised as a pointer, or, critically for Objective-C programmers, a reference to an Objective-C object), the API accepts a void * for that slot, hands it right back to you unexamined when it invokes your callback, and trusts you to cast it back to whatever type you originally put in. The API never dereferences or interprets the void * itself — it is purely a return-trip courier.

• void * is a pointer with no attached type information — you cannot dereference it or do pointer arithmetic on it directly, only cast it to a real type first. • memcpy and malloc use void * because they operate on raw, generically-typed memory and genuinely don't care what type the bytes represent. • In callback-registration C APIs, a void *context (or userInfo/userData/info) parameter exists purely so the caller can attach arbitrary data, of any type, that the API itself never inspects. • The API's only job with a void *context is to store it and hand it back unchanged to your callback later — it is a courier, not a consumer, of that pointer. • Because void * erases type information, the responsibility for casting it back to the *correct* original type falls entirely on your code, with zero compiler help if you get it wrong.

Smuggling an Objective-C Object Through void *context

Here is the pattern that makes void *context genuinely useful to an Objective-C programmer rather than just a C curiosity: you can pass an Objective-C object pointer into a C API's void *context parameter, and get a usable reference to that same object back later, inside a plain C callback function that otherwise knows nothing about Objective-C. This is exactly the mechanism 'Toll-Free Bridging' introduced for moving between Objective-C objects and Core Foundation types, applied here to a completely opaque void * slot instead of a specific CF-typed one.

Going in, you write (__bridge void *)someObject at the point where the C API asks for a void *context. The __bridge cast tells ARC 'I am converting this Objective-C object reference to a plain C pointer type, but I am not transferring ownership — ARC's retain/release bookkeeping for this object is unaffected by this cast, and no retain count changes as a result of it.' The C API now holds a void * that, if you looked at its raw bits, is exactly the same address as someObject, but as far as ARC's accounting is concerned, nothing has changed: someObject is still owned exactly as it was before this cast, by whatever strong references already existed to it before you handed its address to the C API.

Coming back, inside the C callback, you write (__bridge id)context (or (__bridge SomeClass *)context when you know and want the specific type) to convert the raw void * back into an Objective-C object reference. Again, __bridge here means 'treat these bits as an object reference, but don't touch the retain count' — ARC trusts you that the bits genuinely came from a live Objective-C object and simply resumes treating that address as an object pointer, at which point you can send it messages normally. The two __bridge casts, one going out and one coming back, are a matched pair: together they let an Objective-C object ride through a C API's generic void * machinery and come out the other side usable again, with ARC never having lost track of the object's true ownership the entire time (because __bridge never touched it) — provided, and this is the crucial caveat the next section covers in depth, that the object is still alive when the callback actually fires.

• (__bridge void *)object passes an Objective-C object's address into a C API's void *context parameter without transferring ownership or changing its retain count. • (__bridge id)context (or a specific-class cast) converts the void * back into a usable Objective-C object reference inside a C callback, again without touching the retain count. • The two casts are a matched pair — the same underlying object, viewed as a raw pointer on the way in and as an object reference on the way back out. • __bridge performs zero memory-management side effects; it only reinterprets the pointer's type for the compiler and ARC, trusting the programmer that the bits are valid. • This pattern is the general-purpose version of Toll-Free Bridging's __bridge casts, applied to an opaque void *context slot rather than a specific CFTypeRef.

Function Pointers as First-Class Values

A function pointer is a pointer that holds the address of executable code rather than the address of a piece of data — it lets you store 'a function' in a variable, pass it as an argument, and call it indirectly through the pointer, all without knowing at compile time exactly which function will end up being called. The declaration syntax is famously awkward: void (*callback)(int, void *) declares a variable named callback that is a pointer to a function taking an int and a void * and returning void. The parentheses around *callback are required — without them, void *callback(int, void *) would instead declare a function named callback that *returns* void *, which is an entirely different thing.

Function pointers are what let a C API be configurable with custom behavior without needing to know, at the time the API itself was written and compiled, anything about what that behavior will be. A C API that wants to notify you when something happens declares a parameter of function-pointer type — void RegisterHandler(void (*callback)(int event, void *context), void *context); — and you satisfy it by passing the address of an actual C function you've written matching that exact signature, typically just its bare name (RegisterHandler(myCallback, someContext);), because a plain function name, unadorned, decays to a pointer to itself in this position, the same way an array name decays to a pointer to its first element.

This is precisely the mechanism underneath what you'd call a 'callback' in Objective-C-adjacent C APIs — it's the same idea as an Objective-C block or a delegate method, just implemented at the C level with a raw address instead of an object reference or a block literal. The function you register must match the expected signature exactly (same parameter types, same return type) — the C API has no runtime type-checking equivalent to respondsToSelector:; if the signatures don't line up, you get undefined behavior when the callback is actually invoked, not a helpful error at the call site.

• A function pointer stores the address of code, letting you call a function indirectly through a variable rather than by its name directly. • Declaration syntax ReturnType (*name)(ParamTypes) requires parentheses around *name to distinguish 'pointer to function' from 'function returning a pointer'. • A bare function name, used where a function pointer is expected, automatically decays to the address of that function — no explicit & is required (though it's legal to write one). • Function pointers are how C APIs accept custom, caller-supplied behavior without knowing anything about that behavior when the API itself was compiled. • There is no runtime signature checking for function pointers the way respondsToSelector: checks Objective-C methods — a mismatched signature is undefined behavior, not a caught error.

Unsafe Casts Versus Safe, Runtime-Checked Casts

'Advanced Objective-C Type System' covered the safe way to narrow an id or a superclass pointer down to a more specific type: isKindOfClass: (or respondsToSelector: for protocol-style checks) performed *before* a cast, so that the cast only happens once you've actually confirmed, at runtime, that the object really is what you're about to claim it is. That pattern costs a small amount of runtime overhead (a real message send, checking the class hierarchy) in exchange for safety: if the check fails, you can branch away and avoid the cast entirely, instead of proceeding on a false assumption.

An unsafe cast skips that check entirely. Writing (SomeType *)ptr — or the C-level equivalent (SomeCStruct *)voidPtr — simply tells the compiler 'trust me, reinterpret these bits as this type,' with no runtime verification of any kind performed alongside it. The compiler happily reinterprets the pointer's static type and emits code as if the reinterpretation is correct; if it isn't — if ptr doesn't actually point to a SomeType instance, or voidPtr doesn't actually point to a SomeCStruct — the program does not fail cleanly at the cast. It fails later, unpredictably, whenever code eventually reads memory through the mistyped pointer and interprets bytes that mean one thing as if they meant something else entirely, which can range from a garbage value to a crash to, worst of all, silent data corruption that surfaces as a bug somewhere completely unrelated much later.

The void *context round-trip from earlier in this lesson is, deliberately, an unsafe cast by construction: (__bridge SomeClass *)context has no way to verify that the void * you're casting really does hold the address of a live SomeClass instance — it trusts the discipline of the code that originally set up the context, full stop. This is exactly why the pairing matters so much: the safety of an unsafe cast at the receiving end depends entirely on the correctness of the code at the sending end having put the right kind of object in, and kept it alive. When you control both ends (you registered the callback and you wrote the callback), this is a manageable, well-understood risk; the discipline required is just consistency between the two sides, not a runtime guarantee.

• isKindOfClass:-guarded casting (from 'Advanced Objective-C Type System') performs a real runtime check before trusting a narrower type, and lets you branch away safely if it fails. • An unsafe cast like (SomeType *)ptr or (SomeCStruct *)voidPtr performs zero runtime verification — it just reinterprets the pointer's compile-time type and proceeds. • A wrong unsafe cast doesn't fail at the cast itself; it fails later and unpredictably, when mistyped memory is actually read and misinterpreted. • (__bridge SomeClass *)context on a void *context is inherently an unsafe cast — its correctness depends entirely on the discipline of whoever set up that context originally. • Unsafe casts are an acceptable, well-understood risk specifically when you control both the producing and consuming ends of the pointer and can guarantee they agree.

Object Lifetime Across C Callbacks: The Hidden Hazard

The single most dangerous property of the void *context pattern is one that has nothing to do with casting correctness and everything to do with *time*: the object you pass in as context must remain alive for the entire span during which the C API might still call back into it — which is often much longer than the lexical scope in which you registered the callback, and sometimes longer than you'd naturally expect the object itself to live.

Consider registering a callback from inside an Objective-C instance method, passing (__bridge void *)self as context. The __bridge cast, as established, does not retain self — it does not increase self's retain count at all. If nothing else in the program is holding a strong reference to self by the time the C API actually invokes the callback (say, the object that called the registration method has since been deallocated, perhaps because it was a short-lived local object with no other owners), then the void *context the C API hands back to your callback is now a dangling pointer: it holds the address where an Objective-C object used to live, but ARC has already deallocated that memory, and quite possibly the memory has since been reused for something entirely different.

When your callback then does (__bridge id)context and sends that 'object' a message, you are not necessarily crashing immediately, which is what makes this bug class so insidious — you might be reading and interpreting memory that now belongs to a completely different object, or to nothing meaningful at all, producing corrupted behavior, a crash in unrelated code much later, or (least likely but not impossible) something that happens to look like it worked, purely by chance, until it doesn't on a different run. This is a textbook use-after-free, and it is one of the most common real-world bugs at the Objective-C/C boundary specifically because __bridge deliberately does *not* retain, leaving lifetime entirely up to you to reason about by hand.

• The context object must outlive every possible future invocation of the callback, not just the moment you registered it — these lifetimes can differ enormously. • (__bridge void *)self does not retain self; if no other strong reference keeps the object alive, it can be deallocated while the C API still holds its address as context. • A callback firing after its context object has been deallocated reads freed (and possibly reused) memory — a classic use-after-free bug, not a guaranteed crash, which makes it hard to diagnose. • The fix is to balance ownership explicitly: retain the object for the C API's benefit (commonly via (__bridge_retained void *)object, releasing later with (__bridge_transfer id)context when the API hands it back for the last time) whenever the API's lifetime isn't provably bounded by your own scope. • This exact hazard — and its fix — is the main subject of the sibling lesson 'Objective-C + C Callback Interoperability'.

The void *context Round-Trip, in Outline

(__bridge void *)self on the way in and (__bridge MyClass *)context on the way out must agree on what object is really there — the compiler cannot check this for you. Neither __bridge cast changes self's retain count; lifetime safety depends entirely on some other strong reference keeping the object alive until the callback can no longer fire. If the C API might outlive the registering scope, use __bridge_retained going in and __bridge_transfer coming out (in the unregister/teardown path) to balance ownership explicitly instead of relying on an incidental external strong reference.

// 1. Register with a C API, passing an object as void * context
SomeCAPIRegister(myCCallback, (__bridge void *)self);

// 2. The C callback receives it back as a plain void *
static void myCCallback(int event, void *context) {
    // 3. Cast it back to the real Objective-C type before using it
    MyClass *me = (__bridge MyClass *)context;
    [me handleEvent:event];
}

Code Examples

allocate_buffer takes char **outBuffer so it can write a newly allocated pointer directly into the caller's own buf variable. Passing &buf supplies the address of that variable; *outBuffer = malloc(size) then writes through that address. Expected result: (no printed output — buf holds a valid 128-byte pointer after the call) void allocate_buffer(char **outBuffer, size_t size) {

&writeError passes the address of the local NSError *writeError variable. Inside writeToFile:options:error:, the method can write a new NSError * through that address, which is why writeError holds a real error object after the call even though it started as nil.

self is passed in as (__bridge void *)self and recovered as (__bridge TemperatureMonitor *)context. This is safe here specifically because TemperatureMonitor retains itself elsewhere (e.g. a strong property on its owner) for the full lifetime during which RegisterSensorCallback might fire.

TemperatureMonitor *monitor = (__bridge TemperatureMonitor *)context;

The unsafe cast trusts the programmer with no verification; if someObject is actually, say, an NSNumber, sending it length is undefined behavior. The isKindOfClass:-guarded version checks first and only proceeds down the cast-and-use path once the type is confirmed.

if ([someObject isKindOfClass:[NSString class]]) { Common mistake: Casting (NSString *)someObject and calling length without first checking isKindOfClass: when the object's real type is not statically guaranteed.

temp has no strong owner beyond the local variable temp itself, which disappears when MakeShortLivedRegistration returns. (__bridge void *)temp never retained it, so by the time the C API invokes myCallback, context very likely points to freed memory.

// temp goes out of scope here with no other strong reference --

Common mistake: Passing (__bridge void *)temp for an object with no other strong reference, then letting the registering scope end before the C API is done calling back.

#include <stdlib.h>

void allocate_buffer(char **outBuffer, size_t size) {
    *outBuffer = malloc(size);
}

int main(void) {
    char *buf = NULL;
    allocate_buffer(&buf, 128);
    // buf now points at 128 freshly allocated bytes
    free(buf);
    return 0;
}
NSError *writeError = nil;
BOOL ok = [someData writeToFile:@"/tmp/out.dat"
                        options:0
                          error:&writeError];
if (!ok) {
    NSLog(@"Write failed: %@", writeError.localizedDescription);
}
typedef void (*SensorCallback)(double value, void *context);

void RegisterSensorCallback(SensorCallback cb, void *context);

static void temperatureCallback(double value, void *context) {
    // (__bridge id) does not retain -- it only reinterprets the pointer
    TemperatureMonitor *monitor = (__bridge TemperatureMonitor *)context;
    [monitor recordTemperature:value];
}

@implementation TemperatureMonitor
- (void)start {
    RegisterSensorCallback(temperatureCallback, (__bridge void *)self);
}
@end
// Unsafe: no runtime verification at all
NSString *unsafeName = (NSString *)someObject;
[unsafeName length]; // crashes or misbehaves if someObject isn't really an NSString

// Safe: verified before the cast is trusted
if ([someObject isKindOfClass:[NSString class]]) {
    NSString *safeName = (NSString *)someObject;
    NSLog(@"Length: %lu", (unsigned long)[safeName length]);
} else {
    NSLog(@"someObject was not an NSString");
}
void MakeShortLivedRegistration(void) {
    MyClass *temp = [[MyClass alloc] init];
    RegisterSensorCallback(myCallback, (__bridge void *)temp);
    // temp goes out of scope here with no other strong reference --
    // ARC deallocates it, but the C API still has its address as context
}

static void myCallback(double value, void *context) {
    MyClass *obj = (__bridge MyClass *)context; // context may be dangling
    [obj recordTemperature:value]; // use-after-free if obj was deallocated

Pointer-to-Pointer and the void *context Round-Trip

Top: a pointer-to-pointer lets a callee write a new value into a variable that lives in the caller's frame. Bottom: __bridge casts move an object's address into a C API's void *context and back, with no retain/release happening at either boundary.

POINTER-TO-POINTER (NSError **error)

  caller's stack           the callee writes through
  +----------------+       the address it was given
  | error: 0x1000   |----->  +----------------+
  +----------------+          | 0x1000: NULL   |  <- before call
                               +----------------+
                                      |
                          callee does *error = newErr
                                      v
                               +----------------+
                               | 0x1000: 0x9000 |  <- after call: points at NSError
                               +----------------+

void *context ROUND-TRIP THROUGH A C CALLBACK

  Objective-C object            C API storage           C callback
  +----------------+   __bridge   +---------+  invokes  +------------------+
  | MyClass @ 0x9000|----void*--->| context |---------->| void *context    |
  | (still owned by |   (no       | = 0x9000|           | == 0x9000        |
  |  something else)|  retain)    +---------+           +------------------+
  +----------------+                                            |
                                                     (__bridge MyClass *)context
      
…

Important Rules

Use a pointer-to-pointer (T ) parameter whenever a function needs to write a new pointer value back into a variable owned by its caller, such as an NSError out-parameter.

Treat void * as carrying zero type information — never dereference it directly; cast it to a real type first, and only after you're certain what that type actually is.

Use (__bridge void *)object to pass an Objective-C object into a C API's void *context slot without affecting its retain count, and the matching (__bridge SomeClass *)context to recover it.

Prefer isKindOfClass:-checked casts over bare unsafe casts whenever an object's exact runtime type is not already statically guaranteed by the surrounding code.

Whenever a C API might invoke a callback after your registering scope has ended, guarantee the context object's lifetime explicitly — via an existing strong owner or via __bridge_retained/__bridge_transfer — rather than assuming it will still be alive.

Why This Matters in Real Frameworks

Apple's own C-level frameworks lean on exactly these patterns throughout: Core Audio's AudioQueueNewOutput takes a callback function pointer plus a void *inUserData context; Core Foundation's run-loop source and observer APIs take void *info; even NSNotificationCenter's lower-level C equivalents and libdispatch's dispatch_set_context/dispatch_get_context pair follow this identical void-pointer-context idiom. Any time you see Objective-C code casting self through __bridge void * on the way into a C function whose name doesn't start with NS, you are looking at exactly the pattern this lesson covers, and the lifetime discipline from the final section is the single most common source of real-world crashes at that boundary.

RegisterSensorCallback(cb, (__bridge void *)[[MyClass alloc] init]); // object has no other owner self.monitor = [[MyClass alloc] init]; // strong property keeps it alive RegisterSensorCallback(cb, (__bridge void *)self.monitor);

Passing a freshly allocated object via __bridge with no other strong reference to it means ARC is free to deallocate it the moment the current scope ends, since __bridge never retains. The C API then holds a dangling context pointer, and the next callback invocation reads freed memory — a use-after-free. Ensuring a real owner (here, a strong property) keeps the object alive for as long as the C API might call back fixes it.

What does a pointer-to-pointer parameter like NSError **error primarily let a function do? A. Point to two different NSError objects at once B. Write a new pointer value into a variable that belongs to the caller, since C passes arguments by value C. Automatically retain the NSError object twice D. Skip runtime type checking on the error object

B. Write a new pointer value into a variable that belongs to the caller, since C passes arguments by value — Because C passes everything by value, a function needs the address of the caller's variable (a pointer-to-pointer) in order to write a new value into it and have that change visible after the call returns.

Why can't you dereference a void * directly? A. void * is always NULL B. It's a syntax error in every C compiler C. void * carries no type information, so the compiler doesn't know how many bytes to read or how to interpret them D. Only Objective-C objects can be dereferenced

C. void * carries no type information, so the compiler doesn't know how many bytes to read or how to interpret them — void * intentionally erases the pointed-to type; without a real type, the compiler has no basis for deciding how many bytes constitute one value or how to interpret those bytes.

What does (__bridge void *)someObject do to someObject's retain count? A. Increments it by one B. Decrements it by one C. Leaves it completely unchanged D. Sets it to zero, deallocating the object immediately

C. Leaves it completely unchanged — __bridge performs a pure type reinterpretation with no memory-management side effects at all — the retain count is exactly what it was before the cast.

An unsafe cast like (SomeType *)ptr, compared to an isKindOfClass:-guarded cast, differs in what way? A. It performs the same runtime check but faster B. It performs no runtime verification at all, so a wrong cast fails later and unpredictably rather than being caught at the cast site C. It only works on Core Foundation types D. It automatically converts the object to the correct type if it's wrong

B. It performs no runtime verification at all, so a wrong cast fails later and unpredictably rather than being caught at the cast site — An unsafe cast simply reinterprets the pointer's compile-time type with zero verification; if the assumption is wrong, the failure surfaces later, when the mistyped memory is actually used, not at the cast itself.

Why can passing (__bridge void *)self into a C callback registration lead to a use-after-free? A. Because __bridge always deallocates the object immediately B. Because __bridge performs no retain, so if nothing else keeps self alive, ARC may deallocate it before the C API's callback fires C. Because C callbacks can never receive Objective-C objects D. Because void * pointers expire after one use

B. Because __bridge performs no retain, so if nothing else keeps self alive, ARC may deallocate it before the C API's callback fires — Since __bridge does not retain, the object's survival depends entirely on some other strong reference. If none exists for as long as the C API might still call back, the context pointer can dangle, producing a classic use-after-free.

Write a small C API surface (a struct-free header is fine) with a registration function void RegisterClickHandler(void (*handler)(int x, int y, void *context), void *context); and a matching static C callback function. In an Objective-C class, register the callback passing (__bridge void *)self as context, and implement the callback so it casts the context back to your class and forwards to an instance method - (void)handleClickAtX:(int)x y:(int)y;. Then deliberately introduce the use-after-free bug from this lesson by registering with a temporary object that has no other owner, and explain in a comment exactly why it's unsafe.

A correct version keeps a strong reference (e.g. a property on some longer-lived owner, or a singleton) to the object passed as context, ensured to be released only after the C API is explicitly told to stop calling back (an 'unregister' call) — matching the pattern the sibling callback-interoperability lesson develops in full.

Focus on where the strong reference to the context object actually lives — is it the same scope as the registration call, or does it genuinely outlive that scope? If you can't point to a specific strong owner that outlives the possible callback window, the code is unsafe regardless of how it looks.