Implementing Key-Value Coding

Sat, 1 Aug 2020 18:34:00 +0200

Using the Objective-C runtime to read and write property values dynamically.

In this article we’ll see how Key-Value Coding (KVC) works internally, and implement a basic version of it ourselves using the Objective-C runtime. The ability to read and write properties dynamically is a useful tool to have in our reflection toolbox, and we’ll be making use of it in later articles.

KVC is one of the core features of Foundation, and enables us to access the properties of an object indirectly. With KVC, we can get and set property values using the property names instead of their accessor methods.

// Normal accessor usage:
int i = person.age;
person.age = 18;

// With KVC:
NSNumber *n = [person valueForKey:@"age"];
[person setValue:@18 forKey:@"age"];

There’s a few interesting points to note about KVC. The property name is a string, so we can construct these at run time and pass in any string as the key. Since we can pass any key, the compiler can’t check this at build time so KVC calls can fail, unlike synthesized accessor methods. Using an invalid key that can’t be mapped to a property will result in a run time exception.

Also of interest is how KVC deals with property values without knowing what type they are.

Boxing

KVC has to work with properties of any type, not just objects. The only reasonable way to do this is to treat all values as id, which is the most generic type in Objective-C. Even non-objects like int need to be handled as id, so these scalar values are automatically wrapped or “boxed” into NSValue or its subclass, NSNumber. You can read about how boxing works in Apple’s KVC guide.

You can see in the example above that valueForKey returned an NSNumber for age instead of an int. Likewise, we had to box the 18 into an NSNumber by writing it as @18.

This is a necessary overhead when dealing with types dynamically. It also unfortunately means that we lose the type safety that the compiler would normally provide:

NSString *a = person.age;
// Compiler error: conversion of 'int' to 'NSString *'

NSString *b = [person valueForKey:@"age"];
// No problem! Until you try to use it...

However we can easily check the type of the returned value if necessary, or use one of the NSNumber convenience methods like intValue to extract the type we want.

How KVC works

If we look at just the getter side first, calling valueForKey on an object does something like:

1. Look for a property on that object with the same name as the key

2. Find the actual implementation of the getter method for that property, which will be a C function

3. Call the getter function using a return type the same as the property’s type

4. Box up scalar values into NSValue or NSNumber

5. Return the value

The process for setValue:forKey: is very similar, with a few steps reversed.

In step 1, KVC’s valueForKey actually searches for multiple getter methods that are variants of the key, and also has some special support for collection properties. Apple’s KVC also supports valueForKeyPath, which recursively calls valueForKey on a nested object.

We won’t be adding any of these extra features here, because all of the interesting runtime stuff is in the basic valueForKey implementation.

KVC also has the ability to get and set ivar values directly, in case it can’t find a matching property name or accessor method. While accessing ivars dynamically is done in a similar way, it’s a little more complicated than using property accessors, so we’ll tackle that in a separate article.

Let’s jump in and start building our own KVC!

The API

Since the KVC methods are implemented on NSObject, we can’t just use the same method names on a new class. So while we’ll also add ours to a category on NSObject, we’ll name our methods a little differently. Our method names will also reflect that the “key” in this case has to be a property name.

/// Returns the value of a property with the given name
- (id)valueForProperty:(NSString *)propertyName;

/// Sets a property with the given name to the specified value
- (void)setValue:(id)value forProperty:(NSString *)propertyName;

Implementing the KVC getter

Step one of the implementation was to find a property matching the name that was passed in. For this, we’ll be using some code and the ClassProperty wrapper from the earlier article on Inspecting Properties:

