Aspect Oriented Programming

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.

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

  1. funwithobjc posted this
Short URL for this post: http://tmblr.co/Zt522y1Oltfl
blog comments powered by Disqus