macの最近のブログ記事

2011年5月24日

Core Foundationを知る / CFHostでエラーハンドリング

さて、プログラミングは正常系ばかりではだめで、エラーもちゃんと扱えるようにしとかないといけない。とりわけネットワークプログラミングについては特にそれが言えるんじゃないでしょうか。並のPCでも最近は潤沢なメモリ事情を拝啓にすれば、malloc()がNULLを返すことはそうそう無いと看做すことはできても(でも危険だよ!)、それにくらべればネットワークは格段に不確実であり、名前が引けなかったり、TCPコネクションが確立できなかったりすることは普通にある。正常系以上にエラー系におけるAPIの挙動はぜひ把握しておきたい。

前のエントリまででCFHostを非同期による解決例を追ったので、それに続けて話を進めてみます。

○ネットワークが無効な場合
○DNSサーバが応答しない場合

上記2例については、いずれの場合もコールバック関数(CFHostClientCallBack)の呼び出しにエラー(CFStreamError)を渡される。具体的には、CFStreamErrorのdomainフィールドに12(kCFStreamErrorDomainNetDB)、errorフィールドに8(EAI_NONAME)が与えられた状態でのコールバックとなる。

domainがkCFStreamErrorDomainNetDBだった場合、errorコードは/usr/include/netdb.hに定義されている通りであるとはCFHost Referenceに書かれてある通りなのだが、当のnetdb.hを読んでみるとEAI_XXX、すなわちgetaddrinfo()のエラーコードが定義されているのみである。つまるところ、CFHostの下位レイヤーではgetaddrinfo()が使われ、そのエラーコードがCFStreamErrorに設定されてコールバックされるということなのだろう。

http://developer.apple.com/library/mac/#documentation/CoreFoundation/Reference/CFHostRef/Reference/reference.html%23//apple_ref/doc/uid/TP40003333-CH6-DontLinkElementID_2
CFHost Reference

○存在しない名前を引いた場合

DNS的に言えばNXDOMAINだったときの話。この時も、コールバックで受け取るパラメータとしては上記2項と同じであり、domain = 12およびerror = 8が設定されたCFStreamError構造体が渡されることになる。EAI_NONAMEの意味からすればそのとおりではあるのだけれども、上記二つと区別がつかないというのは何となく納得が行かない気も、しないでもない。まあ、区別をつけたくば他の方法を使えということなのだろう。

○CFHostGetAddresses()の第二引数(Boolean *hasBeenResolved)について

上記"CFHost Reference"にあるように、CFHostGetAddresses()は名前解決の完了したCFHostオブジェクトを第一引数に持ち、名前解決を済ませたことによって得られたsockaddr構造体を生のバイト列としてCFDataオブジェクトにし、それをCFArray配列オブジェクトとして返すという仕様の関数である。

ただし、前述のようにいくつかの要因によって名前解決が出来なかった場合もあり、その際にはエラーコード指定の上でCFHostClientCallBack関数が呼び出される訳だが、そのエラー発生時のコールバック発生後にCFHostGetAddresses()関数を使うと、第二引数hasBeenResolvedは、名前解決ができたできないに関わらずTRUEが与えられるらしい点に注意せねばいけない模様。つまり、名前解決ができたかどうかはこの第二引数から得られる値で確かめることはできず、もっぱら返値が非NULL(何らかのCFArrayオブジェクト)かどうかでしか分からないらしい、ということである。

挙動から見る限りは、この第二引数は単にコールバック関数が呼ばれたかどうか、すなわち「名前解決の処理がおわったかどうか」だけを見ることができる、と看做したほうが自然なようである。

2011年5月17日

Core Foundation Hacks / iPhoneでも同じように動くのか?

さて、ここまでやってきた内容はMacOSX上の話にてございました(しかもSnow Leopard)。これがiOS上でも同じように挙動するかどうかも追っておきたい。

一応、MacOSX/iOS間で(ある程度)共通の環境基盤ということで用意されている以上、挙動がMacとiPhone/iPadとで食い違ったりしてたら嫌じゃんと。適宜こうした差異があるんだかないんだかというのは自分の目で確かめておきたいものでありまして。

下記の実装によって、MacOSXでの動作と同様にデフォルトではaccept()されたサーバ側ソケットがCFReadStreamClose()/CFWriteStreamClose()の呼び出しの際に同時にはcloseされず、kCFStreamPropertyShouldCloseNativeSocketプロパティをTRUE(kCFBooleanTrue)に設定することで同時closeとなるよう挙動を変更できることが確認できました。よろしければ皆様もご確認くださいませ。

#import <UIKit/UIKit.h>

@interface iPhoneSocketServerAppDelegate : NSObject <UIApplicationDelegate, NSStreamDelegate> {
    CFSocketRef _socket;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;

@property (nonatomic, retain) IBOutlet UINavigationController *navigationController;

@end

#import "iPhoneSocketServerAppDelegate.h"

#include <sys/socket.h>
#include <arpa/inet.h>


@implementation iPhoneSocketServerAppDelegate


- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
    NSLog(@"stream = %@, eventCode = %u", aStream, eventCode);
    