- (id)valueForProperty:(NSString *)propertyName {

    objc_property_t prop = class_getProperty(self.class,
                                             propertyName.UTF8String);

    if (prop == NULL) {
        [NSException raise:NSInvalidArgumentException
                    format:@"Can't find property %@.%@",
                           NSStringFromClass(self.class), propertyName];
    }

    ClassProperty *property = [[ClassProperty alloc] initWithProperty:prop];

    SEL getter = property.getter;

Here we’re using class_getProperty to get the details of the property with the exact name specified, and throwing an exception if it doesn’t exist. Exceptions are not commonly used in Objective-C, and we could just return nil instead, but we’ll do it this way to match the behaviour of KVC.

We’ll create a ClassProperty wrapper object to make it simpler to look up some details of the property, like the selector of the getter. Usually the selector just has the same name as the property, in which case we could just use NSSelectorFromString(propertyName), but ClassProperty also handles custom getter names for us.

Getting the implementation function

This next step is the real meat of KVC.

Every property has a getter method and optional setter, and whether they are provided by us or generated by the compiler, they are all implemented as underlying C functions.

We need to get a pointer to the actual method implementation so we can call into it. These implementation pointers are represented by the IMP type in Objective-C, and we can get one using the class_getMethodImplementation runtime function.

This function can also fail, so we’ll throw an exception if the implementation doesn’t exist:

    IMP imp = class_getMethodImplementation(self.class, getter);

    if (imp == NULL) {
        [NSException raise:NSInternalInconsistencyException
                    format:@"Can't find implementation of %@.%@",
                    NSStringFromClass(self.class), propertyName];
    }

Ok that wasn’t so hard. Now how do we use imp?

Well since this is a C function pointer, we can just call it like imp()! 😯

But as you might have guessed, it’s not quite so simple. The compiler has no idea what kind of parameters or return type this function has, so we have to explicitly cast it first.

Calling the getter implementation

Just like casting object pointers or scalars to other types, you can also cast function pointers. The syntax is a bit harder to read though.

But what do need to cast it to? A getter method doesn’t take any arguments, and returns a value of the same type as the property. However, every Objective-C method does have two implicit arguments: self and _cmd (which is the method’s selector). These arguments are explicit for the underlying C function.

Let’s take the person.age example from above. The getter method signature is:

- (int)age;

That means the signature of the underlying implementation function would be:

int AgeGetter(id self, SEL _cmd);

Note that the implementation would actually be an anonymous function without a name.

So for an int property, we can cast imp to the correct type with:

(int (*)(id, SEL))imp

It might not be the easiest to read if you’re not familiar with function pointers, but the type is the same as the function signature above, except with (*) where the function name would be.

If we assigned that to a properly-typed function pointer, then we could call it like so:

int (*intGetterFunc)(id, SEL) = (int (*)(id, SEL))imp;

int value = intGetterFunc(self, getter);

Which would be exactly the same as calling the property getter normally, such as person.age.

Supporting any property type

That would be fine if we only needed to handle int properties, but KVC needs to handle (nearly) every property type. Unfortunately there’s no shortcut here – we have to handle every type individually.

We can get the type using the TypeEncoding helper class from the previous article on Type Encodings, and then handle them all in a big switch statement:

switch (property.type.type) {
    case TypeChar: {
        char (*charGetter)(id, SEL) = (char (*)(id, SEL))imp;
        char c = charGetter(self, getter);
        return [NSNumber numberWithChar:c];
    }
    case TypeInt: {
        int (*intGetter)(id, SEL) = (int (*)(id, SEL))imp;
        int i = intGetter(self, getter);
        return [NSNumber numberWithInt:i];
    }
    case TypeShort: {
        short (*shortGetter)(id, SEL) = (short (*)(id, SEL))imp;
        short s = shortGetter(self, getter);
        return [NSNumber numberWithShort:s];
    }

You’ll notice very quickly that this looks quite repetitive. We’ll have to repeat this function casting, calling, and boxing for all thirteen scalar types.

We can at least remove some of the boilerplate by defining a macro:

#define RETURN_BOXED(TYPE) {                              \
    TYPE (*getterFunc)(id, SEL) = (TYPE (*)(id, SEL))imp; \
    TYPE val = getterFunc(self, getter);                  \
    return @(val);                                        \
}

The trailing backslashes join these lines together, since a macro must be defined on a single line.

Note that we can just use the @() boxing syntax to create a NSNumber here, since the compiler knows what type we’re boxing by this point.

Now we can replace all the scalar switch cases like so:

switch (property.type.type) {
    case TypeChar:              RETURN_BOXED(char)
    case TypeInt:               RETURN_BOXED(int)
    case TypeShort:             RETURN_BOXED(short)
    case TypeLong:              RETURN_BOXED(long)
    case TypeLongLong:          RETURN_BOXED(long long)
    case TypeUnsignedChar:      RETURN_BOXED(unsigned char)
    case TypeUnsignedInt:       RETURN_BOXED(unsigned int)
    case TypeUnsignedShort:     RETURN_BOXED(unsigned short)
    case TypeUnsignedLong:      RETURN_BOXED(unsigned long)
    case TypeUnsignedLongLong:  RETURN_BOXED(unsigned long long)
    case TypeFloat:             RETURN_BOXED(float)
    case TypeDouble:            RETURN_BOXED(double)
    case TypeBool:              RETURN_BOXED(BOOL)

The rest of the types we’ll handle case by case.

After handling every scalar individually, object types are easy… they can all be treated the same! There’s also no boxing required, just return the result of the getter directly:

    case TypeObject:
    case TypeClass:
        return ((id (*)(id, SEL))imp)(self, getter);

Properties with a type of Class can be handled just like any other object type.

Less common property types

This actually takes care of the vast majority of types that we’d typically use properties for.

Some of our EncodedType cases we don’t have to handle at all. You can’t even define a property which is a C array or a bitfield, so we can ignore those completely.

Other types are valid to use as properties, but KVC doesn’t support them: void, unions, function pointers and other non-object pointers. You’ll get a “not key value coding compliant” exception if you try to access them, so we’ll do the same. No big loss, since these these types are rarely (if ever) used for properties.

    case TypeVoid:
    case TypeArray:
    case TypeUnion:
    case TypeBitField:
    case TypePointer:
    case TypeUnknown:
        [NSException raise:NSInvalidArgumentException
                    format:@"Not KVC-compliant: %@.%@",
                    NSStringFromClass(self.class), propertyName];

There are two more types which KVC doesn’t support, C strings and selectors. But we can support them! C strings can just be boxed into an NSString, and selectors can also be convert to strings:

    case TypeCString:
        RETURN_BOXED(char *)

    case TypeSelector: {
        SEL (*selGetter)(id, SEL) = (SEL (*)(id, SEL))imp;
        SEL sel = selGetter(self, getter);
        return NSStringFromSelector(sel);
    }

Note we can use RETURN_BOXED here because the @() boxing syntax also works for C strings.

So that’s all the types handled. Well, all except one…

Getting struct values

Structs are occasionally used as property types – think CGSize or NSRange – but they are considerably more difficult to handle. This is because we don’t know what kind of struct the getter should return, or how big it is. However, KVC supports struct properties, so we should be able to get it working too.

With all the other types, we knew exactly what the getter function should return, so we could cast it correctly. If we only wanted to support say, CGRect we could do something like this:

CGRect (*rectGetter)(id, SEL) = (CGRect (*)(id, SEL))imp;
CGRect r = rectGetter(self, getter);
return [NSValue valueWithCGRect:r];

But that only handles one struct type. There’s plenty of others in Foundation and UIKit with varying sizes, not to mention user-defined structs that could be of any size. How can we handle those?

The first thing we need to know is that with KVC, a struct is boxed into NSValue. While NSValue has a few handy methods like valueWithPoint and valueWithRect for handling specific structs, those are not useful to us because they aren’t generic enough. We need some way to construct an NSValue from any struct.

Taking a look at the API, we see valueWithBytes:objCType:, which takes a pointer to a memory buffer, plus the type encoding of the value. We already have the type encoding, but how do we get a function to return a value into a memory buffer?

There are two new classes which can help us here:

1. NSMethodSignature. Describes the types of a method’s arguments and return value, including the size of the return value.

2. NSInvocation. This is an object that encapsulates a whole method call. You specify what to use as self, which method selector to use, and which argument values to pass in. Then you can invoke the method, and most importantly, get the return value in a memory buffer.

Looks like we have all the pieces we need to call a struct getter now. Note that we won’t be casting or calling imp in this case.

    case TypeStruct: {
        // allocate a buffer to hold the return value
        NSMethodSignature *sig = [self methodSignatureForSelector:getter];
        void *buffer = malloc(sig.methodReturnLength);

        // set up and call the invocation
        NSInvocation *invoc = [NSInvocation invocationWithMethodSignature:sig];
        invoc.selector = getter;
        [invoc invokeWithTarget:self];
        [invoc getReturnValue:buffer];

        // box the returned struct
        const char *encoding = property.type.encoding.UTF8String;
        NSValue *val = [NSValue valueWithBytes:buffer objCType:encoding];
        free(buffer);
        return val;
    }

That’s quite a lot to take in! But no-one said runtime programming would be easy 😅

So you might be asking yourself, if this handles any type, why couldn’t we just use it for all the boxed cases? After all, NSValue can hold any scalar type too.

Good question, but there’s a couple of reasons why it’s worth handling the scalar cases individually:

1. We can’t wrap scalars in an NSNumber in a generic way. The valueWithBytes:objCType: on the NSValue superclass won’t give us back an NSNumber. So our only choice is to use one of the specific initialisers like numberWithInt or numberWithDouble.

2. Direct function calls are much faster than using NSInvocation. At least 10x faster from my rough benchmarking.

The final step is to handle any unknown types after the switch, which we can do by throwing an exception, using an assert, or just returning nil.

Now we just need to implement the other half of KVC: setValue:forProperty:.

Setting property values

This part is quite similar to what we’ve already done above, so we’ll go through it more quickly. The first part is getting the IMP for a property setter:

- (void)setValue:(id)value forProperty:(NSString *)propertyName {

    objc_property_t prop = class_getProperty(self.class,
                                             propertyName.UTF8String);

    if (prop == NULL) {
        [NSException raise:NSInvalidArgumentException
                    format:@"Can't find property %@.%@",
                    NSStringFromClass(self.class), propertyName];
    }

    ClassProperty *property = [[ClassProperty alloc] initWithProperty:prop];

    SEL setter = property.setter;

    if (property.isReadOnly || setter == NULL) {
        [NSException raise:NSInvalidArgumentException
                    format:@"Can't set read-only property %@.%@",
                    NSStringFromClass(self.class), propertyName];
    }

    IMP imp = class_getMethodImplementation(self.class, setter);
    if (imp == NULL) {
        [NSException raise:NSInternalInconsistencyException
                    format:@"Can't find setter of %@.%@",
                    NSStringFromClass(self.class), propertyName];
    }

This is almost identical to the start of valueForProperty, with an additional check for read-only properties. It could be extracted into a method called impForProperty:isGetter: if we wanted.

The next part also looks very familiar — we’ll need to handle each property type individually by using a switch. This time though, we have to cast the IMP to a setter function, which for our age property, has the form:

void AgeSetter(id self, SEL _cmd, int newValue);

The other difference is that this time, we need to unbox the scalar from an NSValue, using the correct method for the type such as intValue. We’ll define another macro to reduce code duplication.

#define SET_UNBOXED(TYPE, METHOD) {                                   \
    void (*setterFunc)(id, SEL, TYPE) = (void (*)(id, SEL, TYPE))imp; \
    setterFunc(self, setter, [value METHOD]);                         \
    break;                                                            \
}

switch (property.type.type) {
    case TypeChar:             SET_UNBOXED(char, charValue)
    case TypeInt:              SET_UNBOXED(int, intValue)
    case TypeShort:            SET_UNBOXED(short, shortValue)
    case TypeLong:             SET_UNBOXED(long, longValue)
    case TypeLongLong:         SET_UNBOXED(long long, longLongValue)
    case TypeUnsignedChar:     SET_UNBOXED(unsigned char, unsignedCharValue)
    case TypeUnsignedInt:      SET_UNBOXED(unsigned int, unsignedIntValue)
    case TypeUnsignedShort:    SET_UNBOXED(unsigned short, unsignedShortValue)
    case TypeUnsignedLong:     SET_UNBOXED(unsigned long, unsignedLongValue)
    case TypeUnsignedLongLong: SET_UNBOXED(unsigned long long, unsignedLongLongValue)
    case TypeFloat:            SET_UNBOXED(float, floatValue)
    case TypeDouble:           SET_UNBOXED(double, doubleValue)
    case TypeBool:             SET_UNBOXED(BOOL, boolValue)
    case TypeObject:
    case TypeClass:            SET_UNBOXED(id, self)
    case TypeCString:          SET_UNBOXED(const char *, UTF8String)
    case TypeSelector: {
        void (*setterFunc)(id, SEL, SEL) = (void (*)(id, SEL, SEL))imp;
        setterFunc(self, setter, NSSelectorFromString(value));
        break;
    }
    case TypeVoid:
    case TypeArray:
    case TypeUnion:
    case TypeBitField:
    case TypePointer:
    case TypeUnknown:
        [NSException raise:NSInvalidArgumentException
                    format:@"Not KVC-compliant: %@.%@",
                    NSStringFromClass(self.class), propertyName];

Notice that objects, classes, and C strings can also be handled using SET_UNBOXED. Because we’re just calling any method to unbox the value, we can call UTF8String to “unbox” an NSString to a C string. We can also use a small trick to do this with objects… calling self on any object returns the same object.

There’s no real need to do any type checking on value here (like making sure it’s an NSNumber), since trying to call these unboxing methods on the wrong type of object will result in a runtime exception anyway.

Finally, the setter for struct properties.

Setting struct values

This almost looks the same as for getters, except that NSMethodSignature doesn’t have an equivalent of methodReturnLength for the parameter sizes. Luckily there’s another runtime function we can use, NSGetSizeAndAlignment, which will calculate the size of any type based on its type encoding.

Otherwise, it’s just the reverse of above: we unbox the NSValue into a buffer, and set that buffer as the third argument on the NSInvocation (with self and _cmd being the first two).

    case TypeStruct: {
        // get the size of the struct parameter
        const char *encoding = property.type.encoding.UTF8String;
        NSUInteger size;
        NSGetSizeAndAlignment(encoding, &size, NULL);

        // allocate a buffer and copy the value into it
        void *buffer = malloc(size);
        [value getValue:buffer size:size];

        // set up and call the invocation
        NSMethodSignature *sig = [self methodSignatureForSelector:setter];
        NSInvocation *invoc = [NSInvocation invocationWithMethodSignature:sig];
        invoc.selector = setter;
        [invoc setArgument:buffer atIndex:2];
        [invoc invokeWithTarget:self];

        free(buffer);
        break;
    }
}

Wrapping up

That’s it! We’ve built the main functionality of Key-Value Coding.

While you may not need to actually use a custom implementation like this, it’s useful to understand how to read and write properties dynamically.

Like many features of Objective-C, you find there’s not much magic to it when you lift the curtain, but just different edge cases that need to be handled properly.

Useful links

Apple’s Key-Value Coding Guide

Inspecting Objective-C Properties article

Type Encodings article

Let’s Build Key-Value Coding by Mike Ash