Inside the Bracket, part 6 - Using the Runtime API

Mark Dalrymple's Headshot
Mark Dalrymple

Last time you saw the parts of the Objective-C runtime API that let you query a bunch of the metadata the compiler keeps around once it's done building your project. Like most discussions of API, it was pretty abstract. "Look at all the pretty tools! Ooh, we can print out Lists of Stuff! Isn't that amazing!"

This time around is an actual application of this API for Great Justice. I was working on a client project that consumed blobs of JSON data from a third-party web service. The data was pretty simple, just a dictionary of key/value pairs we used to update our model. We constructed our model such that the keys matched property names in our model objects.

For the most part. There were chunks of data they sent us that were kind of ugly, name-wise (it made sense to them...). For those properties so we changed the ivars that backed the property to match the ugly name. Say a typical bolus of data looked like this:

{ name : "<a href="http://cuteoverload.com/2006/09/12/xtreme_gluttony/">Moose McGrapersons</a>",
  grapeSize: "large",
  d813n_creatre_Type : "hamster" }

Our model looked something like this:

@interface Creature : NSObject
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *grapeSize;
@property (copy, nonatomic) NSString *creatureType;
@end

@implementation Creature
@synthesize creatureType = d813n_creatre_Type;
@end

This way we can take the JSON dictionary and call setValuesForKeysWithDictionary:

NSDictionary *newStuff = ...;
Creature *critter = [Creature new];
[critter setValuesForKeysWithDictionary: newStuff];

We could use pretty property names in our code, and the occasional weird web service name got mapped transparently by Key/Value Coding. That setValuesForKeys call walks through the keys and values of the dictionary and does the equivalent of

[critter setValue: newStuff[key]  forKey: key];

KVC will automatically fall back to setting instance variables for the d813n_creatre_Type case. Life is beautiful.

That is, until the web service decides to throw in some random new values.

{ name : "Moose McGrapersons",
  grapeSize: "large",
  d813n_creatre_Type : "hamster",
  tracking_ident : "all your base" }

There is no property on Creature called tracking_ident. Feeding this dictionary to setValuesForKeysWithDictionary causes KVC to gripe about the last one. "Sorry, Creature is not KVC compliant for the key tracking_ident". It throws an exception and your app will probably terminate.

You can guess that I ran into this. I didn't control the web service, they added a new key, and suddenly a prototype the client was experimenting with went kablooey. It was entirely my fault, though, for not being more careful with KVC.

There's ways to fix this, of course, like wrapping an exception handler around the bulk setValuesForKeys, or override setValue:forUndefinedKey: and consume the misdirection. I don't like those two options because they eat errors. I might have a legitimate bug that sent a bad key to setValue:forKey: which could get masked.

I remembered my friend Tom Harrington of Atomic Bird had opensourced some code that used the Objective-C runtime to do a safe bulk-set of values. I snarfed it (making sure the license was ok), tweaked it and added support for our use of odd variable names. Now no more criminal death to the face courtesy of the web service. Tom was kind enough to let me modify his code and dissect it in public, so here it is.

The Approach

This gist has similar code to what we used for the previously mentioned project. There's two objects involved. The "target" is the model object with properties that will have its values changed by the incoming dictionary. The "incoming dictionary" is the one with key/value pairs, supplied through JSON, with the new data to use.

There's two approaches you could take. One is to walk the incoming dictionary, see if there are any corresponding properties or instance variables defined in the target's class (and superclasses). Can't find any? Throw it on the floor. The other is to walk the properties in the class and superclasses and see if any of them are in the incoming dictionary. The bad values aren't even considered.

If you had a plethora of properties, the first approach could be more efficient, being order dictionary.count. If the target had a small number of properties, or the size of the incoming dictionary is large, the second approach could be more efficient, being order number-of-properties involved. Either way, it's kind of a wash because the network transmission times would completely dwarf the time spent setting the values.

The approach Tom and I took is to walk the classes and look at the properties. You can copy out All The Things and then just scan through them.

The code is in a category on NSObject, adding two calls:

- (void) xx_setValuesForKeysWithJSONDictionary: (NSDictionary *) keyedValues
                                 dateFormatter: (NSDateFormatter *) dateFormatter;

