davedelong.com

  • home
  • blog
  • downloads
  • portfolio
  • contact
Home

Search










Have a look at my Amazon Wishlist.

davedelong's tweets

  • @mdjensen it depends on the db system. %% might work. — 3 hours 26 min ago
  • Only have one more level on #starcraft2. I'll save it for tomorrow — 4 hours 35 min ago
  • dumb dumb dumb dumb duuuuuuuummmmmmmb http://www.heyuguys.co.uk/2010/07/28/titanic-2-trailer-no-this-is-not-a-... — 1 day 13 hours ago
  • @jergason they screwed up my order, and their help email address never sent anything but automated responses — 1 day 14 hours ago
  • Fed up with @gamestop's terrible customer service. Cancelled my order for #starcraft2 and will buy it from anyone but them — 1 day 14 hours ago
  •  
  • 1 of 592
  • ››
more

Aspect Oriented Programming in Objective-C

davedelong — Mon, 04/13/2009 - 15:22

Recently in a CS class of mine, we briefly discussed Aspect Oriented Programming. The idea intrigued me, and knowing some of the capabilities of the Objective-C runtime, I decided to play around with it and see what I could do.

I first needed a good aspect that I could work on. After a bit of thought, I decided that NSCoding was a good candidate. Usually, Cocoa developers implement NSCoding in order to serialize a custom object. My preferred way is via NSKeyedArchiver and NSKeyedUnarchiver. I realized that whenever I implement the requisite initWithCoder: and encodeWithCoder: methods, they're almost always the same thing: Save all the instance variables.

So what I've come up with is what I call "NSCodingAspect". The idea of the class is simple: It has one public class method called addToClass:error:, and that method will add NSCoding compliance to whatever class is passed in.

Included below is the full .m file for NSCodingAspect. This is my first stab at it, and it's probably not the cleanest, but I think it's pretty darn nifty:

#import "NSCodingAspect.h"
#import <objc/runtime.h>
 
@implementation NSCodingAspect
 
+ (void) addMethod:(SEL)aSelector toClass:(Class)aClass error:(NSError **)error {
  IMP implementation = class_getMethodImplementation([self class], aSelector);
  Method method = class_getInstanceMethod([self class], aSelector);
  NSLog(@"  Adding -[%@ %@]", NSStringFromClass(aClass), NSStringFromSelector(aSelector));
  BOOL worked = class_addMethod(aClass, aSelector, implementation, method_getTypeEncoding(method));
  if (!worked) {
    *error = [NSError errorWithDomain:NSStringFromClass(aClass) 
                                 code:0 
                             userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Error adding method: %@", 
                                                                         NSStringFromSelector(aSelector)] 
                                                                  forKey:@"errMsg"]];
  } else {
    error = nil;
  }
}
 
+ (void) addToClass:(Class)aClass error:(NSError **)error {
  Protocol * codingProtocol = objc_getProtocol("NSCoding");
  BOOL classConforms = class_conformsToProtocol(aClass, codingProtocol);
  NSString * className = NSStringFromClass(aClass);
  NSLog(@"Conforming [%@ class] to <NSCoding>", className);
 
  if (!classConforms) {
    class_addProtocol(aClass, codingProtocol);
 
    if (!class_getInstanceMethod(aClass, @selector(initWithCoder:))) {
      [NSCodingAspect addMethod:@selector(initWithCoder:) toClass:aClass error:error];
      if (error) { return; }
    }
    if (!class_getInstanceMethod(aClass, @selector(encodeWithCoder:))) {
      [NSCodingAspect addMethod:@selector(encodeWithCoder:) toClass:aClass error:error];
      if (error) { return; }
    }
    //all the ivars need to conform to NSCoding, too
    unsigned int numIvars = 0;
    Ivar * ivars = class_copyIvarList(aClass, &numIvars);
    for(int i = 0; i < numIvars; i++) {
      NSString * type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivars[i])];
      if ([type length] > 3) {
        NSString * class = [type substringWithRange:NSMakeRange(2, [type length]-3)];
        Class ivarClass = NSClassFromString(class);
        [NSCodingAspect addToClass:ivarClass error:error];
      }
    }
  }
}
 
