May 22 2013

Illuminating ARCLite

Today we’re going to look at using Objective-C’s array and dictionary subscripting syntax with older iOS and OS X versions.

If you haven’t already, I can’t recommend enough reading Part 1 and Part 2 of Mark Dalrymple’s excellent two-part series about Objective-C’s literal/boxing/subscripting syntax. The code in those posts can be found on GitHub as a Gist.

Recap

When the literal syntax for creating dictionaries and arrays (and numbers) was announced, we as a developer community sang the praises of short, concise code. Then we were introduced to the subscripting syntax for the same, and many of us squealed with joy.

In case you’ve been living under a very large rock, I’m talking about our ability to read a value (and store one) in an NS(Mutable)Array using C-array-like syntax:

NSMutableArray *cultOfSkaro = [NSMutableArray arrayWithObjects:@"Sec",@"Caan",@"Thay",@"Jast",nil];
Dalek *leader = cultOfSkaro[0]; // The new hotness
cultOfScaro[0] = [NSNull null]; // We can also write values!

And don’t forget dictionaries!

NSMutableDictionary *theGuide = [NSMutableDictionary dictionary];
theGuide[@"Earth"] = @"Mostly harmless."; // ...mostly.

This “new” subscripting syntax is super-duper wonderful for sparing us lots of typing.

The problem, of course, is that it’s still new. Subscripting is only supported at runtime in iOS 6 and OS X 10.8, and while we can dream, most clients are not yet ready to drop support for iOS 5 and OS X 10.7.

What makes this feature not backward-compatible? Consider that:

cultOfScaro[0]

doesn’t equate to:

[cultOfScaro objectAtIndex:0]

as we’d expect. It instead equates to:

[cultOfScaro objectAtIndexedSubscript:0]

and that method doesn’t exist on NSArray in iOS 5 or OS X 10.7. We get the same raw deal with dictionaries, where

theGuide[@"Earth"] = @"Mostly harmless.";

is short for:

[theGuide setObject:@"Mostly harmless." forKeyedSubscript:@"Earth"];

So what do I do about it?

If you want to be able to use array and dictionary subscripting syntax with older versions of iOS and OS X, you’re in luck!

If you’re already familiar with Objective-C categories, you can just add the subscript-related methods to NS(Mutable)Array and NS(Mutable)Dictionary yourself!

For example, you could implement a cheap -objectAtIndexedSubscript: in a category on NSArray, like so:

- (id)objectAtIndexedSubscript:(NSInteger)index
{
    NSAssert(index >= 0, @"If you want negative indices, see Mark's posts linked above.");
    return [self objectAtIndex:index];
}

And there you have it. This implementation isn’t particularly robust, but it would get you back to level ground for being allowed to use subscripting syntax for reading values.

But wait, there’s more!

Unfortunately, there are two problems with this approach. One is more technical and one more practical.

The technical problem is that when a category is loaded, its methods are installed onto their appropriate classes, which is a good thing. If you implement a method in a category that was already present on the class, however, you replace that method. It’s generally poor form to replace a method whose implementation you didn’t write (or don’t fully understand). On devices running iOS 5, we’d be adding the method, but under iOS 6, we’d be replacing it. Harrumph.

The practical problem is that we’d be wasting our time to begin with! Apple has already provided us with a mechanism for utilizing subscripting syntax with older deployment targets.

You’re welcome to try it yourself. Take a new command line project, set its deployment target to OS X 10.7, and give it a basic test:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        NSArray *strings = @[@"E",@"G",@"B",@"D",@"F"];
        NSString *eString = strings[0];
        NSLog(@"%@",eString);
    }
    return 0;
}

You’ll find that this (tiny) app will build and run just fine, even on a Mac running OS X 10.7. It just works! Magic!

Welp, that was a short blog post. Cheers!

Not so fast, Bub.

Nerds tend not to like magic. Oh, we love to see a good trick. But things that we can’t explain really tend to get under our skin.

“Now, Mikey,” you might ask, “what in tarnation is going on that makes this work?”

First, I’d laugh at you for using a word like “tarnation”. Then, I’d tell you a short story about a small library called ARCLite.

When ARC was first introduced, our minds were blown. We could feel years being added to our lives at the thought of not having to write -retain and -release calls anymore. Even better, it turned out that ARC was backward-compatible! But how?

Apple had provided a build shim in the form of a static library called libarclite. Xcode 4.2 and later knew to link against this library anytime you built a project with a deployment target of iOS 4 or Mac OS X 10.6 with ARC enabled. This library provides ARC support for older systems.

It turns out that the-little-lib-that-could has also taken up the torch of providing older systems with support for container subscripting, while also solving our technical problem above: libarclite provides implementations of -objectAtIndexedSubscript: and friends to the container classes on iOS 5 without replacing the implementations provided by iOS 6.

libarclite does this by waiting until runtime to decide whether or not to add the method, by first finding out whether the method already exists.

Waiting until whattime?