    switch (eventCode) {
        case NSStreamEventOpenCompleted:
            NSLog(@"open completed");
            [aStream close];
            [aStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
            [aStream release];
            break;
            
        case NSStreamEventHasBytesAvailable:
            [aStream close];
            [aStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
            [aStream release];
            break;
    }
}

- (void)setupInputStream:(NSInputStream*)instream
{
    [instream setDelegate:self];
    [instream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [instream open];
}

- (void)setupOutputStream:(NSOutputStream*)outstream
{
    [outstream setDelegate:self];
    [outstream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [outstream open];
}


static void
_server_socket_callback_2(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void* data, void* info)
{
    CFSocketNativeHandle handle = *(CFSocketNativeHandle*)data;
    iPhoneSocketServerAppDelegate* appdele = (iPhoneSocketServerAppDelegate*)info;
    
    NSLog(@"accepted. (s = %p)", s);
    NSLog(@"handle = %d", handle);
    
    CFReadStreamRef readStream = NULL;
    CFWriteStreamRef writeStream = NULL;
    
    CFStreamCreatePairWithSocket(kCFAllocatorDefault, handle, &readStream, &writeStream);
    
    CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
    CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
    
    [appdele setupInputStream:(NSInputStream*)readStream];
    [appdele setupOutputStream:(NSOutputStream*)writeStream];
    
    NSLog(@"readStream = %p, writeStream = %p", readStream, writeStream);
}

- (void)doServer
{
    CFSocketContext context = { 0, self, NULL, NULL, NULL };
    CFSocketRef socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, 0, kCFSocketAcceptCallBack, _server_socket_callback_2, &context);
    
    int yes = 1;
    setsockopt(CFSocketGetNative(socket), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
    
    struct sockaddr_in sin;
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = INADDR_ANY;
    sin.sin_port = htons(8888);
    sin.sin_len = sizeof(struct sockaddr_in);
    
    CFDataRef data = CFDataCreate(kCFAllocatorDefault, (UInt8*)&sin, sizeof(sin));
    CFSocketSetAddress(socket, data);
    CFRelease(data);
    
    CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    CFRelease(source);
    
    NSLog(@"socket = %p", socket);
}

@synthesize window=_window;

@synthesize navigationController=_navigationController;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Override point for customization after application launch.
    // Add the navigation controller's view to the window and display.
    self.window.rootViewController = self.navigationController;
    [self.window makeKeyAndVisible];
    
    [self doServer];
    return YES;
}

- (void)dealloc
{
    [_window release];
    [_navigationController release];
    
    CFSocketInvalidate(_socket);
    CFRelease(_socket);
    _socket = nil;
    [super dealloc];
}

2011年5月16日

もっとCore Foundation / サーバソケットとCFStreamと私

で。

CFStream APIについて触れたかったのはこっち。CFStreamはネットワーク部分はいわゆるTCP/IPソケットと同じなので、connectとacceptによって通信路が確立される。

前のエントリで触れた内容ではクライアント側の実装、すなわちconnect側だったわけだが、もちろんサーバソケット(accept側)の実装もできる。ちょっと手間はかかるけれどもこんな感じ。

static void
_server_socket_callback(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void* data, void* info)
{
    CFSocketNativeHandle handle = *(CFSocketNativeHandle*)data;
    
    NSLog(@"accepted. (s = %p, handle = %d)", s, handle);
    
    CFReadStreamRef readStream = NULL;
    CFWriteStreamRef writeStream = NULL;
    
    CFStreamCreatePairWithSocket(kCFAllocatorDefault, handle, &readStream, &writeStream);
    
    if (_setup_read_stream(readStream) != 0) {
        CFRelease(readStream);
        readStream = NULL;
    }
    if (_setup_write_stream(writeStream) != 0) {
        CFRelease(writeStream);
        writeStream = NULL;
    }
    
    NSLog(@"readStream = %p, writeStream = %p", readStream, writeStream);
}

static void
_do_server()
{
    CFSocketContext context = { 0, NULL, NULL, NULL, NULL };
    CFSocketRef socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, 0, kCFSocketAcceptCallBack, _server_socket_callback, &context);

    int yes = 1;
    setsockopt(CFSocketGetNative(socket), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
    
    struct sockaddr_in sin;
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = INADDR_ANY;
    sin.sin_port = htons(8888);
    sin.sin_len = sizeof(struct sockaddr_in);
    
    CFDataRef data = CFDataCreate(kCFAllocatorDefault, (UInt8*)&sin, sizeof(sin));
    CFSocketSetAddress(socket, data);
    CFRelease(data);
    
    CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    CFRelease(source);
    
    NSLog(@"socket = %p", socket);
}
  1. CFSocketCreate()でCFSocketオブジェクトを生成し、
  2. CFSocketSetAddress()でバインディングアドレスを指定、
  3. CFSocketCreateRunLoopSource()およびCFRunLoopAddSource()でRun LoopにCFSocketオブジェクトを登録、
  4. 最初のCFSocketCreate()で登録したコールバック(_server_socket_callback)にacceptされたソケットディスクリプタ(=CFSocketNativeHandle)が渡り、
  5. 4.のソケットディスクリプタからCFStreamCreatePairWithSocket()より、CFReadStream/CFWriteStreamを生成。

これ以降は前エントリの扱いと同じ。

んで。

このパターンで生成されたCFReadStream/CFWriteStreamは、CFReadStreamClose()/CFWriteStreamClose()してもTCP接続は閉じないという現象を確認してしまいました。これそういうもん?と色々調べたところ、CFReadStream/CFWriteStreamオブジェクトのプロパティkCFStreamPropertyShouldCloseNativeSocketをTRUE(kCFBooleanTrue)にするとちゃんと閉じてくれるようになった。

static void
_server_socket_callback(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void* data, void* info)
{
    switch (type) {
        case kCFSocketAcceptCallBack: {
            CFSocketNativeHandle handle = *(CFSocketNativeHandle*)data;
            
            NSLog(@"accepted. (s = %p, handle=%d)", s, handle);
            
            CFReadStreamRef readStream = NULL;
            CFWriteStreamRef writeStream = NULL;
            
            CFStreamCreatePairWithSocket(kCFAllocatorDefault, handle, &readStream, &writeStream);
            
            CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
            CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
            
            if (_setup_read_stream(readStream) != 0) {
                CFRelease(readStream);
                readStream = NULL;
            }
            if (_setup_write_stream(writeStream) != 0) {
                CFRelease(writeStream);
                writeStream = NULL;
            }

            NSLog(@"readStream = %p, writeStream = %p", readStream, writeStream);
        }
            break;
    }
}

もっとCore Foundation / CFStreamこそがtoll-freeだよ

さて前のエントリでCFHostをうっかりtoll-freeだと勘違いして書いてしまったせいで、引き続きのこのエントリが内容がらりと変えることになってしまいましてね、と前置きをしつつの第2回であります。

さて無事クエリを解決できたCFHostを用いていよいよ通信の確立ですが。CFStream APIを使えばそのまま非同期接続が可能なのでそれで充分です。さっそく行ってみる。ざっくり。

static int
_setup_read_stream(CFReadStreamRef readStream)
{
    CFStreamClientContext context = { 0, NULL, NULL, NULL, NULL };
    if (!CFReadStreamSetClient(readStream,
                               kCFStreamEventOpenCompleted |
                               kCFStreamEventHasBytesAvailable |
                               kCFStreamEventErrorOccurred |
                               kCFStreamEventEndEncountered,
                               _read_stream_callback, &context)) {
        CFStreamError err = CFReadStreamGetError(readStream);
        NSLog(@"err = %d/%ld", err.error, err.domain);
        return -1;
    }
    CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
    
    if (!CFReadStreamOpen(readStream)) {
        // error.
        CFStreamError err = CFReadStreamGetError(readStream);
        NSLog(@"err = %d/%ld", err.error, err.domain);
        
        CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL);
        CFReadStreamUnscheduleFromRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
        return -1;
    }
    
    return 0;
}

static int
_setup_write_stream(CFWriteStreamRef writeStream)
{
    CFStreamClientContext context = { 0, NULL, NULL, NULL, NULL };
    
    if (!CFWriteStreamSetClient(writeStream,
                                kCFStreamEventOpenCompleted |
                                kCFStreamEventCanAcceptBytes |
                                kCFStreamEventErrorOccurred |
                                kCFStreamEventEndEncountered,
                                _write_stream_callback, &context)) {
        CFStreamError err = CFWriteStreamGetError(writeStream);
        NSLog(@"err = %d/%ld", err.error, err.domain);
        return -1;
    }
    CFWriteStreamScheduleWithRunLoop(writeStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
    
    if (!CFWriteStreamOpen(writeStream)) {
        // error.
        CFStreamError err = CFWriteStreamGetError(writeStream);
        NSLog(@"err = %d/%ld", err.error, err.domain);
        
        CFWriteStreamSetClient(writeStream, kCFStreamEventNone, NULL, NULL);
        CFWriteStreamUnscheduleFromRunLoop(writeStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
        return -1;
    }
    
    return 0;
}

static void
_do_connect(CFHostRef host)
{
    NSLog(@"host = %@", host);
    
    CFReadStreamRef readStream = NULL;
    CFWriteStreamRef writeStream = NULL;
    
    CFStreamCreatePairWithSocketToCFHost(kCFAllocatorDefault, host, 80, &readStream, &writeStream);
    
    NSLog(@"readStream = %@", readStream);
    NSLog(@"writeStream = %@", writeStream);
    
    if (_setup_read_stream(readStream) != 0) {
        CFRelease(readStream);
        readStream = NULL;
    }
    if (_setup_write_stream(writeStream) != 0) {
        CFRelease(writeStream);
        writeStream = NULL;
    }

    NSLog(@"connect start.");
}

_do_connect()で使うCFStreamCreatePairWithSocketToCFHost()からCFReadStream/CFWriteStreamオブジェクトを取り出してこれをCFRunLoopにバインドするまでで一連の処理になります。read/writeともに、使用するAPIの流れは以下の通り。

  1. CFXxxStreamSetClient()
  2. CFXxxStreamScheduleWithRunLoop()
  3. CFXxxStreamOpen()

最初のCFXxxStreamSetClient()でコールバック関数を指定した後、Run Loopに登録。その後にCFXxxStreamOpen()でCFStreamを開けばコールバック関数に処理が渡り始める。

static void
_read_stream_callback(CFReadStreamRef readStream, CFStreamEventType eventType, void* info)
{
    NSLog(@"readStream = %p, eventType = %ld", readStream, eventType);
    
    switch (eventType) {
        case kCFStreamEventOpenCompleted:
            ...            
        case kCFStreamEventHasBytesAvailable:
            ...
        case kCFStreamEventEndEncountered:
            ...
        case kCFStreamEventErrorOccurred:
            ...
    }
}

static void
_write_stream_callback(CFWriteStreamRef writeStream, CFStreamEventType eventType, void* info)
{
    NSLog(@"writeStream = %p, eventType = %ld", writeStream, eventType);
    
    switch (eventType) {
        case kCFStreamEventOpenCompleted:
            ...
        case kCFStreamEventCanAcceptBytes:
            ...
        case kCFStreamEventEndEncountered:
            ...
        case kCFStreamEventErrorOccurred:
            ...
    }
}

一方で、ここまでコールバックを設定するのにCFHostClientContextおよびCFStreamClientContextという構造体があったけれども、これがコールバック関数の引数に渡ってくることになる。

struct CFHostClientContext {
  CFIndex			 version;
  void *			  info;
  CFAllocatorRetainCallBack  retain;
  CFAllocatorReleaseCallBack  release;
  CFAllocatorCopyDescriptionCallBack  copyDescription;
};
typedef struct CFHostClientContext	  CFHostClientContext;

typedef CALLBACK_API_C( void , CFHostClientCallBack )(CFHostRef theHost, CFHostInfoType typeInfo, const CFStreamError *error, void *info);

typedef struct {
    CFIndex version;
    void *info;
    void *(*retain)(void *info);
    void (*release)(void *info);
    CFStringRef (*copyDescription)(void *info);
} CFStreamClientContext;

typedef void (*CFReadStreamClientCallBack)(CFReadStreamRef stream, CFStreamEventType type, void *clientCallBackInfo);
typedef void (*CFWriteStreamClientCallBack)(CFWriteStreamRef stream, CFStreamEventType type, void *clientCallBackInfo);

CFHostClientContextおよびCFStreamClientContextのinfoフィールドで指定したアドレスが、CFHostClientCallBackの第4引数info、CFReadStreamClientCallBack/CFWriteStreamClientCallBackの第3引数clientCallBackInfoに渡ってくる。

このCFHostClientContext/CFStreamClientContextでは、infoフィールドのアドレスをCoreFoundationオブジェクトが標準でそうであるように、参照カウントの仕組みを前提としており、infoを引数にするretain/release関数を指定することができる。それぞれretain/releaseフィールドはこの用途で使われる。また、copyDescriptionフィールドは、デバッグ用途などでオブジェクトの内容を文字列化する際に使うことができる。CocoaでNSLogに書式化文字列を使って"%@"にオブジェクトを直接ぶち込み、オブジェクトの内容をコンソールに簡易に出力することができるアレのこと。

ただ、retain/release/copyDescriptionフィールドはあまり重要性が高くなく、Cocoaとの連携で考える限りにおいて参照カウントの管理についてもObjective-C側でのみ面倒を見ていれば大抵は用が足りると思われ、あんまりそこは気にしなくてよさそうな感触である。

2011年5月13日

Core Foundationをもっと使おうじゃないか / CFHostで非同期に名前解決

ちょっと自分で必要があって調べたので、ここにまとめておこうと思う。Core Foundationネタである。

大抵のことはCocoa(というかFoundationとAppKit)レイヤーで済んでしまうので、割とこう厄介になることは少ないもんなんだけど、いざ細かいところが気になり出して手を加えようとしはじめると粒度が荒くて...なんてことが割とある。以下はそのときに調べて使ったものを少しずつ小分けにまとめてみたもの。備忘録的に行ってみよう。

***

CFHostで非同期に名前解決を行えることを知ったのでそのやり方をまとめてみた。CFHostは普通の同期解決も非同期解決もできるんだが、メインループとの親和的な統合を歌っているだけあり、非同期解決の方でこそその旨味が発揮されるというものかもしれない。っていうか同期解決なら普通にNSHost(Cocoa)でもできるし。

非同期で名前解決することで得られるメリットは「アプリの挙動が安定すること」だ。MacOSX/iOSいずれであっても、ユーザの操作にはできるだけダイレクトに応答するようにした方が、アプリが安定して動いている感じがしてよろこばれるし、また何かしらのブロッキングな処理で一見アプリの動作が止まっているように見えても、「名前解決をしているのか」「データを読もうとしているのか」がわかる、あるいは本当に異常で「フリーズして」しまっているのかを見分けられるというのはアプリケーションに対する安心感として帰ってくる。また、もし誤操作でブロッキングさせた、もしくはブロックしている間に気が変わって他のことをしたくなったなら、簡単にキャンセルして元に戻せるようにもしたい。快適なインターネットライフ(何)のために、アプリケーションの挙動はできるだけユーザに安心と信頼を与えられるようでありたいものだ。

ネットワークアプリケーションを作るときに、ブロッキングが発生するタイミングは大きく三つある。

  • 名前解決
  • TCP接続の確立(connect(),accept())
  • データの送受信

これらの状況で何も考えずに処理を書くと簡単にブロックが発生してしまう。ブロックが発生するというのは即ちGUI操作が止まってウィンドウは動かせずボタンは押せず、レインボーカーソルのお出ましである。「サーバとの通信を行います」とモーダルダイアログで告知してたまに通信を行うだけの必要最低限のネットワーク機能しか無いようなやつならまだしも、メールソフトやブラウザが頻繁に固まるようなアプリケーションばかりでは、正直快適とは言いがたい。

Cocoaではこの「データの送受信」については既に非同期処理が導入されている(NSStreamsとか)。このため大抵のケースはこれで間に合うが、前の二つ「名前解決」「connect()/accept()」については同期のみ、というか手の入れようが無い。まあDNSはローカル上でもキャッシュ機構が働いているし、NSURLなどではそこも含めて隠蔽しているはずなので(未確認だけど)、こまけえこたぁ気に(ryってことなのかもしれない。

しかしNSURLにはそぐわないがヘビーにTCP/IPをつつきたい用途なんかもやっぱりあるわけで、そうするとあとはBSDソケットレイヤーでO_NONBLOCKを...とか言いたくなるところだが、Core FoundationにはCFHostというAPIがあって、こいつで非同期クエリが飛ばせるらしいと知った(しかもNSHostのtoll-free)←勘違いでした(_ _)。

http://developer.apple.com/library/ios/#documentation/CoreFoundation/Reference/CFHostRef/Reference/reference.html
CFHost Reference

おおまかな流れとしては以下の通り。

  1. CFHostCreateWithName()でインスタンス生成
  2. CFHostSetClient()でコールバックを設定
  3. CFHostScheduleWithRunLoop()でメインループに接続
  4. CFHostStartInfoResolution()でクエリ送信
  5. 2.で設定したコールバックでクエリ応答を受信
static void
_host_client_callback(CFHostRef host, CFHostInfoType typeInfo, const CFStreamError *err, void* info)
{
    Boolean resolved = FALSE;
    
    CFArrayRef addr = CFHostGetAddressing(host, &resolved);
    
    NSLog(@"ASYNC: resolved = %d, err = %d/%ld", resolved, err->error, err->domain);
    NSLog(@"ASYNC: result = %@", addr);    
}

static void
_do_query(CFStringRef s)
{
    Boolean ret, resolved;
    CFStreamError err;
    
    CFHostRef host = CFHostCreateWithName(NULL, s);
    CFHostClientContext context = { 0, NULL, CFRetain, CFRelease, NULL };
    
    CFHostSetClient(host, _host_client_callback, &context);
    
    CFHostScheduleWithRunLoop(host, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
    ret = CFHostStartInfoResolution(host, kCFHostAddresses, &err);
    {
        CFArrayRef addr;
        
        NSLog(@"SYNC: ret = %d", ret);
        
        addr = CFHostGetAddressing(host, &resolved);
        
        NSLog(@"SYNC: resolved = %d, err = %d/%ld", resolved, err.error, err.domain);
        NSLog(@"SYNC: result = %@", addr);    
    }
}

CFHostStartInfoResolution()に至る前にCFHostSetClient()を行ってコールバックを設定していなければ、非同期解決にはならずにそのままCFHostStartInfoResolution()でブロックがかかる。

そして_do_query()の引数にCFStringRefが与えられているがこれもNSStringのtoll-free bridgeがかかっているので、呼び出す時はNSStringオブジェクトにキャストして渡してやればよい。

    NSString* s = @"www.google.com";
    _do_query((CFStringRef)s);

そして実行結果は下記のようになる。

2011-05-13 12:17:54.757 CFHostTester[65231:903] SYNC: ret = 1
2011-05-13 12:17:54.760 CFHostTester[65231:903] SYNC: resolved = 0, err = 0/0
2011-05-13 12:17:54.761 CFHostTester[65231:903] SYNC: result = (null)
2011-05-13 12:17:54.860 CFHostTester[65231:903] ASYNC: resolved = 1, err = 0/0
2011-05-13 12:17:54.861 CFHostTester[65231:903] ASYNC: result = (
    <10020000 40e9b769 00000000 00000000>,
    <10020000 40e9b76a 00000000 00000000>,
    <10020000 40e9b793 00000000 00000000>,
    <10020000 40e9b763 00000000 00000000>,
    <10020000 40e9b767 00000000 00000000>,
    <10020000 40e9b768 00000000 00000000>
)

ログの出力に「SYNC:」と「ASYNC:」とそれぞれ付く箇所の出力内容に注意。SYNC:の付く箇所はCFHostStartInfoResolution()を呼んだ直後に取ったデータだが、ここには決して名前解決したデータが乗ってくることはない。何度も同じ名前解決を行って確実にDNSキャッシュに乗ったと思われる状況でも同じだったので、非同期設定を行ったCFHostではコールバックが行われるまではDNS情報はCFHostオブジェクトには渡って来ない、と見るべきなよう。もちろん、非同期設定を行わないつまりCFHostSetClient()およびCFHostScheduleWithRunLoop()を呼ばなければ、普通に「SYNC:」の行で結果は返る。

2010年3月27日

NSViewのbindingには既にfont周りの実装が入っているっぽいぞ。

NSUserDefaultsってNSFontとかNSColorを保存するときってNSArchiver/NSUnarchiver通してNSDataにしてから保存するじゃないですか。まあ、何となく使ってると不便だけど何となくそのしきたりに慣れちゃってそこそこ何となくいい感じになっていってしまうって感じなんですが。

        NSFont* font = [NSFont messageFontOfSize:12];
        NSData* data = [[NSUserDefaults standardUserDefaults] dataForKey:@"defaultFont"];
        if (data) {
            font = (NSFont*)[NSUnarchiver unarchiveObjectWithData:data];
        }

こんな風にですね。

で、最近もう随分とCocoa Bindingsにご厄介になりっぱなしなものだから、NSUserDefaultsの内容もこいつBindingを使って、PreferencesパネルでUserDefaultsを変更したら勝手に追従してくれるようなNSViewがあったらいいじゃんとかぐーたらなこと考えついたわけですよ。

	[myView bind:@"font" toObject:[NSUserDefaultsController sharedUserDefaultsController] withKeyPath:@"values.defaultFont" options:nil];

...

もちろんmyViewのクラスはNSViewから継承した上でこうしておいたわけです。

- (void)bind:(NSString*)binding toObject:(id)observable withKeyPath:(NSString*)keyPath options:(NSDictionary*)options
{
    [super bind:binding toObject:observable withKeyPath:keyPath options:options];

    if ([binding isEqualToString:@"font"]) {
        _fontController = observable;
        [_fontKey release];
        _fontKey = [keyPath copy];
        
        [_fontController addObserver:self forKeyPath:keyPath options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:FontContext];
    }
}

- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context
{
    NSLog(@"change = %@", [change description]);
    if (_fontController == object && [keyPath isEqual:_fontKey]) {
        [self setFont:[object valueForKey:keyPath]];
        
        [self reloadData];
    }
}

unbind:ももちろん実装したけどひとまず割愛。でもって動かしてみたらこうなった。

2010-03-27 01:34:59.954 HogeApp[34125:a0f] Cannot create NSFont from object <040b7374 7265616d 74797065 6481e803 84014084 8484064e 53466f6e 741e8484 084e534f 626a6563 74008584 01692484 055b3336 635d0600 00001a00 0000fffe 4c007500 63006900 64006100 47007200 61006e00 64006500 00008401 660d8401 63009801 98009800 86> of class NSCFData

ああ、そりゃそうだ。setFont:メソッドはこのmyViewのクラスでNSFontのインスタンスを扱うようにしてあるもの。でもNSUserDefaults/NSUserDefaultsControllerから取り出したのがNSDataのインスタンスであっても、setterメソッドくらいは呼ばれるだろ、っと思ってトレースしていっても、どうにも押さえられない。それどころかbind:ofObject:withKeyPath:options:メソッド中でも[super bind:...]した後のfontプロパティ用に独自実装した部分にすらやってこずにエラーになってて、あれーおかしいなって首ひねってたら、どうも面白いことがわかった。

- (void)testExposedBindings
{
	NSView *view = [[HogeView alloc] initWithFrame:NSZeroRect];
	NSLog(@"HogeView exposedBindings = %@", [view exposedBindings]);
	[view release];
}

HogeViewはこうしておく。

@interface HogeView : NSView {
    NSFont* font;
};

@property(retain,nonatomic) NSFont* font;

@end

@implementation HogeView

@synthesize font;

@end

このHogeViewに、fontプロパティを(setter/getterメソッドを実装するか、@propertyを使うか)実装した状態で実行したときと、無い場合(@property行と@synthesize行をコメントアウト)では、自動的にexposedBindingsの結果が違ってるのね。

2010-03-27 02:03:34.658 Intermezzo[34475:a0f] HogeView exposedBindings = (
    fontFamilyName,
    fontSize,
    fontBold,
    hidden,
    fontName,
    font,
    toolTip,
    fontItalic
)
2010-03-27 02:03:50.261 Intermezzo[34505:a0f] HogeView exposedBindings = (
    toolTip,
    hidden
)

前者がfontプロパティ有りのとき、後者が無しのとき。どうもリフレクションか何かが働いてbindingのexposeをよしなにやってくれるらしい。これはちょっとびっくりした。そうか。bind:...メソッドを継承して独自実装とかってのはそもそも要らんわけね。

しかしこのままだとbind:toObject:withKeyPath:options:メソッドが使えない。このメソッドの中でNSUserDefaultsから取り出したNSDataオブジェクトを勝手にNSFontだと勘違いして処理を進めようとしてエラー吐いてるらしいってことなので、何とかしてこれを解決させなければならない。...とここまで来て忘れてましたよ。NSValueTranslatorの存在に。NSDataとNSFontの間を変換してくれるトランスレータを書いてあっさりと解決。


@interface CZFontDataTransformer : NSValueTransformer {
}

+ (Class)transformedValueClass;
+ (BOOL)allowsReverseTransformation;

@end

@implementation CZFontDataTransformer

+ (Class)transformedValueClass
{
	return [NSFont class];
}

+ (BOOL)allowsReverseTransformation
{
	return YES;
}

- (id)transformedValue:(id)value
{
	if ([value isKindOfClass:[NSData class]]) {
		return [NSUnarchiver unarchiveObjectWithData:value];
	}
	return nil;
}

- (id)reverseTransformedValue:(id)value
{
	if ([value isKindOfClass:[NSFont class]]) {
		return [NSArchiver archivedDataWithRootObject:value];
	}
	return nil;
}

@end

もちろんbind時のオプションに指定すんですよ。

	[logView bind:@"font" toObject:[NSUserDefaultsController sharedUserDefaultsController] withKeyPath:@"values.defaultFont" options:
	 [NSDictionary dictionaryWithObject:[[[CZFontDataTransformer alloc] init] autorelease] forKey:NSValueTransformerBindingOption]];

font以外にも同じように面倒見てくれるbindingがあるかどうかは続報を待て。

2009年9月13日

stdout/stderrはclose()するべからず

デバッグログの出力用にNSLog()は普通に使うものだけど、リリースにあたってこの出力が鬱陶しいなと思う事がある。Console.app(コンソール)で見えてしまうというのもあるし、syslogで言うようなログレベルの設定もできないので、エラー時などに重要な意味を持つログメッセージから、「備えあれば憂い無し」的にたまに必要になるかもしれないときのために出しておくデバッグ情報まで、区別無く一カ所に出てしまうというのはどうしても扱いづらいなと感じるものだ。

そんなこんなで、リリースビルドの際にはNSLog()の出力をデバッグファイルにリダイレクトしてConsole.appには出さないような仕組みをIntermezzoには入れていた。おおむね下記の要領で。

int
redirectlog(const char* appname)
{
    char path[64], env[32], *p;
    time_t t;

    snprintf(path, sizeof(path) - 1, "/tmp/%s.debug.%lu.log", appname, time(&t));
    snprintf(env, sizeof(env) - 1, "%s_DEBUG", appname);
    for (p = env; *p; *p++) {
        *p = toupper(*p);
    }
    
    if (getenv(env)) {
        freopen(path, "w", stderr);
    } else {
        fclose(stderr);
    }
    return 0;
}

さてこれでSnow Leopardになって問題が起こった。Leopard時代にはこれで何事もなかったのだが、あるいは問題は発生していたのに発見できていなかったのだろうか。検証できる環境を無くして(=2台あるMacをともにSnow Leopardに上げて)しまったので、真相はひとまず分からなくなってしまったが。

どんな問題かと言うと、IntermezzoがIRCサーバとの通信を確立した直後にいきなり"Excess Flood"で通信を切断されてしまうのだった。どうも見てみると、ログ出力の内容がそのままIRCサーバに向けて送出されているらしい。何のことはない、IRCサーバとの接続でソケットディスクリプタがstderrがcloseされているために空きとなった2番に入ってしまったのである。NSLog()は仕様に沿ってSTDERR_FILENOすなわち2番に出力しようとするので、大量のログがIRCサーバとの間のソケットに書き出されてしまい、floodingと看做されて切断を余儀なくされたと言う訳だ。

という訳で、fclose()を以下のように置き換えて問題を回避することができた。まああとから考えれば自然なことだし、うんそうだよ普通に気付けよ俺っていうか。残るはLeopardでどうだったのか、ということだけども、うーん。どうなんでしょう。

int
redirectlog(const char* appname)
{
    ....

    if (getenv(env)) {
        freopen(path, "w", stderr);
    } else {
        freopen("/dev/null", "w", stderr);
    }
    return 0;
}

2009年9月 4日

Snow Leopardでx86_64非対応なframeworkを使わなければならない場合にどうするか

まだ生きてるよ!(挨拶)

ご他聞に漏れずうちもSnow Leopard入れましたが、Growlが現時点(1.1.6)でどうやらまだ対応してない(公式曰く1.2まで待ってけろとな)、というかx86_64用バイナリを持ってないようですね。

IntermezzoがGrowlを使用しているので、本体のx86_64対応を進めとこうと思ってもビルド時にリンクさせておくと当然エラーが出て怒られる。Growlが動かなくてもかまわないからとりあえずx86_64動作時にもGrowlの如何に関わらず動かしたいと思ったので、実行時の動的ロードに切り替えよっかってしてみたんだけどなかなか動かないからアレッと思ってギアいじったっけロー入っちゃってもうウィリーさ(何)

http://growl.info/documentation/developer/implementing-growl.php?lang=cocoa
Implementing Growl support in your Cocoa Application

ここに書いてあったのにね。

この"Using Cocoa, you can use the following code snipped..."を参考に少し修正したのが下です。

    Class bridgeClass = nil;

    NSString* growlPath = [[[NSBundle mainBundle] privateFrameworksPath] stringByAppendingPathComponent:@"Growl.framework"];
    NSBundle* growlBundle = [NSBundle bundleWithPath:growlPath];
    if (growlBundle && [growlBundle load]) {
        // bridgeClass = [growlBundle principalClass];
        bridgeClass = [growlBundle classNamed:@"GrowlApplicationBridge"];
        
        if ([bridgeClass isGrowlInstalled] == NO || [bridgeClass isGrowlRunning] == NO) {
            // Growl本体がインストールされてないか、動いてないよ
            return;
        }
        
        [bridgeClass setGrowlDelegate:self];
    } else {
        // Growlフレームワークがない、またはロードできないよ
        // x86_64の場合はここに来るよ
        return;
    }

    // ok

GrowlApplicationBridgeクラスがビルド時のリンクで解決できないので、classNamed:メソッドを使いバンドル(フレームワーク)から読み込んでリンクする方向に倒して、ようやく動くようになったとです。Growl.frameworkのInfo.plist読む限りでは、Principal ClassはGrowlApplicationBridgeクラスなので、classNamed:の代わりにprincipalClassメソッドでも行けるはずだけど、将来に渡ってどうかは分かんないのでとりあえずclassNamed:で名指しにしてます。まあそんときゃGrowlApplicationBridgeクラスだってdeprecatedになってるかもしんないけどねー。

まあ、Growl 1.2が出れば多分それで無問題なんだろうけどねー。

(*´・ω・)(・ω・`*)ネー

2009年5月31日

NSStreamの読み込みがコンテクストメニューの表示でふん詰まる件

生きてますよ。(挨拶)

NSInputStreamの取り扱いでずっと気になってたことがあって、ポップアップメニューを表示しているとIRCの接続が切れてしまう現象に遭遇してしまってこれをどう解決しようかちょっと悩んでいた。

まあ、端的に言えばIRCサーバから送られるPINGの受け取り—正確には、TCPレベルではPINGを含むデータストリームが届けられてはいるものの、このデータストリームを取り出して解釈し、PING応答の処理に回す処理が働かないのだな。

http://developer.apple.com/DOCUMENTATION/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW12
Threading Programming Guide: Run Loop Management

直接の要因は、ポップアップメニュー表示中はRun LoopがNSEventTrackingRunLoopModeで回ってしまうこと。通常はNSInputStreamの読み込みはNSDefaultRunLoopModeでしか行われない(というかDefaultモードでの登録しかしない)ので、Run Loopのモードが切り替わってしまうと手も足も出ない。まあ、多くの場合はこれでも問題はないんだけど、IRCの場合はサーバからPINGが来たらなるはやでPONGを返さないとなので、そこでもしポップアップメニューをうっかり出しっぱなしにしてたりなんかすると、下手するとping timeoutで接続切られちゃうんだよな。

当面をしのげればいいやと-[NSInputStream scheduleInRunLoop:forMode:]をNSEventTrackingRunLoopModeで登録してみると、確かにポップアップメニュー表示中もPONGが行えて不都合無く疎通を続けることができる。けど、これじゃ何のためにEventTrackingモードで面倒見るイベントを搾ってるのか意味がなくなってきそう。なのであまり使いたくはない。とするとNSInputStreamの扱いを別スレッドに移してしまうしかないのか。うーん。それをやりたくなかったからNSStreams使ってたのになあ。めんどくさ...。

2009年5月20日

Intermezzo 0.10.5について

10.4(Tiger)対応について、色々と紆余曲折がありましたので。一度0.10.5はアップしたんですが、修正を加えてもう一度上げ直しました。

$ ls -l Intermezzo-0_10_5.dmg 
-rw-r--r-- 1 hironobu hironobu 802374 2009-05-20 13:22 Intermezzo-0_10_5.dmg

このタイムスタンプおよびファイルサイズのものが正規のものとなりますので、よろしくお願いします。