- (id) initWithCoder:(NSCoder *)decoder {
  if ([super respondsToSelector:@selector(initWithCoder:)] && ![self isKindOfClass:[super class]]) {
    self = [super performSelector:@selector(initWithCoder:) withObject:decoder];
  } else {
    self = [super init];
  }
  if (self == nil) { return nil; }
 
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
  unsigned int numIvars = 0;
  Ivar * ivars = class_copyIvarList([self class], &numIvars);
  for(int i = 0; i < numIvars; i++) {
    Ivar thisIvar = ivars[i];
    NSString * key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
    id value = [decoder decodeObjectForKey:key];
    if (value == nil) { value = [NSNumber numberWithFloat:0.0]; }
    [self setValue:value forKey:key];
  }
  if (numIvars > 0) { free(ivars); }
  [pool drain];
  return self;
}
 
- (void) encodeWithCoder:(NSCoder *)encoder {
  if ([super respondsToSelector:@selector(encodeWithCoder:)] && ![self isKindOfClass:[super class]]) {
    [super performSelector:@selector(encodeWithCoder:) withObject:encoder];
  }
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
  unsigned int numIvars = 0;
  Ivar * ivars = class_copyIvarList([self class], &numIvars);
  for (int i = 0; i < numIvars; i++) {
    Ivar thisIvar = ivars[i];
    NSString * key = [NSString stringWithUTF8String:ivar_getName(thisIvar)];
    id value = [self valueForKey:key];
    [encoder encodeObject:value forKey:key];
  }
  if (numIvars > 0) { free(ivars); }
  [pool drain];
}
 
@end

Here's the general idea of what it's doing:

  1. It checks to see if the class already conforms to NSCoding. If it does, then it exits.
  2. If not, it proceeds to copy the two NSCoding methods from NSCodingAspect into the target class.
  3. It then loops through all the instance variables of the target class to make sure that they all conform to NSCoding as well. If any don't, NSCoding gets added to them, too
    The implementation of the initWithCoder: and encodeWithCoder: methods are as follows:
  1. Check to see if the superclass needs to init or encode. If so, then go do that.
  2. Get the list of all the instance variables
  3. For each instance variable, get its name, convert it to an NSString, and then either encode or decode the value that corresponds to that instance variable

In reality, the logic is pretty straightforward. What's so neat about this is that it's even possible! You can't do this with most compiled languages, since the information on what methods or instance variables a class has are statically compiled into the code.

It's stuff like this that make me absolutely LOVE working in Objective-C.

Dave

(While not tested, the above code should work just fine on the iPhone)

  • Add new comment

Pretty cool

Anonymous — Sat, 03/13/2010 - 21:02

Pretty cool. It's kind of like a mixin class. It makes me wonder if you could get something like Ruby mixins working in objective-c.

--Tom Dalling

  • reply

Changed for Obj-C++

Anonymous — Fri, 04/23/2010 - 08:36

Hello,

I've used this on an ObjC++ program, and I have 2 comments.

1) the use of  NSString * class is invalid in ObjC++
2) the use of  if (error) { return; } should instead be  if (error == nil) { return; } since nil != false/NO

Finally, while it might be obvious to a pro, you should perhaps add a Caveat that the code does not add NSCodingAspect to classes of objects contained in Arrays or Dictionaries. Adding this functionality, I assume, would only be possible after the object was instantiated and data loaded into the arrays.

Other than that, this is a beautiful tool for adding NSCoding to a black-box library I have.

THANK YOU SO MUCH.

-Stephen

  • reply
  • home
  • blog
  • downloads
  • portfolio
  • contact