🍫 CocoaPatterns — iOS & Mac Development Since 2009 RSS
🍫 CocoaPatterns
Because someone has to write this stuff down
"We ship apps. Sometimes they even work."

Macro-Driven Development: Write Code That Writes Code That You Can't Read

Why type the same boilerplate 200 times when a C preprocessor macro can type it for you incorrectly once?

EB
Elliot Baxter
@ebaxter_dev · CocoaPatterns

Boilerplate is the enemy of productivity. Every UIViewController subclass has the same skeleton: viewDidLoad, viewWillAppear, delegate conformances, property declarations, the same six lines of navigation bar setup. You write it once, it's fine. You write it fifty times, you start questioning your career. You write it two hundred times, you start looking for a way out.

I found a way out. It's the C preprocessor, and it's been waiting for us since 1972.

"The best code is code you don't write. The second-best code is code a macro writes for you. The worst code is code a macro writes for you that doesn't compile and the error message points to a line number that doesn't exist in your source file."

The Macro Library

Macro-Driven Development (MDD) replaces hand-written boilerplate with a library of C preprocessor macros that generate Objective-C code at compile time. The macros live in a single header file called MDD.h. It's 2,400 lines long. It is, by a significant margin, the most important file in our project, and it is completely unreadable.

// MDD.h — The Macro Bible (excerpt)

#define MDD_PROPERTY(TYPE, NAME) \
    @property (nonatomic, strong) TYPE *NAME;

#define MDD_LAZY(TYPE, NAME, INIT) \
    - (TYPE *)NAME { \
        if (!_##NAME) { _##NAME = INIT; } \
        return _##NAME; \
    }

#define MDD_SINGLETON(CLASS) \
    + (instancetype)shared##CLASS { \
        static CLASS *instance; \
        static dispatch_once_t onceToken; \
        dispatch_once(&onceToken, ^{ \
            instance = [[CLASS alloc] init]; \
        }); \
        return instance; \
    }

#define MDD_DELEGATE_CONFORM(...) \
    /* Variadic macro that generates protocol conformance
       declarations. The ... is doing a LOT of heavy lifting. */

#define MDD_VIEWCONTROLLER_FULL(NAME, ...) \
    /* This macro is 340 lines long. It generates an entire
       view controller including viewDidLoad, viewWillAppear,
       viewWillDisappear, dealloc, three delegate conformances,
       a table view data source, and a networking call.
       It accepts 14 parameters. Nobody remembers what
       parameter 9 does. We think it's a color. */
💡 Key Principle

A well-designed macro library means your source files look clean and minimal. A 400-line view controller becomes a 12-line file with three macro invocations. The complexity isn't gone — it's in MDD.h. And MDD.h is like a black hole: dense, powerful, and terrifying to look at directly.

What a Source File Looks Like

// UserProfileViewController.m
#import "MDD.h"

MDD_VIEWCONTROLLER_FULL(UserProfile,
    MDD_TABLE_VIEW,
    MDD_NAVIGATION_BAR_STYLE_DARK,
    MDD_DELEGATE_CONFORM(UITableViewDelegate, UITableViewDataSource),
    MDD_NETWORKING_CLIENT(UserAPI),
    MDD_ANALYTICS_TRACKING("user_profile"),
    /* param 7: */ YES,
    /* param 8: */ 44.0,
    /* param 9: */ [UIColor redColor], // probably a color
    /* param 10: */ 0,
    /* param 11-14: */ nil, nil, nil, nil
)

This generates 847 lines of Objective-C. The view controller has a table view, a dark navigation bar, networking integration, analytics, and four nil parameters that we're afraid to remove because the last time someone removed a nil parameter, the macro generated a dealloc that called [super viewDidLoad] and the app crashed in a way that took two days to diagnose.

The Debugging Experience

When a macro-generated line crashes, the debugger shows the line number from the expanded code, which doesn't match any line in your source file. You see "crash at UserProfileViewController.m:847" but your file is 12 lines long. Where is line 847? It's in the preprocessor's imagination. It's in the expanded output that exists only during compilation.

To debug MDD code, you must first run the preprocessor manually (clang -E) to generate the expanded source, then find line 847 in a 12,000-line wall of generated Objective-C, then trace it back to which macro parameter caused the issue. This process takes between 20 minutes and 3 hours, depending on how deeply nested the macros are.

The Nesting Problem

Macros can invoke other macros. MDD macros frequently do. MDD_VIEWCONTROLLER_FULL expands to code that uses MDD_LAZY, which uses MDD_PROPERTY, which uses MDD_IVAR. A single top-level macro can trigger a chain of 12 expansions. If any macro in the chain has a typo, the error message references the outermost macro and says something like "expected ';' after expression." There are 340 lines of generated expressions. Which one is missing the semicolon? Good luck. We've started calling this "macro archaeology."

⚠️ The MDD.h Edit Freeze

After an incident where a one-character change to MDD.h caused 23 compilation errors across 14 files, we instituted an edit freeze on the macro library. Changes to MDD.h now require a code review from two senior engineers and a 48-hour waiting period. We call this "macro governance." It's the most bureaucracy we've ever applied to a single file and it's still not enough.

Results

  • Average source file length: 15 lines (down from 350)
  • Average expanded file length: 900 lines (up from 350)
  • Time to create a new view controller: 2 minutes (just add the macro call)
  • Time to debug a new view controller: 2 hours (find the macro expansion, understand it, cry)
  • Engineers who can modify MDD.h: 1 (me, and I'm scared every time)
  • Compilation time: up 40% (preprocessor expansion is not free)
  • Lines in MDD.h: 2,400 (and growing)
  • Comments in MDD.h: 12

MDD is the best and worst thing I've ever built. It saves hours of typing and costs hours of debugging. The net productivity change is approximately zero. But the feeling of writing a 12-line file that generates an entire view controller? That's worth it. That's power. That's the preprocessor working as God and Dennis Ritchie intended.

4 Comments — Powered by Disqus
BK
bradkow_ios · 3 days ago
I expanded MDD_VIEWCONTROLLER_FULL and it generated 847 lines of Objective-C. I read all 847 lines. I understand none of them. The app works.
TW
twren_fp · 2 days ago
Elliot, the C preprocessor is a textual substitution engine from 1972. You're building an application architecture on top of a find-and-replace from the Nixon administration.
EB
ebaxter_devAuthor · 2 days ago
@twren_fp The Nixon administration also gave us the EPA. Sometimes questionable origins produce lasting institutions.
MO
mel_ios_eng · 1 day ago
The compiler error on line 1 that says 'expected expression' but the actual error is in a macro defined in a different file that expands to code referencing a third macro is my personal hell.