Carpe diem (Felix's blog)

I am a happy developer

Dance With Objective-C Dynamic Types

Objective-C is a super set of C language. The entire language is a preprocessor skin added to C language and a powerful runtime system. With this runtime system, one can have full featured object oriented programming interface, functional programming environment, and magical dynamic typing system.

In this post, I’ll go through common tasks you can do with Objective-C typing system, including querying normal NSObject types, packing static type with NSValue, testing core foundation references, and validating if a pointer is a valid object pointer.

Objective-C type system

To determine an Objective-C object type is super easy. Just use isKindOfClass method and it is done.

1
2
3
4
5
6
7
8
- (void)testObjectType:(id)obj
{
    if ([obj isKindOfClass: [NSNumber class]]) {
        // do something with number
    } else if ([obj isKindOfClass: [NSValue class]) {
        // do something with values...
    }
}

Why do we need this mechanism? One application is implementing key value coding with some known range of types. For example, Core Animation listed these properties are animatable:

  • anchorPoint
  • backgroundColor
  • backgroundFilters
  • borderColor
  • borderWidth
  • bounds
  • compositingFilter
  • contents
  • contentsRect
  • cornerRadius
  • doubleSided
  • filters
  • frame
  • hidden
  • mask
  • masksToBounds
  • opacity
  • position
  • shadowColor
  • shadowOffset
  • shadowOpacity
  • shadowRadius
  • sublayers
  • sublayerTransform
  • transform
  • zPosition

These properties are categorized in several types includes CGPoint, CGRect, CGFloat, CGImageRef, CGColorRef, and even BOOL. Each kind of type require individual implementation to operate its value. Thankfully, Objective C dynamic type system allows us to pass-in the value with generic type id and determine the actual type at runtime. id is simply a void pointer. The objective c object itself is a struct which have a isa pointer points to actual class which defines its instance variables, methods, and class inheritances.

Packaging static C types with NSValue

Objective C is a skin language based on C, so it is very often to use C types like int, float, pointer to struct…etc. However, these static types violate Objective-C’s dynamic typing idioms. Apple introduced NSValue as a container for a single C or Objective-C data item. It can hold any C types such as int, float, char, pointers, structures, and object ids. It not only wrap the item into an Objective-C object, but also encode the type information of the original object.

To create an NSValue object, you pass it a pointer to the item, along with the encoded type information generated by @encode() keyword.

1
2
CGPoint origin = CGPointMake(0,0);
NSValue * originValue = [NSValue valueWithBytes:&origin objCType:@encode(CGPoint)];

@encode() is a compiler directive which can accepts all types that can be used as an argument of C sizeof() operator. @encode() returns a const char* string encoding that type. The encoding is specified in Objective-C runtime type encodings.

To illustrate this, see the following examples:

1
2
3
4
5
6
7
8
@encoding(int ** )
// ==> "{^^i}"
@encoding(CGPoint)
// ==> "{CGPoint=ff}"
@encoding(CGColorRef)
// ==> "^{CGColor=}"
@encoding(NSObject)
// ==> "{NSObject=#}"

With this encoded type information, it only takes few steps to determine which type it is at runtime:

1
2
3
4
5
6
7
8
if ([obj isKindOfClass:[NSValue class]]) {
    NSValue* value = (NSValue*) obj;
    if strcmp([value objCType], @encode(CGPoint)) == 0) {
        CGPoint origin;
        [value getValue:&origin];
        // do things with origin...
    }
}

UIKit addition to NSValue

UIKit added a category for NSValue to represent iOS related geometry-based data. You can use these method instead of encoding CGPoint, CGRect, and else every time.

Bridging with Core Foundation objects

Though NSValue covers many kind of types, in practice there are still some types don’t fit this solution for dynamic typing. More specifically, CGColorRef, CGImageRef and other Core Foundation types that can be treated as Objective-C object through toll-free briding are the types we don’t pack with NSValue.

A core foundation references is also a void pointer as same as id is. To find out the type of an unknown CFTypeRef, you can query it with C function CFGetTypeID.

1
2
3
4
5
if (CFGetTypeID((__bridge CFTypeRef)obj), == CGImageGetTypeID()) {
    CGImageRef imgRef = CFBridgingRetain(obj);
    // do things with imgRef
    CFRelease(imgRef);
}

A CFTypeRef marked as id type can also accept basic objective C messages like isKindOfType:. Hence testing an id typed object is quite safe as long as it is either a NSObject, NSValue, CFTypeRef, CGColorRef or any other Objective-C object/Core Foundation reference.

Testing if a pointer is a valid NSObject

There is a blog post on Cocoa with love about how to test if an arbitary pointer is a valid NSObject. In my point of view, programmer should pass in a valid object for sure. If it is not a valid object, just let it crash.

Puting it all together

This piece of code is part of my project FCAnimationFactory for the purpose of interpolating different kinds of value with respect to their types.

FCAnimationFactory.m link
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
- (id(^)(float))makeValueScalingBlockFromValue:(id)fromValue ToValue:(id)toValue
{
    if (fromValue==nil || toValue==nil) NSAssert(0, @"fromValue and toValue must not be nil");

    id value = fromValue;

    /*
     * single float is handled in NSNumber
     */
    if ([value isKindOfClass:[NSNumber class]]) {
        float v1 = [(NSNumber*)fromValue floatValue];
        float v2 = [(NSNumber*)toValue floatValue];
        float diffValue = v2 - v1;
        return ^id(float factor){
            float result = factor*diffValue + v1;
            return [NSNumber numberWithFloat:result];
        };
    }

    /*
     * NSValue handles CGPoint, CGSize, CGRect, and CATransform3D
     */
    if ([value isKindOfClass:[NSValue class]]) {
        const char* objCType = [value objCType];
        if (strcmp(objCType, @encode(CGPoint))==0) {
            CGPoint pt0, pt1;
            [(NSValue*)fromValue getValue:&pt0];
            [(NSValue*)toValue getValue:&pt1];
            return ^id(float factor){
                float x = (pt1.x - pt0.x)*factor + pt0.x;
                float y = (pt1.y - pt0.y)*factor + pt0.y;
                return [NSValue valueWithCGPoint:CGPointMake(x, y)];
            };
        } else if (strcmp(objCType, @encode(CGSize))==0) {
            CGSize size0, size1;
            [(NSValue*)fromValue getValue:&size0];
            [(NSValue*)toValue getValue:&size1];
            return ^id(float factor){
                float w = (size1.width - size0.width)*factor + size0.width;
                float h = (size1.height - size0.height)*factor + size0.height;
                return [NSValue valueWithCGSize:CGSizeMake(w, h)];
            };
        } else if (strcmp(objCType, @encode(CGRect))==0) {
            CGRect rect0, rect1;
            [(NSValue*)fromValue getValue:&rect0];
            [(NSValue*)toValue getValue:&rect1];
            return ^id(float factor){
                float x = (rect1.origin.x - rect0.origin.x)*factor + rect0.origin.x;
                float y = (rect1.origin.y - rect0.origin.y)*factor + rect0.origin.y;
                float w = (rect1.size.width - rect0.size.width)*factor + rect0.size.width;
                float h = (rect1.size.height - rect0.size.height)*factor + rect0.size.height;
                return [NSValue valueWithCGRect:CGRectMake(x, y, w, h)];
            };
        } else if (strcmp(objCType, @encode(CATransform3D))==0) {
            NSAssert(0, @"CATransform3D type currently not supported");
        } else {
            NSAssert(0, @"Unknown NSValue type %s",objCType);
        }
    }

    if (CFGetTypeID((__bridge CFTypeRef)value) == CGColorGetTypeID()) {
        return ^id(float factor){
            CGColorRef fromColor = (__bridge CGColorRef)fromValue;
            CGColorRef toColor = (__bridge CGColorRef)toValue;
            size_t num = CGColorGetNumberOfComponents(fromColor);
            const CGFloat *fromComp = CGColorGetComponents(fromColor);
            const CGFloat *toComp = CGColorGetComponents(toColor);

            CGFloat newComp[num]; // same as malloca
            for (size_t i = 0; i < num; ++i) {
                newComp[i] = (toComp[i] - fromComp[i]) * factor + fromComp[i];
            }
            CGColorRef retColor = CGColorCreate(CGColorGetColorSpace(fromColor), newComp);

            return (__bridge_transfer id)retColor;
        };
    }
    if (CFGetTypeID((__bridge CFTypeRef)value) == CGImageGetTypeID()) {
        NSAssert(0, @"CGImageRef should be handled in another class");
    }

    NSAssert(0, @"value type unknown");
    return ^id(float factor){ return nil;};    // turn off compiler warnings
}

Conclusion

It is amazing that a langauge so close to C can create such a rich type system without byte code, VM, or complex sybol tricks (like what C++ does). Though handling differnt types can be a bit painful sometimes, but it brings powerful polimorphsm to the language. Thus programmer can create highly abstract API and framework with differnt data types that share the same methods.

References

Comments