dev etc

Where mistakes go to die.

Compile-time checking plain key path strings

Safe and sane key paths.
Published on Saturday, 2014-05-17.

Key–value coding — valueForKeyPath: and setValue:forKeyPath: — is very useful for converting data from one format to another, such as copying from a property list-like structure (e.g. deserialized JSON) into model objects proper. This avoids a lot of boilerplate typically found in, for example, Java-based systems1.

On the Mac, with Cocoa Bindings you can throw together a simple UI in a flash, setting up key paths in Interface Builder. On iOS you have to provide some of the glue yourself, but you typically let key–value observing do the hardest work.

The problem

These “key–value” techniques have one main thing in common: They use strings to reference code. This makes it fragile. For example:

@implementation ContrivedExample
- (NSString *)title
{ return @"foo"; }

- (NSUInteger)contrivedLength
{ return [[self title] length]; }

- (NSUInteger)contrivedLengthKVC
{ return [[self valueForKey:@"title"] length]; }
@end

If we rename the method title, the compiler will immediately show that contrivedLength won’t work without changes. But contrivedLengthKVC will have no such warning, because @"title" remains a perfectly valid string. We would only see an error at run-time, if and when that code was triggered. Of course in real code, string references like this appear much further apart than in the above snippet, making stale references and even typos a real problem.

The common solution

In short: The compiler does not validate key paths, because they are strings.

One approach toward validation is avoid strings2. Of course, a string must emerge at some point, but pre-processor macros allow using the same expression (title) as both code and data, at compile-time. Many have taken this route, resulting in a large variety3 with different trade-offs. A couple that are representative:

  1. With libextobjc you have to specify the target of the key path twice — [self valueForKey:@keypath(self.title)] above — though this is often redundant, and obscures that the resulting string will be simply “title”.
  2. DMSafeKVC is more concise at use — [self valueForKey:K(title)] — but require annotating declarations, and only checks that some class has an annotated “title” accessor.

Compiler error messages are typically obscured due to macro expansion. And if you’re writing library code4, often the best approach is to use the lowest common denominator — static strings.

A few months back, my colleagues at Fitbit discussed the ups and downs of various macro implementations and didn’t arrive at consensus. Additionally, retrofitting our codebase would be a mostly manual task and likely generate many conflicts (our repository has around 10 active committers).

I began to consider a different approach.

The code less traveled

The compiler does not validate key paths, because they are strings. In our phrasing of the problem, a second solution becomes apparent: make the compiler validate the strings!

Key path warnings in Xcode

In the past, touching the compiler has been considered off-the-table for various reasons. With clang, this is now more practical5, but the previous stigma still lingers.

This code is currently proof-of-concept quality. (I’m only a C++ novice, and barely familiar with the clang codebase.) It currently:

The compiler plug-in doesn’t do everything that some of the macro approaches do, but has the giant advantage of requiring zero code changes and no dependencies. It also doesn’t exclude the use of macros, either. Only one small project setting change is required:

Key path warnings in Xcode

If you’d like to try this with your own project, look at the Clang-KeyPathValidator README file. Then check out the GitHub Issues page and see if you can help out with something. Or just help get the word out!

  1. This opinion is several years old, before Java reflection was commonly available or efficient.

  2. The term “stringly-typed” is just great.

  3. “Library code” refers to code shared between projects which might use different safe KVC techniques, or none at all.

  4. It’s unfortunately still not completely painless, because the particular versions shipped by Apple with Xcode are not made publicly available, and have plug-in support disabled.