How NSFastEnumeration Facilitates for…in Loops in MulleObjC

NSFastEnumeration provides the countByEnumeratingWithState:objects:count: method that the MulleObjC compiler uses to transform for (id obj in collection) syntax into a high-performance while loop that batches object pointers without retaining them.

The mulle-objc/mulleobjc repository implements standard Objective-C fast enumeration through the NSFastEnumeration protocol. This mechanism allows any container class to expose its contents to the compiler’s for…in syntax by implementing a single state-driven batching method.

The NSFastEnumeration Protocol and State Structure

In src/protocol/NSFastEnumeration.h, MulleObjC declares the protocol and the NSFastEnumerationState struct that drives the iteration:

typedef struct {
    unsigned long state;
    id *itemsPtr;
    unsigned long *mutationsPtr;
    unsigned long extra[5];
} NSFastEnumerationState;

@protocol NSFastEnumeration
- (NSUInteger) countByEnumeratingWithState:(NSFastEnumerationState *)state
                                 objects:(id *)buffer
                                   count:(NSUInteger)len;
@end

The state field acts as a cursor (often initialized to 0), while itemsPtr points to a C array of id objects that the container fills. The mutationsPtr field references a mutation counter; if the collection changes during enumeration, the runtime can detect the mismatch and raise an exception. The header comments explicitly note that objects returned through this protocol are not retained, eliminating retain/release overhead inside the loop.

How the Compiler Expands for…in Syntax

When the MulleObjC compiler encounters for (id obj in container), it generates code equivalent to the following pattern:

  1. State initialization – The compiler allocates an NSFastEnumerationState on the stack and zeroes it. On the first call to countByEnumeratingWithState:objects:count:, the container sets state->itemsPtr to its internal buffer (or the supplied buffer), stores a cursor in state->state, and assigns state->mutationsPtr to point to a mutation counter.

  2. Batch fetching – The container copies up to len object pointers into the buffer and returns the actual count. A return value of 0 terminates the loop.

  3. Iteration – The generated while loop walks the buffer returned in state->itemsPtr, assigning each element to the loop variable. Because the container vends direct pointers to its storage, the loop avoids per-object messaging overhead.

  4. Mutation guards – If the container is modified during enumeration, the value at *state->mutationsPtr changes, allowing the runtime to throw an NSGenericException to prevent undefined behavior.

Fast Enumeration in Built-in Collections

Core collection classes in MulleObjC adopt the protocol in src/protocol/NSContainer.h and provide concrete implementations in their respective class files.

  • src/class/NSArray.m implements the method to vend elements from its contiguous storage.
  • src/class/NSDictionary.m enumerates keys (or key-value pairs depending on the internal representation).
  • src/class/NSSet.m returns pointers to its hashed object storage.

Usage requires no special syntax beyond the standard for…in pattern:

#import <MulleObjC/NSArray.h>

NSArray *names = @[ @"Alice", @"Bob", @"Charlie" ];

for (id name in names) {
    printf("%s\n", [(NSString *)name UTF8String]);
}

Under the hood, the call to countByEnumeratingWithState:objects:count: happens automatically, fetching objects in batches sized to the stack buffer provided by the compiler.

Implementing Custom Fast Enumeration

Any class can support for…in by adopting NSFastEnumeration and maintaining its own cursor. The following example from the MulleObjC source patterns demonstrates a minimal wrapper around a C array:

/* MyContainer.h */
@interface MyContainer : NSObject <NSFastEnumeration>
{
    id   *_storage;
    NSUInteger _count;
}
- (instancetype) initWithObjects:(id *)objects count:(NSUInteger)cnt;
@end

/* MyContainer.m */
@implementation MyContainer

- (instancetype) initWithObjects:(id *)objects count:(NSUInteger)cnt
{
    self = [super init];
    if (self) {
        _count = cnt;
        _storage = calloc(cnt, sizeof(id));
        memcpy(_storage, objects, cnt * sizeof(id));
    }
    return self;
}

- (NSUInteger) countByEnumeratingWithState:(NSFastEnumerationState *)state
                                 objects:(id *)buffer
                                   count:(NSUInteger)len
{
    /* First call initialization */
    if (state->state == 0) {
        state->state = (unsigned long)self;   // Store container reference
        state->mutationsPtr = &state->extra[0]; // Dummy mutation tracker
        state->extra[0] = 0;
    }

    NSUInteger idx = (NSUInteger)state->state;
    if (idx >= _count) {
        return 0;  // Enumeration complete
    }

    NSUInteger remaining = _count - idx;
    NSUInteger batch = (remaining < len) ? remaining : len;

    for (NSUInteger i = 0; i < batch; i++) {
        buffer[i] = _storage[idx + i];
    }

    state->itemsPtr = buffer;
    state->state = idx + batch;  // Advance cursor
    return batch;
}

@end

With this implementation, the container works seamlessly with fast enumeration:

id data[] = { @1, @2, @3 };
MyContainer *box = [[MyContainer alloc] initWithObjects:data count:3];

for (id item in box) {
    NSLog(@"Value: %@", item);
}

Summary

  • NSFastEnumeration in src/protocol/NSFastEnumeration.h defines the single required method that powers for…in loops.
  • The NSFastEnumerationState struct tracks iteration progress, provides a pointer to the current batch of objects, and monitors for mutations via mutationsPtr.
  • Built-in collections (NSArray, NSDictionary, NSSet) implement the protocol in src/class/NSArray.m, src/class/NSDictionary.m, and src/class/NSSet.m, enabling zero-overhead enumeration.
  • Custom containers adopt the protocol and implement batching logic inside countByEnumeratingWithState:objects:count: to support the same syntax without retaining enumerated objects.

Frequently Asked Questions

How does NSFastEnumeration improve performance compared to NSEnumerator?

NSFastEnumeration batches object pointers into a stack buffer and avoids per-object method calls. Unlike NSEnumerator, which requires a message send for every nextObject call, fast enumeration supplies multiple items in a single C array, reducing message overhead and eliminating retain/autorelease traffic for the enumerated items.

What happens if a collection is modified during fast enumeration?

The container sets state->mutationsPtr to point at a mutation counter before returning objects. If the underlying collection changes (e.g., an item is added or removed), the counter increments. The MulleObjC runtime compares this value between batches and raises an NSGenericException if a mutation is detected, preventing inconsistent iteration state.

Can I use fast enumeration with custom C data structures?

Yes. As long as your class adopts the NSFastEnumeration protocol and implements countByEnumeratingWithState:objects:count:, you can vend any data structure—C arrays, linked lists, or custom hash tables—through the for…in syntax. The protocol is storage-agnostic; it only requires that you fill the provided buffer with id pointers and track progress via state->state.

Does the for…in loop retain the objects it enumerates?

No. According to the header comments in src/protocol/NSFastEnumeration.h, objects returned through the itemsPtr buffer are not retained by the fast enumeration machinery. This design keeps enumeration lightweight, but it means the container must ensure the objects remain valid for the duration of the batch (typically guaranteed because the container itself is retained by the caller for the lifetime of the loop).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →