Blocksをメソッドとして追加する

imp_implementationWithBlock()という、BlockからIMPを作り出す関数がiOS 4.3 or Lionから使えるようになったみたいなので、これを使って実行時にインスタンスメソッドを追加する実験

例えば、モーダルウィンドウをクローズした時に自動的にstopModalするコードは下のようになる。簡単。*1

/* CLANG_ENABLE_OBJC_ARC = YES */
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <objc/runtime.h>
...
    NSApplication *application = [NSApplication sharedApplication];
    
    BOOL (^windowShouldClose)(id, id) = ^(id self, id sender) {
        NSLog(@"windowShouldClose:%@", sender);
        if ([application modalWindow] == self) {
            [application stopModal];
        }
        return YES;
    };
    // Block to IMP
    IMP imp_windowShouldClose = imp_implementationWithBlock((__bridge void*) windowShouldClose);
    // add IMP to class as Method
    class_addMethod([NSWindow class], @selector(windowShouldClose:), imp_windowShouldClose, "B@:@");
...

Blockの引数は、第一引数がid型(selfが渡される)で、第二引数以降がメソッドの引数の列になる。戻り値の型はメソッドの戻り値の型を指定する。この場合、windowShouldClose:はid型の引数を取り、戻り値の型はBOOLなので、「BOOL (^)(id, id)」になるようにBlockを定義する。
class_addMethod()の最後の引数は、メソッドのシグネチャ文字列(NSMethodSignatureの+signatureWithObjCTypes:の引数と同じ)で、「戻り値がBOOL(="c")で引数がid(="@")」なので、"c@:@"となる*2。以下も参照。

既存のメソッドの実装を置き換えることも出来る。置き換え元の実装を呼び出しやすくするために「Blockを作るBlock」を渡せるようにしてみる。

void
replaceMethodWithBlock(Class cls, SEL sel, id (^blockGen)(SEL, IMP))
{
    IMP originalImplementation = NULL;
    Method originalMethod = class_getInstanceMethod(cls, sel);
    if (!originalMethod) {
        [NSException raise:NSInvalidArgumentException format:@"method not found:%s", sel];
    }
    originalImplementation = method_getImplementation(originalMethod);
    id block = blockGen(sel, originalImplementation);
    if (!block) {
        return;
    }
    IMP imp = imp_implementationWithBlock((__bridge void*) block);
    method_setImplementation(originalMethod, imp);
}

これを使って、例えばNSWindowのmouseDown:の前後にログを吐くようにする。

    replaceMethodWithBlock([NSWindow class], @selector(mouseDown:), 
        // block to generate block for method.
        (id) ^(SEL selector, IMP originalImplimentation) {
            // block for method.
            void (^block)(id, NSEvent*) = ^(id self, NSEvent *theEvent) {
                NSLog(@"[BEFORE] mouseDown:%@", theEvent);
                originalImplimentation(self, selector, theEvent);
                NSLog(@"[AFTER] mouseDown:");
            };
            return block;
       });

ちょっとAOPっぽい感じ。

なんかちょっとメモリ管理回りが不安だけど。[block copy]とかやんなくて良くなったのかな?

おまけ: 関数の型について

IMPの型はヘッダによるとこう

typedef id 			(*IMP)(id, SEL, ...); 

一方Blockの関数部分はこんな感じ(cf: http://d.hatena.ne.jp/terazzo/20101103/1288807194)

id (struct __block_literal_1 *, ...)

引数の数だけで言えばBlock側はSELが足りない。メソッド用Blockの第二引数がSELではないのは、この辺と関係ありそう。

というようなこと考えてたら、解説があったのでリンクしておく。中身まだあんまり読めてないけど。

*1:但し全NSWindowに適用されるので危険だけど

*2:当初BOOL(="B")としていましたがコメントでツッコミがあったので訂正しました