Re: Objective-C で AUTOLOAD (あるいは method_missing )

お題:

リンク先の実装だと引数の値が取得出来ないので、以下の制約付きで引数が取れるようにしてみた。

  • 戻り値は常にid型
  • 引数は全てid型
  • 引数にnilを指定されたときはNSNullに変換
#import <Foundation/Foundation.h>

@interface Foo : NSObject
- (id)call:(NSString *)selectorString withArguments:(NSArray *)arguments;
@end

@implementation Foo
- (id)call:(NSString *)selectorString withArguments:(NSArray *)arguments
{
    NSLog(@"call:'%@' withArguments: %@", selectorString, arguments);
    return [NSString stringWithFormat:@"Hello, '%@!'", selectorString]; // 何でも良いのでid型を戻す
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    NSMethodSignature *methodSignature = [invocation methodSignature];
    int i, numberOfArguments = [[invocation methodSignature] numberOfArguments];

    NSMutableArray *arguments  = [NSMutableArray arrayWithCapacity:numberOfArguments-2];
    for (i = 2; i < numberOfArguments; i++) {
        // 一応引数の型がid型かを確認
        const char *argTypes = [methodSignature getArgumentTypeAtIndex:i];
        if (strcmp(argTypes, @encode(id))) {
            [NSException raise:@"NSInvalidArgumentException" format:@"Unsupported argument type:'%s' occurs.", argTypes]; 
        }
        id arg = nil;
        [invocation getArgument:&arg atIndex:i];
        if (!arg) arg = [NSNull null];
        [arguments addObject:arg];
    }

    // 一応戻り値の型がid型かを確認
    const char *returnType = [methodSignature methodReturnType];
    if (strcmp(returnType, @encode(id))) {
        [NSException raise:@"NSInvalidArgumentException" format:@"Unsupported return type:'%s' occurs.", returnType]; 
    }

    id ret = [self call:NSStringFromSelector([invocation selector]) withArguments:arguments];
    [invocation setReturnValue:&ret];
}

- (NSMethodSignature*)methodSignatureForSelector:(SEL)sel
{
    NSMethodSignature* sig = [super methodSignatureForSelector:sel];
    if (sig) return sig;
    
    // typesを準備。引数なしでも「戻り値」「自分自身」「SEL」の分の3つは必要。下の例なら「@@:」
    NSString *types = [NSString stringWithFormat:@"%s%s%s",
                                                 @encode(id),    // return type
                                                 @encode(id),    // 'self'
                                                 @encode(SEL)];   // selector
                       
    // 引数の数=「:」の数なので、:の数を数える(手抜き実装)
    int countOfArguments = [[NSStringFromSelector(sel) componentsSeparatedByString:@":"] count] - 1;
    // 「:」の数だけ「@」を追加
    types = [types stringByPaddingToLength:countOfArguments + 3
                    withString:[NSString stringWithUTF8String:@encode(id)]
                    startingAtIndex:0];
    return [NSMethodSignature signatureWithObjCTypes:[types UTF8String]];
}
@end

int
main()
{
    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

    Foo* foo = [[Foo alloc] init];
    
    [foo bar];
    [foo buzz];

    NSString *ret = [foo asd:@"1" qwe:nil zxc:[NSNumber numberWithInt:3]];
    NSLog(@"ret = %@", ret);

    [foo release];
    
    [pool drain];
    return 0;
}
// gcc -framework Foundation -o foo foo.m

出力結果:

2011-12-06 01:34:05.238 foo[77485:a0f] call:'bar' withArguments: (
)
2011-12-06 01:34:05.241 foo[77485:a0f] call:'buzz' withArguments: (
)
2011-12-06 01:34:05.241 foo[77485:a0f] call:'asd:aaa:qwe:' withArguments: (
    1,
    "<null>",
    2
)
2011-12-06 01:34:05.242 foo[77485:a0f] ret = Hello, 'asd:aaa:qwe:!'

簡単な解説1. メソッドシグネチャの偽造

NSMethodSignatureの+signatureWithObjCTypes:で存在しないメソッドのNSMethodSignatureを作ることが可能(OS X 10.5以降)。引数には、メソッドのシグネチャを文字列で渡してやる必要がある。この文字列の構成は、

  1. 戻り値の型
  2. 自分自身(id型)
  3. SEL
  4. 第1引数の型
  5. 第2引数の型
  6. ...

となっていて、それぞれの型に対応する文字列は「Objective-C Runtime Programming Guide: Type Encodings」に乗ってます。