- (void) xx_setValuesForKeysWithJSONDictionary: (NSDictionary *) keyedValues;

If you have dates (as strings) coming back from your web service you can use the first form and supply a date formatter configured to convert those strings into NSDate objects. If you don't have dates, you can use the second form, which just turns around and calls the two-parameter version with a nil date formatter.

The Driving Loop

Here's what the two main loops look like. Objective-C runtime calls, whether from the low-level API or NSObject wrappers, are bolded.

- (void) xx_setValuesForKeysWithJSONDictionary: (NSDictionary *) keyedValues
                                 dateFormatter: (NSDateFormatter *) dateFormatter {

    // Walk the current class, as well as superclasses.  The property or ivar
    // to set might be up in the inheritance chain.
    for (Class clas = [self <strong>class</strong>]; clas; clas = <strong>class_getSuperclass</strong>(clas)) {

        // Walk the properties seeing if any of the properties appear in
        // the dictionary of JSON values.
        unsigned int propertyCount;
        objc_property_t *properties = <strong>class_copyPropertyList</strong>(clas, &propertyCount);
        for (unsigned int i = 0; i < propertyCount; i++) {
            objc_property_t property = properties[i];

So you get the target's Class object. Remember that the Class is the selector:method mapping plus all the metadata, and the object's isa pointer references it. The target's class and all of its super classes are processed.

Once you have a Class in hand, get its list of properties. This does limit the automatic setting to things explicitly declared with @property. It doesn't try to look at arbitrary method names. This is a fair trade-off because @properties are so convenient that we use them everywhere.

Finding the Property

Once you have that list, iterate through each of the properties and see if the incoming dictionary has that key. If so, fetch the value. First get the property name:

NSString *keyName = @( <strong>property_getName</strong>(property) );
id value = [keyedValues objectForKey:keyName];

Notice the use of the Objective-C literal syntax for making the key name. The Objective-C runtime API lives below the level of objects so we can't expect it to know what an NSString is. property_getName returns a C string. The @( ) expression, if given a string, will convert it to an NSString using NSUTF8StringEncoding. Be aware that @( ) will throw an exception if the expression inside of it is of a string (char *) type and evaluates to NULL. Properties have to have names, so the boxing of the value directly is safe.

If you get a value, great! That means the web service sent along the property name. What about the weird-name using the backing instance variable? You can ask the property for the name of its backing instance variable. Snarf that and look inside the incoming dictionary:

if (value == nil) {
    // Pull the ivar name out of the property.
    char *ivarPropertyName = <strong>property_copyAttributeValue</strong>(property, "V");

    // Make sure we're not dealing with a @dynamic property
    if (ivarPropertyName != NULL) {
        // See if that lives in the incoming dictionary.
        NSString *ivarName = @( ivarPropertyName );
        value = [keyedValues objectForKey: ivarName];
    }
    free (ivarPropertyName);
}

Remember property_getAttributes from last time? It returned a string, comma-separated, with different parts of it looking like Td,N,V_gilliganFactor. You can ask the property for a particular chunk of that string using property_copyAttributeValue. In this case, asking for the "V" value returns you the name of the backing ivar, if any. Also recall that any runtime call that has "copy" in the name returns dynamically allocated memory and it's your responsibility to free it.

So, at this point in time, you either have a value that you've looked up in the incoming dictionary via property name, or if that didn't exist, via ivar name. If there's no value, this property doesn't exist in the incoming dictionary. If so, skip to the next property:

if (value == nil) {
    // This property doesn't have anything in the incoming dictionary.
    continue;
}

Processing the Value

So, yay, you now have an object value from the dictionary. Time to think what can go wrong. What if you have a numeric property, say a shoe size, and the incoming dictionary has a string that happens to look like a number? Will anything bad happen if you try to setValue:forKey: this string into a numeric field?

Experimentally, it looks like KVC is converting between strings and numbers transparently. I asked Uncle Google and trolled through headers and docs and didn't find the description of "here is exactly what is being done to convert between types under these circumstances." Rather than be bitten by a bug in a later version of the OS, it's time to be a little defensive, and be explicit in exactly how you're doing to convert types. If the type of the value you got out of the incoming dictionary doesn't jive with the type expected by the property, convert it.

How to get the type of the property? Get its "T" property for type encoding:

char *typeEncoding = <strong>property_copyAttributeValue</strong>(property, "T");

// No type available, so we're done.
if (typeEncoding == NULL) {
    continue;
}

Take a look at the first character to see the type. Handle objects first.

switch (typeEncoding[0]) {
case '@': {

The property type encoding can include a class name, being of the form T@"NSURL". Pull the class name out of it, and find a Class with that name.

Class propertyClass = nil;
size_t typeEncodingLength = strlen(typeEncoding);
if (typeEncodingLength >= 3) {
    char *className = strndup (typeEncoding + 2,         // skip over @"
                               typeEncodingLength - 3);  // trim off trailing "
    propertyClass = NSClassFromString (@(className));
    free (className);  // The strdup family allocates memory
}

Now that you (might) know what kind of class is the destination, time to handle the cases. If the property is a string and the incoming value is a number, convert it to a string:

if ([propertyClass <strong>isSubclassOfClass</strong>:[NSString class]]
    && [value <strong>isKindOfClass</strong>:[NSNumber class]]) {
    // number converted into a string.
    value = [value stringValue];

isSubclassOfClass compares two classes for their relationship. It then uses isKindOfClass to compare an instance's class to some other class.

Another case is the property type being an NSNumber and the incoming value is a string, then try to convert it using NSNumberFormatter. NSNumberFormatter instances are expensive to create, so if you were calling this method very often, you might want to allocate one and reuse it.

} else if ([propertyClass <strong>isSubclassOfClass</strong>:[NSNumber class]]
           && [value <strong>isKindOfClass</strong>:[NSString class]]) {
    // String converted into a number.  We can't tell what its
    // intention ls (float, integer, etc), so let the number
    // formatter make a best guess for us.
    NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
    [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
    value = [numberFormatter numberFromString:value];

The last object case is if the property is of an NSDate type. Run it through the date formatter. If there is no date formatter, or the date couldn't be parsed, the value is nil.

} else if (dateFormatter
           && [propertyClass <strong>isSubclassOfClass</strong>:[NSDate class]]
           && [value <strong>isKindOfClass</strong>:[NSString class]]) {
    // If the caller provided a date formatter, try converting
    // the date into a string.
    value = [dateFormatter dateFromString:value];

And of course there's a break at the end of this case.

The next case are the numeric types. If the incoming value is a string, convert it to an NSNumber using NSNumberFormatter:

case 'i': // int
case 's': // short
case 'l': // long
case 'q': // long long
case 'I': // unsigned int
case 'S': // unsigned short
case 'L': // unsigned long
case 'Q': // unsigned long long
case 'f': // float
case 'd': // double
case 'B': // BOOL
    if ([value <strong>isKindOfClass</strong>:[NSString class]]) {
        NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
        [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
        value = [numberFormatter numberFromString:value];
    }
    break;

And finally handle characters:

case 'c': // char
case 'C': // unsigned char
    if ([value <strong>isKindOfClass</strong>:[NSString class]]) {
        char firstCharacter = (char)[value characterAtIndex:0];
        value = [NSNumber numberWithChar:firstCharacter];
    }
    break;

So, now that the different corner cases have been covered, and there's an incoming value, set it on the object:

if (value) {
    [self setValue: value  forKey: keyName];
}

It's OK to pass nil to setValue:forKey:, but that has the side effect of wiping out the previous value in the object. In this case, the code is deciding to preserve the existing value, assuming that there's just something insane with the value provided by the web service.

Finally, clean up stuff gotten from from the runtime API, and you're done.

            free(typeEncoding);
        } // end of for-each-property loop
        free(properties);
    } // end of for-class-and-superclasses loop
} // setValues with dateFormatter

And that's it! You can see how the application of a little bit of lower-level knowledge has given us convenient and safe bulk-setting of object properties.

Next Time

Tune in next time - wrapping it up with the other part of the runtime API: changing stuff!

Recent Comments

comments powered by Disqus