2010年5月7日金曜日

VA_ARGS マクロ

objective-c の NSLog について、デバッグ時のみログを出力し、 リリースビルドではおとなしくさせる方法を知りたく、ググってみた。
いろいろなサイトで紹介されており、 次のようなマクロを定義すれば出来るとある。

    #ifdef MYDEBUG 
    #define MYLOG(...) NSLog(__VA_ARGS__)
    #else
    #define MYLOG(...) 
    #endif
で、この、__VA_ARGS__ って一体何だ? と思い、本家 gccの ドキュメント を探ってみると、 にずばりそのものの記述があった。
どうやら、printf みたいなことをマクロでやるためのものらしい。
C言語なんて もう何十年も前に K&R を読んだっきり、 全く勉強していなかったけど、いつのまにか進歩している!
デバッグする上では、この MYLOG マクロで充分なんだけど、どうせなら 、 NSLog を書いたクラスやメソッドの場所がわかればいいなあと思い、さらに gccのページを探ってみると.....
みつけた!
__PRETTY_FUNCTION__ という変数が、それを使った場所のメソッド名や関数名を、 文字列として保持しているらしい。
じゃあ、ということで、メソッド名も出力してくれる、MYLOG2を書いてみた。

#define MYLOG2(fmt, ...) \
  NSLog([@"%s " stringByAppendingString:(fmt)], \
        __PRETTY_FUNCTION__, ##__VA_ARGS__)

__VA_ARGS__ の前の ## は、 VA_ARGS が空の時に、 ',' の展開をさせないためのおまじないだ。

以下が、そのサンプルと実行結果。
# サンプルソース
$ cat test-mylog.m
// -*-mode:objc; coding:utf-8;  -*-
#import <Foundation/Foundation.h>

#define MYLOG2(fmt, ...) \
  NSLog([@"%s " stringByAppendingString:(fmt)], \
        __PRETTY_FUNCTION__, ##__VA_ARGS__)

@interface MyTest : NSObject {
  NSString *message; 
}
@property (readwrite, copy) NSString *message;
@end

@implementation MyTest
- (id) init {
  self = [super init];
  if(self){message = nil;}
  return self;
}
- (void) dealloc {
  MYLOG2(@"dealloc!"); // Test for Empty VA_ARGS
  [self setMessage:nil];
  [super dealloc];
}
-(NSString *)message{ return message;}
-(void)setMessage:(NSString *)m {
  NSString *fmt = @"message will change from %@ to %@.";
  if( ! m )fmt = @"message '%@' will release.";
  MYLOG2(fmt, message, m);
  m = [m copy]; [message release]; message = m;
  MYLOG2(@"message did change to %@", message);
}
@end

int main(int argc, char *argv[]){
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

  MyTest *test = [[[MyTest alloc] init] autorelease];
  [test setMessage:@"Hello"];

  [pool release];
  return 0;
}

# コンパイル
$ gcc test-mylog.m -framework Foundation 

# 実行
$ ./a.out
 -[MyTest setMessage:] message will change from (null) to Hello.
 -[MyTest setMessage:] message did change to Hello
 -[MyTest dealloc] dealloc!
 -[MyTest setMessage:] message 'Hello' will release.
 -[MyTest setMessage:] message did change to (null)
$
ちゃんと、[ クラス名 セレクター ] が表示される。

調子に乗って、MYLOG2と同じやり方で、alert の簡易マクロを書いてみた。

#define MYALERT_OK(title, ...) \
  ([[NSAlert alertWithMessageText:(title) \
                    defaultButton:@"OK"  \
                  alternateButton:nil otherButton:nil \
        informativeTextWithFormat:__VA_ARGS__ ] runModal])

#define MYALERT_CANCEL_OK(title, ...) \
  ([[NSAlert alertWithMessageText:(title) \
                    defaultButton:@"CANCEL" \
                  alternateButton:@"OK" otherButton:nil \
        informativeTextWithFormat:__VA_ARGS__ ] runModal])

このマクロは、こんな感じで使える。
if( ! [[NSFileManager defaultManager] fileExistsAtPath:fileName] ){
  int choice = 
    MYALERT_CANCEL_OK(@"Confirm", @"Create new file:%@ ?",fileName);
  if(choice == NSAlertDefaultReturn){
    MYALERT_OK(@"CANCELED!",@"");
    ..................;
  }else{
    ..................;
  }
}
これなら、javascript 並みにたやすく alert デバッグができそうだ。

ところが、この MYLOG2 と MYALERT_CANCEL_OK をいろいろと使っているうち に、うまくいかない場合を見つけてしまった。
例えば、
   MYLOG2([NSString stringWithFormat:@"%@ %%s",
         @"Program name is"], argv[0]);
と書くと、これは、
    error: syntax error before ‘)’ token
    error: syntax error before ‘]’ token
のコンパイルエラーになってしまう。
でも、最初の引数を、次のように括弧で囲むとエラーは無くなる。
  MYLOG2( ([NSString stringWithFormat:@"%@ %%s",
        @"Program name is"]), argv[0]);

どうやら、マクロの実引数に、stringWithFormat: のような VA_ARGS形式のセレクターを使うと、そのままでは、 ',' が変な風に解釈されてしまい、 コンパイルエラーになってしまうみたいだ。 でも、その実引数が、仮引数の ... 部分にマッチする場合は何の問題もない。
とりあえずの解決策は、( ) で囲め、ということか。
このあたりはさっぱりわからない。でも、マクロは楽しい。

マクロを使って、こんなことも出来てしまった。
$cat test-indexset.m
// -*-mode:objc; coding:utf-8;  -*-
#import <Foundation/Foundation.h>

/*
 * Ruby の NSIndexSet.each{|val| ...} 相当のつもり。
 * VA_ARGS を使っているが、実際には、3引数のマクロとして使う
*/
#define MYEACH_INDEXSET(idx,idxvalue, ...) \
{ \
  NSIndexSet *___tmpidx___ = (idx); \
  NSUInteger idxvalue = [___tmpidx___ firstIndex]; \
  while( idxvalue != NSNotFound ){ \
    __VA_ARGS__ \
    idxvalue = [___tmpidx___ indexGreaterThanIndex:idxvalue]; \
  } \
}

int main(int argc, char *argv[]){
  NSAutoreleasePool *pool;
  pool = [[NSAutoreleasePool alloc] init];
  
  NSArray *strList =
    [NSArray arrayWithObjects:@"zero",@"one",@"two"
             ,@"three",@"four",@"five", nil];
  NSMutableIndexSet *idxSet =
    [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(0,4)];
  [idxSet addIndex:10];[idxSet addIndex:5];
  MYEACH_INDEXSET(idxSet, val,
                  {
                    NSString *s = @"?";
                    if( val < [strList count]){
                      s = [strList objectAtIndex:val];
                    }
                    NSLog(@"strList[%d]=%@",val,s);
                  });
  [pool release];
  return 0;
}

$ gcc  test-indexset.m -framework Foundation

$ ./a.out
   strList[0]=zero
   strList[1]=one
   strList[2]=two
   strList[3]=three
   strList[5]=five
   strList[10]=?
$
僕としては、ruby のイテレータもどきを、 objective-c で実装したつもりだ。
マクロの実引数に { } で囲んだ文を指定して、さらに、展開も文なんだけど、 とりあえずサンプルは動作している。
でも、こんなの、たぶん問題だらけなんだろうなあ....
確か、昔読んだCの落とし穴本に、 「マクロは文ではない」と書いてあったと思う。