例えば、NSStringの-compare:options:range:だと、

  1. 戻り値の型 → 'q' (=long long)
  2. 自分自身 → '@' (=id)
  3. SEL → ':' (=SEL)
  4. 第1引数の型 → '@' (=id)
  5. 第2引数の型 → 'Q' (=unsigned long long)
  6. 第3引数の型 → '{_NSRange=QQ}' (=NSRange)

となり、全体では「q@:@Q{_NSRange=QQ}」になる。


ObjCTypes文字列は「@encode(型)」で取れるので、分からなければコードを書いて調べることも出来る。

    NSLog(@"objcType: %s", @encode(NSRange));

あるいは、NSMethodSignatureの非公開メソッド-debugDescriptionを呼んでやると引数の構成をゲロってくれるようですよ(さっき見つけた。)

    NSString *string = @"hoge";
    NSMethodSignature *sig = [string methodSignatureForSelector:@selector(compare:options:range:)];
    NSLog(@"%@", [sig debugDescription]);

出力:

2011-12-06 01:07:34.985 foo[76998:a0f] <NSMethodSignature: 0x10010cea0>
    number of arguments = 5
    frame size = 224
    is special struct return? NO
    return value: -------- -------- -------- --------
        type encoding (q) 'q'
        flags {isSigned}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 0: -------- -------- -------- --------
        type encoding (@) '@'
        flags {isObject}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 1: -------- -------- -------- --------
        type encoding (:) ':'
        flags {}
        modifiers {}
        frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 2: -------- -------- -------- --------
        type encoding (@) '@'
        flags {isObject}
        modifiers {}
        frame {offset = 16, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 3: -------- -------- -------- --------
        type encoding (Q) 'Q'
        flags {}
        modifiers {}
        frame {offset = 24, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 4: -------- -------- -------- --------
        type encoding ({) '{_NSRange=QQ}'
        flags {isStruct}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
        memory {offset = 0, size = 16}
            type encoding (Q) 'Q'
            flags {}
            modifiers {}
            frame {offset = 32, offset adjust = 0, size = 8, size adjust = 0}
            memory {offset = 0, size = 8}
            type encoding (Q) 'Q'
            flags {}
            modifiers {}
            frame {offset = 40, offset adjust = 0, size = 8, size adjust = 0}
            memory {offset = 8, size = 8}

今回は、引数・戻り値の型をidに固定することで、引数の数さえ分かればシグネチャを偽造して返せるようにしている。引数の数はSELに含まれるコロンの数で決まるので、その個数分だけ「@」をくっつけてやれば良い。

簡単解説2. 引数の取得

-methodSignatureForSelector:が呼ばれた時に、上の方法で作成したNSMethodSignatureを戻してやると、その型の構成にあわせてNSInvocationに引数を詰めて-forwardInvocation:を呼んでくれるので、そのNSInvocationから引数の値を取得出来る。


まず引数の数を[ [invocation methodSignature] numberOfArguments]で取得する。この数には、上で出て来た「自分自身」「SEL」が必ず含まれていて、2以上の数になる。実際の引数は3つ目以降になる。


引数は、NSInvocationの-getArgument:atIndex:で取れる。第一引数にポインタを渡すと値を埋めてくれる。第二引数に指定する数値は(自分自身」「SEL」がある関係上、) プラス2した値にする。

簡単解説3. 戻り値の設定

-forwardInvocation:の引数で渡されたNSInvocationに戻り値を設定してやることで、メソッド呼び出し側に戻り値が渡せる。

    id ret = [self call:NSStringFromSelector([invocation selector]) withArguments:arguments];
    [invocation setReturnValue:&ret];

戻り値を設定するには、-setReturnValue:に、値の入った状態でポインタを渡してやる。中の値はnil(NULL)でも良い。戻り値の型がvoidなのに-setReturnValue:したりするとエラーになる。

追記

最初「@」を個数分追加するのに

    NSMutableString *types = [NSMutableString stringWithFormat:@"%s%s%s",
...
    while (countOfArguments--) {
        [types appendFormat:@"%s",@encode(id)];
    }

ってやってたけど、OS X 10.2から文字をパディングするメソッドが出来てたので書き直した。

    NSString *types = [NSString stringWithFormat:@"%s%s%s",
...
    types = [types stringByPaddingToLength:countOfArguments + 3
                    withString:[NSString stringWithUTF8String:@encode(id)]
                    startingAtIndex:0];


というかほぼ同じ回答あった→「Respond to an unknown method call - Rosetta Code