If you’ve ever swizzled a method, go ahead and move on. If you haven’t, you’ll want to be seated for this bit. We’re going to talk about the Objective-C runtime library, and use it to understand a bit about how Apple’s pulling off the feat of only adding a category method when it doesn’t already exist.

The Objective-C runtime is the beating heart of a Cocoa application. It’s the low-level bit that puts the “Objective” in Objective-C by teaching C how to be object-oriented. The runtime library is also almost completely open source! We’re interested in one particular and integral header, however: runtime.h.

If you take a moment to browse this file, your imagination will start to run wild with possibilities. With these functions, you can define an entire class after your program has started running! There’s not really much practical use for such a thing (in our position), but when has that ever stopped us from trying? In fact, runtime class definition is how KVO works! But that could be a totally separate post.

There’s a specific function of interest to us in runtime.h:

OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

This function allows you to attach to a class an entirely new method that didn’t appear in the class’ header or implementation file. Of course, the method must be defined somewhere in your code, but this function allows you to attach it to a class that didn’t have it before.

The class_addMethod() function takes four parameters:

  • a Class to which the method should be added, such as that returned by [NSString class]. Just as @"Hello" is an instance of type NSString*, NSString itself is of type Class.
  • a selector that names the method—yes, the same sort of selector that you use with NSTimer
  • a function pointer to the actual implementation of the method you wish to add, and
  • a C character array describing the types of arguments the method accepts

The function returns a BOOL indicating the success or failure of the method addition. The function will fail and return NO if the target Class already has a method with the specified selector.

So what now?

How can we use class_addMethod() for ultimate science?

Let’s do it. Step zero? Import runtime.h so that we can use the functions declared there:

#import <objc/runtime.h>

First, we’ll go ahead and create our NSArray+Subscripting category, as we discussed at the beginning of the post:

@implementation NSArray (Subscripting)

- (void)bnr_objectAtIndexedSubscript:(NSInteger)index
{
    return [self objectAtIndex:index];
}

@end

You’ll notice immediately that I’ve changed the name of the subscripting method. Remember that if I’d named it:

- (void)objectAtIndexedSubscript:(NSInteger)index

then this method would be installed on NSArray as is, regardless of the OS version. We don’t want that, so we define the method using a fake/temporary name.

For this same reason, we should always namespace our category methods like this (prefixing the method name with an identifier of some sort, such as bnr_ above) so that we don’t accidentally replace methods that we didn’t even know existed.

The second step is to find a place to put our call to class_addMethod(). We want to implement or override a method that we know will execute very early in an application run.

When an application launches, one of the very first things that happens (even before main() is called!) is that each class that the application will use is loaded into the runtime. Each class is sent the +load message (and, because it’s special and doesn’t play by the usual rules, to each category on each class). This is a dangerous place for most types of activity, but it’s the perfect place to make changes to the runtime. That’s not to say that making runtime changes is always safe, mind you.

So, here’s our NSArray+Subscripting.m:

#import "NSArray+Subscripting.h"
#import <objc/runtime.h>

@implementation NSArray (Subscripting)

+ (void)load
{
    class_addMethod([NSArray class],
                @selector(objectAtIndexedSubscript:),
                method_getImplementation(bnr_objectAtIndexedSubscript:),
                method_getTypeEncoding(bnr_objectAtIndexedSubscript:)
                );
}

- (void)bnr_objectAtIndexedSubscript:(NSInteger)index
{
    return [self objectAtIndex:index];
}

@end

There are some fantastic bits at work here. First, class_addMethod() will fail if a method already exists with the passed-in selector. Second, arguments that initially looked incredibly daunting when we saw this function’s declaration (what on earth is an IMP or a type encoding?) are satisfied by calling other runtime functions from runtime.h! It feels like cheating!

I challenge you now to spend some time looking through the documentation for the various functions in runtime.h, and experimenting on your own.

Excellent, so this is all I need to get subscripting in iOS 5?

Not so fast. Remember, Apple is already doing this (or something like it) for you with libarclite! We’ve meandered down the runtime’s rabbit-hole as an exercise in pulling away the magic curtains, as it were.

There are certainly plenty of runtime hacks at varying levels of evil and danger that you may find useful in your own application, but this one’s already been done for you.

If you’re interested in learning more about the Objective-C runtime and its dark secrets, sound off in the comments.

Also, check out these excellent resources: *

7 Comments

  1. Eli Ganem

    Great post. I always like reading about what’s under the hood. Nerds really don’t like magic :)

  2. Rimantas

    For iOS 5 all you need is to build with recent Xcode and iOS6 SDK, it deploys back to iOS5 just fine:
    http://developer.apple.com/library/ios/#releasenotes/ObjectiveC/ObjCAvailabilityIndex/index.html

  3. Jorge Luis Mendez

    Awesome post!, thanks.

    You might want to change >= for >= on this line:

    NSAssert(index >= 0, @"If you want negative indices, see Mark's posts linked above.");

Leave a Comment

Join the discussion. Do not worry, your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>