ページ

2009年10月31日土曜日

NSZombieEnabled

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

EXC_BAD_ACCESS の話題が Stack Overflow で上がっていた。

Break on EXC_BAD_ACCESS in XCode? - Stack Overflow

メモリ解放済みのインスタンスへアクセスする時にこのエラーが発生することが多い。デバッグの方法として NSZombieEnabled が紹介されていた。これを使うと解放済みのインスタンスへアクセスすると EXC_BAD_ACCESS の代わりに例外が発生し、エラー原因が発見しやすくなる。

CocoaDev の解説:
CocoaDev: NSZombieEnabled


サンプルプログラムを作って試してみよう。

サンプル:NSZombieEnabledStudy.zip


コードは簡単で、インスタンス変数 _data を初期化後すぐに release し、後でボタンが押された時にこれを参照する。

@interface AppController : NSObject {
NSData* _data;
}
-(IBAction)click:(id)sender;
@end


@implementation AppController

- (void)awakeFromNib
{
    if(getenv("NSZombieEnabled") ||
        getenv("NSAutoreleaseFreedObjectCheckEnabled")) {
            NSLog(@"NSZombieEnabled/NSAutoreleaseFreedObjectCheckEnabled enabled!");
    }

    _data = [[NSData allocinit];
    [_data release];
}

-(IBAction)click:(id)sender
{
    NSLog(@"_data=%@", [_data description]);
}


ボタンを押すと、解放済みの NSData を使おうとするのでプログラムはクラッシュして EXC_BAD_ACCESS を出す(デバッグ実行の場合)。


実行中...
プログラムはシグナルを受信しました:“EXC_BAD_ACCESS”。





続いて NSZombieEnabled を有効にしてみよう。

NSZombieEnabled を有効にする方法はアプリ起動時の引数に与える方法と gdbの設定ファイル(~/.gdbinit)に指定する方法があるようだ。

(参考)

今回は前者の方法を試してみた。Xcode で実行可能ファイルの「情報を見る」を開く。



「環境に設定される変数」へ 名前 "NSZombieEnabled"、値 "YES" として指定する。


デバッグ実行してみる。


NSZombieEnabledStudy[15646:813] NSZombieEnabled/NSAutoreleaseFreedObjectCheckEnabled enabled!
NSZombieEnabledStudy[15646:813] *** -[NSConcreteData description]: message sent to deallocated instance 0x148e90
(gdb)


EXE_BAD_ACCESS の代わりに問題となった箇所が表示されるようになった。これはいい。

※なお、通常の実行(Cmd+R)ではメッセージは出ない。デバッグ実行(Cmd+Y)する。

2009年10月30日金曜日

Safari用独自プラグインを作る(13) - ツールバーを追加する #2

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

(前回)Cocoaの日々: Safari用独自プラグインを作る(12) - ツールバーを追加する #1

Safariのツールバーへボタンを追加する。

ツールバーに関しては過去の検証を参考にした。
Cocoaの日々: ツールバー(その4)
Cocoaの日々: ツールバー(その5)


まずメソッド置換を行うクラスを用意する。コンテキストメニューの時と同じ構成にした。

SXSafariToolbarSwizzler.h

@interface SXSafariToolbarSwizzler : NSObject {
}
+ (void)setup;

@end


次に実装。


SXSafariToolbarSwizzler.m



+ (void)setup
{
if (!_shared_instance) {
_shared_instance = [[SXSafariToolbarSwizzler alloc] init];

[_shared_instance swizzleMethodForClass:objc_getClass("ToolbarController")
orginalSelector:@selector(toolbarDefaultItemIdentifiers:)
alternativeSelector:@selector(_sx_toolbarDefaultItemIdentifiers:)];

[_shared_instance swizzleMethodForClass:objc_getClass("ToolbarController")
orginalSelector:@selector(toolbarAllowedItemIdentifiers:)
alternativeSelector:@selector(_sx_toolbarAllowedItemIdentifiers:)];

[_shared_instance swizzleMethodForClass:objc_getClass("ToolbarController")
orginalSelector:@selector(toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:)
alternativeSelector:@selector(_sx_toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:)];
}
}


swizzleMethodForClass:originalSelector:alternativeSelector: はコンテキストメニューの時と同じ。
Cocoaの日々: Safari用独自プラグインを作る(8) - コンテキストメニューにメニュー追加


- (NSArray *)_sx_toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar
{
NSArray* ids = [self _sx_toolbarDefaultItemIdentifiers:toolbar];
NSLog(@"1:%@", ids);
return ids;
}

- (NSArray *)_sx_toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar
{
NSArray* ids = [self _sx_toolbarAllowedItemIdentifiers:toolbar];
NSLog(@"2:%@", ids);
return ids;
}

- (NSToolbarItem *)_sx_toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
{
NSToolbarItem* item = [self _sx_toolbar:toolbar
 itemForItemIdentifier:itemIdentifier
  willBeInsertedIntoToolbar:flag];
NSLog(@"item: %@", item);
return item;
}


最初は動作を見るために代替メソッドでは単純に元のメソッドを呼び出し、内容をログ出力するだけにしてある。


実行してログ出力を(コンソール.app)で見てみよう。


まず実行直後。



toolbar:itemForItemIdentifier:willBeInsertedToolbar: だけが呼び出された。ボタンの数からして呼び出されるのは Safariのツールバーの初期化が終わった後のようだ。

試しに新規ウィンドウを開くと今度は表示アイコンの数だけ呼び出された。



次にツールバーのあたりで右クリックしてコンテキストメニューから「ツールバーをカスタマイズ...」を選ぶ。


すると最初に toolbarDefaultItemIdentifiers: が呼ばれ、続いて toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar: 、 toolbarAllowedItemIdentifiers: と続く。
"com.evernote.SafariClipperPlugin.ClipToEvernote" が Evernoteの象アイコン。ちゃっかりデフォルト構成にも入っている(コンソール上 1:( ) の箇所)。





アプリケーションを初めて起動した場合は toolbarDefaultItemIdentifiers: が呼ばれ、そこで返されるボタンが初期表示される。その後、カスタマイズが行われるとその状態が保存され、次回起動時は toolbarDefaultItemIdentifiers: は呼び出されない、とリファレンスからは読み取れた。
Mac Dev Center: NSToolbarDelegate Protocol Reference

今回の動作はそんな感じになっている。


Evernote のアイコンは前回も書いた様に、Safari起動後少し間を置いてから表示される。ここだけ見ると、プラグインがロードされたタイミングで Evernote プラグインが後からツールバーに自力でボタンを追加しているように思える。

試しに Evernoteボタンを右の方へ移してみた。そして Safariを再起動する。


元にもどっている。さすがにこのあたり(位置の復元)は難しいか。




(続く)

2009年10月29日木曜日

Safari用独自プラグインを作る(12) - ツールバーを追加する #1

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

(前回)Cocoaの日々: Safari用独自プラグインを作る(11) - スクリーンショットを撮る

コンテキストメニューの次はツールバーを Safariへ追加してみることにする。

Evernote プラグインの場合、インストール直後から現れる。




カスタマイズも他のボタンと同じ様に扱える。

ただ Safari のウィンドウが表示された後、少し経ってからボタンが表示されるようになる。これはプラグイン読み込みのタイミングが Safari全体の初期化の最後の方になるからかもしれない。



ツールバーの追加について、今度は Evernote プラグインのヘッダファイルを参照してみる。
Cocoaの日々: Safari用独自プラグインを作る(7) - Evernote を class-dump する

ENSafariToolbarSwizzler.h
@interface ENSafariToolbarSwizzler : ENSwizzlerHelper
{
}

+ (BOOL)muckWithThisToolbar:(id)arg1;
+ (void)createSharedInstance;
- (BOOL)toolbarContainsElephant:(id)arg1;
- (void)insertElephantIntoToolbar:(id)arg1;
- (void)setupToolbars;
- (id)init;
- (id)toolbarDefaultItemIdentifiers:(id)arg1;
- (id)toolbarAllowedItemIdentifiers:(id)arg1;
- (id)toolbar:(id)arg1 itemForItemIdentifier:(id)arg2 willBeInsertedIntoToolbar:(BOOL)arg3;
- (void)elephantToolbarButtonAction:(id)arg1;
@end

これらのメソッドのうち、toolbar で始まるメソッドは NSToolbarDelegate で定義されているメソッド。
Mac Dev Center: NSToolbarDelegate Protocol Reference

通常ツールバーを作る場合はこの3つのメソッドを実装してやれば良い。


一方、Safari の方で NSToolbarDelegate を実装しているクラスを探すと ToolbarController.h が見つかった。

ToolbarController.h
@interface ToolbarController : NSObject
    
{
   :

このクラスでは NSToolbarDelegate のメソッドの他、ボタン毎に定義された(恐らくプライベートな)メソッドが用意されている。

- (id)toolbarDefaultItemIdentifiers:(id)arg1;
- (id)toolbarAllowedItemIdentifiers:(id)arg1;
- (id)toolbar:(id)arg1 itemForItemIdentifier:(id)arg2 willBeInsertedIntoToolbar:(BOOL)arg3;
- (id)_toolbarItemForBackForward:(BOOL)arg1;
- (void)_prepareBackForwardSegmentedControl:(id)arg1;
- (id)_toolbarItemForInputFields:(BOOL)arg1 attachedButton:(BOOL)arg2;
- (id)_toolbarItemForReportBug:(BOOL)arg1;
- (id)_toolbarItemForAutoFill:(BOOL)arg1;
- (id)_toolbarItemForTopSites:(BOOL)arg1;
- (id)_toolbarItemForHome:(BOOL)arg1;
- (id)_toolbarItemForTextSize:(BOOL)arg1;
- (id)_toolbarItemForDetachedAddBookmark:(BOOL)arg1;
- (id)_toolbarItemForBookmarks:(BOOL)arg1;
- (id)_toolbarItemForHistory:(BOOL)arg1;
- (id)_toolbarItemForMailWebPage:(BOOL)arg1;
- (id)_toolbarItemForShowDownloadsWindow:(BOOL)arg1;- (id)_toolbarItemForShowWebInspector:(BOOL)arg1;
- (id)_toolbarItemForBookmarksBar:(BOOL)arg1;- (id)_toolbarItemForNewTab:(BOOL)arg1;
- (void)_prepareTextSizeSegmentedControl:(id)arg1;
- (id)_toolbarItemForPrint:(BOOL)arg1;
- (id)_toolbarItemForWebClip:(BOOL)arg1;
  :





今回は、NSToolbarDelegateの3つのメソッドを置き換えてみることにする。



なおツールバー用の画像を用意する必要があるが、Evernoteプラグインはこんな感じ(Resourcesフォルダ内、TIFF、18x18ピクセル)。


(続く)

2009年10月28日水曜日

Safari用独自プラグインを作る(11) - スクリーンショットを撮る

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

(前回)Cocoaの日々: Safari用独自プラグインを作る(10) - コンテキストメニューの調査

さていよいよ本題の(?)スクリーンショットを試す。コンテキストメニューへの独自メニュー追加はできているので、ここにスクリーンショットを撮る処理を追加する。

まずメニュー追加の箇所。

SXSafariContextMenuSwizzler.m

- (NSArray *)_sx_webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element
defaultMenuItems:(NSArray *)defaultMenuItems {

NSArray* menu_items = [self _sx_webView:sender
contextMenuItemsForElement:element
  defaultMenuItems:defaultMenuItems];

NSMutableArray* new_menu_items = [NSMutableArray arrayWithArray:menu_items];

[new_menu_items addObject:[NSMenuItem separatorItem]];

NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:@"Take a screenshot"
            action:@selector(takeScreenshot:)
            keyEquivalent:@""] autorelease];

[item setRepresentedObject:sender];
[item setTarget:_shared_instance];
[new_menu_items addObject:item];

return new_menu_items;
}


NSMenuItem の representedObject に WebView インスタンスを渡してやる。


そしてスクリーンショットを撮る処理。

- (void)takeScreenshot:(id)sender
{
WebView* web_view = [sender representedObject];
NSView* doc_view = [[[web_view mainFrame] frameView] documentView];

NSBitmapImageRep* bitmap = [doc_view bitmapImageRepForCachingDisplayInRect:[doc_view bounds]];
[doc_view cacheDisplayInRect:[doc_view bounds] toBitmapImageRep:bitmap];

NSData* outdata = [bitmap representationUsingType:NSPNGFileType
                properties:[NSDictionary dictionary]];
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES);
NSString* path = [NSString stringWithFormat:@"%@/test.png", [paths objectAtIndex:0]];
[outdata writeToFile:path atomically:YES];

NSLog(@"took a screenshot");
}


これは以前検証した時のコードを参考にした

Cocoaの日々: WebKit検証(3) - ビューを画像に保存
Cocoaの日々: WebKit検証(4) - Webのキャプチャ


さて実行してみよう。

apple.com を撮影してみる。まずページを開く。



コンテキストメニューから "Take a screenshot" を選択する。



デスクトップに画像ファイルが作成された。



できた。



おお、いい感じだ。


ついでに Flash ページも見ておく。Flashが含まれるページを開く。




撮影すると。。

出た。



Flash でも問題ないな。
以前の検証ではこの方法を使った場合 Flashの色がおかしかったが今回は問題なかった。 Safariをバージョンアップしたせいかもしれない(以前の検証時は バージョン 3、現在はバージョン 4)。

(以前のFlash検証)Cocoaの日々: WebKit検証(11) - Flash





2009年10月27日火曜日

Safari用独自プラグインを作る(10) - コンテキストメニューの調査

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

(前回)Cocoaの日々: Safari用独自プラグインを作る(9) - Method Swizzling 補足

-[BrowserWebView webView:contextMenuItemsForElement:defaultMenuItems:] で渡される引数を調べてみた。webView は良いとして残りの二つをデバッグ出力してみた。

- (NSArray *)_sx_webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element
defaultMenuItems:(NSArray *)defaultMenuItems {

NSLog(@"element: %@", element);
NSLog(@"defaultMenuItems: %@", defaultMenuItems);


例えば「ログイン」リンクで右クリックすると


こんなものが得られる。
element: {
    WebElementDOMNode = <DOMText [#text]: 0x6bf3000 'ログイン'>;
    WebElementFrame = <WebFrame: 0x4a6a70>;
    WebElementIsContentEditableKey = 0;
    WebElementIsSelected = 1;
    WebElementLinkIsLive = 1;
    WebElementLinkLabel = "\U30ed\U30b0\U30a4\U30f3";
    WebElementLinkURL = https://www.google.com/accounts/ServiceLogin?
      continue=http://www.google.co.jp/ig%3Fhl%3Dja%26source%3Diglk&
      followup=http://www.google.co.jp/ig%3Fhl%3Dja%26source%3Diglk&
      service=ig&passive=true&cd=JP&hl=ja&nui=1&ltmpl=default;
    WebElementTargetFrame = &;tWebFrame: 0x4a6a70>;
} 

defaultMenuItems: (
    <menuitem: 0x51c4b20="" リンクを開く="">,
    <menuitem: 0x51dd5f0="" リンクを新規ウインドウで開く="">,
    <menuitem: 0x484550="" リンク先のファイルをダウンロード="">,
    <menuitem: 0x6e95410="" リンクをコピー="">,
    <menuitem: 0x6a66fb0="">,
    <menuitem: 0x6e951c0="" 要素の詳細を表示="">
) 

element には DOM情報および、WebElementFrame のインスタンス情報などが得られる。文字列などが選択されている時には WebElementIsSelected =1 となる。またリンクの場合は WebElementLinkIsLive =1 となる。一方、defaultMenuItems はその名の通り Safariで用意されているデフォルトのメニューの一覧のようだ。

追加した sayHello: で渡される引数 sender は通常のメニューアクション同様 NSMenuItemのインスタンスが得られる。

- (void)sayHello:(id)sender
{
NSLog(@"%@", sender);
}



コンソール:
Safari[16335] <MenuItem: 0x5ad19a0 Say hello>


なお webView:contextMenuItemsForElement:defaultMenuItems: は右クリックの度に呼び出される(だからコンテキストメニューといえるが)。そこで sayHello: 内で WebView を手っ取り早く得るには -[NSMenuItem setRepresentedObject:] 経由で渡してやればよさそうだ。

- (NSArray *)_sx_webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element
defaultMenuItems:(NSArray *)defaultMenuItems {
                  :
NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:@"Take a screenshot"
                             action:@selector(takeScreenshot:)
                              keyEquivalent:@""] autorelease];
[item setRepresentedObject:sender];




                  :



- (void)sayHello:(id)sender
{
NSLog(@"%@", [sender representedObject]);
}



こうなる。
09/10/26 13:10:39 Safari[16429] <BrowserWebView: 0x44b660> 

※BrowserWebView は Safari独自の WebViewのサブクラス。

2009年10月26日月曜日

Safari用独自プラグインを作る(9) - Method Swizzling 補足

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

(前回)Cocoaの日々: Safari用独自プラグインを作る(8) - コンテキストメニューにメニュー追加

前回の swizzleMethodForClass( ) の補足。


-(void)swizzleMethodForClass:(Class)cls orginalSelector:(SEL)org_sel alternativeSelector:(SEL)alt_sel
{
    Method org_method = class_getInstanceMethod(cls, org_sel);
 class_addMethod(cls, alt_sel,
  class_getMethodImplementation([self class], alt_sel),
  method_getTypeEncoding(org_method));

    Method alt_method = class_getInstanceMethod(cls, alt_sel);
    method_exchangeImplementations(org_method, alt_method);



処理をイメージ化してみた。こんな感じ。


1. まず class_addMethod()を使い、BrowserView へ代替メソッド(_sx_webView:...)を追加する。



2. 次に method_exchangeImplementations() を使い、メソッドの実装を交換する。



3. webView:.. メッセージが投げられると、代替実装が実行される。



class_addMethod( ) の第3引数は、代替メソッドの IMP(実態は関数ポインタ) を渡す必要がある。このために [self class]、すなわち SXSafariContextMenuSwizzler クラスと、代替メソッドのセレクタ(_sx_webView:..)を使い IMP を取得している。

class_getMethodImplementation([self class], alt_sel),


また引数の型情報を得るのに元メソッドの情報を利用している。

method_getTypeEncoding(org_method));


- - - -
SXSafariContextMenuSwizzler を BrowserWebViewのサブクラスにしたり、代替メソッドを BrowserWebView のカテゴリとすれば method_exhcangeImplementations( ) だけで良さそう。class_addMethod( ) は(多分)不要になる。

2009年10月25日日曜日

動画キャプチャソフト〜スクリン・クロラ を試してみる

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

将来何かの役に立つかと思い画面上の操作を動画に落とすソフトを調べてみた。


スクリン・クロラ
(これは昔 Lilypad と呼ばれていたソフトだろうか?)



ネーミングやマニュアル、インタビューなど作品仕立てのテイストがちょっとユニークで面白い。

ダウンロードして試してみた。


起動すると画面が暗くなり範囲選択とメッセージが現れる。録画したい範囲を決めることができる。またカーソルの動きにこの範囲を追随することもできるようだ。




作成された動画は MPEG4形式。





- - - -
試した環境が非力(PowerPC G4 1.25GHz)のせいか、コマ落ちが多かった。今時のマシンなら問題ないかもしれない。

2009年10月24日土曜日

WebKit Plug-in を Xcodeでデバッグする

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

WebKit の Plug-in を Xcodeでデバッグする記事がたまたま見つかった。


Technical Q&A QA1500: Debugging a WebKit Plug-in in Xcode


さらにGoogleで検索すると日本語での解説記事を griffin-stewie さんが書いている記事も見つかった。
SIMBL Plugin 開発でデバッガを使う方法 - griffin-stewieの日記
詳しい説明が図付き紹介されている。


今度試してみよう。

2009年10月23日金曜日

[情報] stackoverflow - tagged: objective-c

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

最近 stackoverflow を RSSでチェックしている。
Hottest 'objective-c' Questions - Stack Overflow(英語)



主に MacOSXプログラミングに関係する投稿を追っているのだが、タグはこんなものがある。
Hottest 'cocoa-touch' Questions - Stack Overflow
Hottest 'iphone-sdk' Questions - Stack Overflow
Hottest 'objective-c' Questions - Stack Overflow
Hottest 'cocoa' Questions - Stack Overflow


Objective-C 関連だと1日10〜20程度の Questions 投稿があるのだが今ぐらいの投稿だと多すぎず少なすぎずと行った感じで英語であっても追っていくのが苦にならない。プログラミングに関する様々な話題が上がっていてなかなか重宝している。


最近のエントリから。

メモリ管理の質問(回答を見る前に自分で考えてみると良いかも)
More Specific, General Objective-C Memory Management. - Stack Overflow

Snow Leopard と Leopard で動くコードを書くには?という話題。
One code base for Snow Leopard and Leopard - Stack Overflow

参考まで。

2009年10月22日木曜日

Safari用独自プラグインを作る(8) - コンテキストメニューにメニュー追加

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

(前回)Cocoaの日々: Safari用独自プラグインを作る(7) - Evernote を class-dump する

これまでの調査を元に Safari のコンテキストメニューへオリジナルのメニューを追加してみよう。

Safari ではコンテキストメニューに表示する NSMenuItem の配列を BrowserWebView の次のメソッドで取得するようだ。

- (id)webView:(id)arg1 contextMenuItemsForElement:(id)arg2 defaultMenuItems:(id)arg3;

Evernote はこのメソッドの置き換えているようなので、今回は同じようにこのメソッドを置き換えてみる。


まずコンテキストメニューに関する処理を受け持つ専用のクラスを用意した。名前は Evernote のそれをまねした。

SXSafariContextMenuSwizzler.h
@interface SXSafariContextMenuSwizzler : NSObject {
}
+ (void)setup;
@end

SXSafariContextMenuSwizzler.m
static SXSafariContextMenuSwizzler* _shared_instance = nil;
+ (void)setup
{
 if (!_shared_instance) {
  _shared_instance = [[SXSafariContextMenuSwizzler alloc] init];

  [_shared_instance swizzleMethodForClass:objc_getClass("BrowserWebView")
   orginalSelector:@selector( 
    webView:contextMenuItemsForElement:defaultMenuItems:)
   alternativeSelector:@selector(
    _sx_webView:contextMenuItemsForElement:defaultMenuItems:)];
 }
}

+[setup] でインスタンスを作り、置換メソッド swizzleMethodForClass:orginalSelector:alternativeSelector を使いメソッドを置き換える。

その置換メソッドはこんな感じ。
-(void)swizzleMethodForClass:(Class)cls orginalSelector:(SEL)org_sel alternativeSelector:(SEL)alt_sel
{
    Method org_method = class_getInstanceMethod(cls, org_sel);
 class_addMethod(cls, alt_sel,
  class_getMethodImplementation([self class], alt_sel),
  method_getTypeEncoding(org_method));

    Method alt_method = class_getInstanceMethod(cls, alt_sel);
    method_exchangeImplementations(org_method, alt_method);

}
メソッドを追加した後、method_exchangeImplementations() でメソッドを置換している。class_addMethod( ) の引数は若干ややこしいかもしれない。


続いて置換した代替メソッド。
- (NSArray *)_sx_webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element
defaultMenuItems:(NSArray *)defaultMenuItems {
 
 NSArray* menu_items = [self _sx_webView:sender
     contextMenuItemsForElement:element
         defaultMenuItems:defaultMenuItems];
 
 NSMutableArray* new_menu_items = [NSMutableArray arrayWithArray:menu_items];

 [new_menu_items addObject:[NSMenuItem separatorItem]];

 NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:@"Say Hello"
     action:@selector(sayHello:)
  keyEquivalent:@""] autorelease];
 [item setTarget:_shared_instance];
 [new_menu_items addObject:item];
 return new_menu_items;
}

最初にオリジナルメソッドを呼び出して MenuItem の配列を取得し、そこへ独自の MenuItem を追加する。ターゲットには+[setup]でとっておいた _shared_instance を割り当てる。

続いてメニューが選択された時の処理。ここではログへ文字列を表示してみた。
- (void)sayHello:(id)sender
{
 NSLog(@"Hello !");
}

最後にコントローラ。プラグインが読み込まれ +[load] が呼び出されたら、メソッドの置換を行う。
static BOOL initialized_flag = NO;
+(void)load
{
 if (!initialized_flag) {
  initialized_flag = YES;
  [SXSafariContextMenuSwizzler setup];
  NSLog(@"+[PluginController load] was called");
 }
}

さて実行してみよう。右クリックでコンテキストメニューを表示させる。

出た。

"Say Hello" を選ぶ。



"Hello !" がログに出た。


これでユーザからの操作を受け取る入り口が一つ確保できた。


ソースコード:SafariPlugInStudy-1.zip


----------------
(参考情報)
下記で公開されている SIMBLプラグインのソースコードも参考になった。
[SIMBL]Safari のコンテキストメニューから項目を削除するプラグイン書いた - Yarukidenized:ヤルキデナイズド
それと GitHub が便利そうだ。ダウンロードせずにソースコードが読めるのがいい。このブログのソースコードもいつか移していきたい(その前に Git を習得しなければならないが)。

2009年10月21日水曜日

Safari用独自プラグインを作る(7) - Evernote を class-dump する

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

(前回)Cocoaの日々: Safari用独自プラグインを作る(6) - Safari のクラス構成

Safari を class-dump した後、Evernote のプラグインが気になったので class-dump してみた。

$ class-dump -H -o evernote ~/Library/Internet\ Plug-Ins/
  EvernoteSafariClipperPlugin.webplugin/Contents/
  MacOS/EvernoteSafariClipperPlugin
※見やすい様に改行を入れてある。


AGProcess-MachTaskEvents.h
AGProcess-Private.h
AGProcess-Signals.h
AGProcess.h
CDStructures.h
ENClipper.h
ENMIMEUtils.h
ENSafariClipper.h
ENSafariClipperPluginView.h
ENSafariContextMenuSwizzler.h
ENSafariToolbarSwizzler.h
ENSwizzlerHelper.h
NSObject-Protocol.h
NSString-NSStringAdditions.h
WebPlugInViewFactory-Protocol.h

Info.plist によると Principal class(最初にロードされるクラス)は、 ESSafariClipperPluginView となっていた。



引用すると:

ENSafariClipperPluginView.h
#import "NSView.h"

#import "WebPlugInViewFactory-Protocol.h"

@interface ENSafariClipperPluginView : NSView <WebPlugInViewFactory>
{
}

+ (void)initialize;
+ (id)plugInViewWithArguments:(id)arg1;
+ (id)mimePListPath;
- (void)deletePListFile;
- (void)delayedPListFileDelete;

@end

よく見ると WebPlugInViewFactoryを実装し、NSViewのサブクラスとなっている。一方、他のヘッダファイルを見たが WebPlugIn(非形式プロトコル)のメソッドは実装していない。一部のみ Webkit-Plugin の作法に準拠させているようだ。それと初期化は +[load] ではなく +[initialize] で行っている。

眺めていると他に Swizzler と名のつくファイルがいくつかあった。
ENSafariContextMenuSwizzler.h
ENSafariToolbarSwizzler.h
ENSwizzlerHelper.h 
このあたりのクラスがメソッド置換を受け持っているようだ。前2つのクラスは名前からしてコンテキストメニューとツールバーに関連していることがわかる。

定義を覗いてみよう。

ENSafariContextMenuSwizzler.h
#import "ENSwizzlerHelper.h"

@interface ENSafariContextMenuSwizzler : ENSwizzlerHelper
{
}

+ (void)createSharedInstance;
- (id)init;
- (id)webViewFromElementInfo:(id)arg1;
- (void)contextMenuAddItemChosen:(id)arg1;
- (void)contextMenuAddPage:(id)arg1;
- (void)contextMenuAddPageAsPDF:(id)arg1;
- (id)webView:(id)arg1 contextMenuItemsForElement:(id)arg2 defaultMenuItems:(id)arg3;

@end

定義では Safariで定義されているクラス BrowserWebView のメソッド -[BrowserWebView webView:contextMenuItemsForElement:defaultMenuItems:] がここで定義されている。なるほどコンテキストメニューへの干渉はこのメソッドを置換すれば良いのか。

同様に ENSafariToolbarSwizzler.h を見ると、Safariの ToolbarController.h で定義されたメソッドが存在する(toolbar*で始まるメソッドなど)。

ENSafariToolbarSwizzler.h
#import "ENSwizzlerHelper.h"

@interface ENSafariToolbarSwizzler : ENSwizzlerHelper
{
}

+ (BOOL)muckWithThisToolbar:(id)arg1;
+ (void)createSharedInstance;
- (BOOL)toolbarContainsElephant:(id)arg1;
- (void)insertElephantIntoToolbar:(id)arg1;
- (void)setupToolbars;
- (id)init;
- (id)toolbarDefaultItemIdentifiers:(id)arg1;
- (id)toolbarAllowedItemIdentifiers:(id)arg1;
- (id)toolbar:(id)arg1 itemForItemIdentifier:(id)arg2 willBeInsertedIntoToolbar:(BOOL)arg3;
- (void)elephantToolbarButtonAction:(id)arg1;

@end

なるほど。

なお、メソッド名に"elephant" と名付けているところがちょっと面白い(Evernoteのアイコンは象の顔)。

上記2つのクラスの親クラスにあたる ENSwizzlerHelper はメソッド置換のメソッドを提供しているようだ。

ENSwizzlerHelper.h
#import "NSObject.h"

@interface ENSwizzlerHelper : NSObject
{
}

- (void *)swizzleInstanceMethodWithSelector:(SEL)arg1 fromClass:(Class)arg2;

@end

セレクタを一つしか渡さないところを見ると、サブクラスでターゲットと同じメソッドを定義しているので暗黙的にそれを置換先のセレクタとしているのかもしれない。今まで見てきたメソッド置換コードの多くは適当なメソッド名を割り当てていたので、それに比べてスマートな感じがして面白い。後で(実装を想像して)自分で書いてみよう。

その他、Evernote本体と連携する目的と思われる AGProcess* などが用意されている。

- - - -
ヘッダファイルだけでもずいぶんと参考になった。
こういった解析はちょっと楽しい。
これらを参考にして Safariへの侵入にとりかかることにしよう。

2009年10月20日火曜日

Safari用独自プラグインを作る(6) - Safari のクラス構成

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

(前回)Cocoaの日々: Safari用独自プラグインを作る(5) - class-dump

前回生成した Safari を構成するクラスのヘッダファイルを簡単に整理してみた。括弧内はファイルの数。例えば、AB* 系は ABAddressBook や ABHomePagesController, ABPerson などがある。右側の * は簡易グラフ。機能構成の目安になるかと思いつけてみた(多いほど複雑だったり、重要な役割を果たすもの)。

目視なので数はおおざっぱなものとして見てほしい(区分けも恣意的)。

AB*                        ( 3) ***
Accepted*                  ( 4) ****
Accessibility*             ( 2) **
Activity*                  ( 4) ****
AddressBookMatch           ( 1) *
AppController*             ( 4) ****
AutoFill*                  ( 2) **
Banner*                    ( 5) *****
Black*                     ( 4) ****
Bonjour*                   ( 4) ****
Bookmark*                  (16) ****************
Browser*                   (29) ******************************
Bug*                       ( 2) **
Button*                    ( 3) ***
CacheController            ( 1) *
CancellableActivity        ( 1) *
CDStructures               ( 1) *
CertificateUtilities       ( 1) *
ClippedItems*              ( 2) **
ClosedWebViewHolder        ( 1) *
ColoredRect                ( 1) *
Completion*                ( 5) *****
Cropping*                  ( 2) **
CustomDisabled*            ( 1) *
DashboardWebClip*          ( 2) **
DateCell                   ( 1) *
DebugUtilities             ( 1) *
DefaultWebApp*             ( 1) *
DevelopMenuController      ( 1) *
DigitStringFormatter       ( 1) *
DiskArbitrationHelper2     ( 1) *
DoJavaScriptCommand        ( 1) *
DOM*                       ( 2) **
Download*                  ( 8) ********
EmailContents              ( 1) *
EtchedString*              ( 2) **
EventSending*              ( 3) ***
FadeIn/Fading*             ( 4) ****
Favorite*                  ( 6) ******
FileLocker*                ( 2) **
Find*                      ( 2) **
FlowView*                  ( 5) *****
Form*                      ( 4) ****
FrameProgressEntry         ( 1) *
GlobalHistory*             ( 2) **
Heartbeat                  ( 1) *
History*                   ( 3) ***
HTMLSource*                ( 4) ****
HTTPAuthentication*        ( 1) *
ImageAndTextCell*          ( 2) *
ImageOverlayView           ( 1) *
InnerSearchFieldDelegate   ( 1) *
ISyncController            ( 1) *
KeyValuePair               ( 1) *
ListView                   ( 1) *
LKView                     ( 1) *
LoadProgressMonitor        ( 1) *
Location*                  ( 7) *******
MessageReceivingPortReader ( 1) *
MiscFormsDataEditor        ( 1) *
MorphingDragImage*         ( 4) ****
MultipartForm*             ( 2) **
MutableDraggingInfo        ( 1) *
NetworkController          ( 1) *
NewBookmarks*              ( 2) **
NewTabButton               ( 1) *
Old*                       (12) **********
OtherUserAgent*            ( 1) *
OutlineView*               ( 2) *
Overlay*                   ( 4) ****
PageLoad*                  ( 5) *****
PasswordsEditor            ( 1) *
PDFLoadingProgress*        ( 3) ***
PlasticButton*             ( 2) **
PlatformFrameLoad*         ( 1) *
PreferencesModule          ( 1) *
PrintingAccessory*         ( 1) *
Purple*                    ( 3) ***
REnderTree*                ( 2) **
ReopensAtLaunch*           ( 1) *
ResetDialogController      ( 1) *
ResourceProgressEntry      ( 1) *
Rollover*                  ( 6) ******
RoundRectView              ( 1) *
RSS*                       ( 2) **
Safari*                    ( 6) ******
Search*                    ( 5) *****
Secure*                    ( 7) *******
ServicesProvider           ( 1) *
SharedLocalizedStrings     ( 1) *
Sheet*                     ( 2) **
ShowBookmarks              ( 1) *
SimpleSheetRequest         ( 1) *
SingleFeedSyncBookmark     ( 1) *
SiteDatabase*              ( 1) *
Sliding*                   ( 7) *******
SnapshotFetcher*           ( 2) **
SnippetEditorDelegate      ( 1) *
SourceListSection*         ( 1) *
SpinningProgressIndicator  ( 1) *
SplitView*                 ( 2) *
Spotlight*                 ( 5) *****
Stopwatch                  ( 1) *
Stress*                    ( 2) **
SyncBookmark*              ( 2) **
TabBar*                    ( 3) ***
TabButton*                 ( 2) **
Table*                     ( 2) **
TabLocation                ( 1) *
Text*                      ( 3) ***
TitleBarButton             ( 1) *
ToolbarController          ( 1) *
TopSites*                  ( 4) ****
TransparentOverlayWindow   ( 1) *
UndoRemoveInfo             ( 1) *
ViewTree*                  ( 2) **
VoiceOverLayerInfo         ( 1) *
Watchdog                   ( 1) *
WebBookmark*               (12) ************
WebClipBanner*             ( 2) **
WebDocument*               ( 6) ******
WebFormDelegate            ( 1) *
WebFrame*                  ( 2) **
WebHTMLRepresentation      ( 1) *
WebIconMenuItem*           ( 2) **
WebSearchField*            ( 3) ***
WebSecurityOrigin          ( 1) *
WebStringTruncator         ( 1) *
WebView*                   ( 3) ***
Window*                    ( 4) ****

*Animation                 ( 4) ****
*Preferences               ( 6) ******

その他に NS*系が 93個。ほとんどがカテゴリによるメソッド追加。

もう少し眺めてみよう(続く)。

2009年10月19日月曜日

Safari用独自プラグインを作る(5) - class-dump

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

(前回)Cocoaの日々: Safari用独自プラグインを作る(4) - Method Swizzling を試す

前回まででメソッドの置換(交換)方法がわかった。さて、ここまでは良かったがこれからどうすれば良いのだろうか。はたと困ってしまった。

ここで一度プラグインの動作イメージを確認してみることにした。

動作イメージとしては、 (1)何らかの方法によってユーザからの指示を受け付けて (2)表示中のWebページのキャプチャを実行,画像をファイルへ保存する、ということがやりたい。(3)またプリファレンスを用意する必要があるかもしれない。

整理するとこんな機能が必要。

(1) ユーザインタラクション(入力)
(2) キャプチャ(出力)
(3) プリファレンス(設定)


(1)は、メインメニューやツールバーボタン、コンテキストメニューなどが考えられる。Safari へ新規にメニューを追加することは比較的容易にできそうだが、ツールバーボタンはちょっとわからない。それ以外に独自の U/I を追加することも考えられる(ビュー上にボタンを表示してしまうとか)。

(2)は、キャプチャ処理自体は過去にさんざんやってきたので問題ない。ただどうやって表示中の WebViewなどを手に入れるか、それが課題。

(3)は、最悪無くてもかまわない。必要になったら考えよう。


上記を念頭におきつつ、メソッド置換を行う対象のクラスとメソッドを決める必要がある。
そこで Safariのクラスを調べることにした。

クラスの定義を知るには class-dump という便利なコマンドラインツールが使える。このツールを使うと指定したアプリケーションの中で使われているクラスのヘッダファイル(*.h)を生成することができる。

Code the Code - Projects - class-dump

(参考)木下さんの紹介記事
【コラム】ダイナミックObjective-C (1) CocoaとObjective-Cと動的なオブジェクト指向 - Cocoaハックの第1歩 | エンタープライズ | マイコミジャーナル


SIMBL向けのプラグインのソースを見るとたいてい Safari.h というファイルがXCodeプロジェクトに含まれているが、恐らく class-dump(もしくは同等のツール)を使って生成したものと思われる


class-dump を使い、Safariのヘッダファイルを作ってみよう。


まずダウンロードしてきて適当なフォルダへコピーする。ターミナル.appで動作確認。

$ ./class-dump 
class-dump 3.3.1 (32 bit)
Usage: class-dump [options] <mach-o-file>

  where options are:
        -a             show instance variable offsets
        -A             show implementation addresses
        --arch <arch>  choose a specific architecture from a universal binary (ppc, ppc64, i386, x86_64)
        -C <regex>     only display classes matching regular expression
        -f <str>       find string in method name
        -H             generate header files in current directory, or directory specified with -o
        -I             sort classes, categories, and protocols by inheritance (overrides -s)
        -o <dir>       output directory used for -H
        -r             recursively expand frameworks and fixed VM shared libraries
        -s             sort classes and categories by name
        -S             sort methods by name
        -t             suppress header in output, for testing
        --list-arches  list the arches in the file, then exit
$


Safariをターゲットにして実行してみる。

(実行環境)Mac OS X 10.5.8 (PowerPC G4) / Safari 4.0.3

./class-dump /Applications/Safari.app/Contents/MacOS/Safari  > Safari.h

生成された Safari.h はこんな感じ。

Safari.h
/*
 *     Generated by class-dump 3.3.1 (32 bit).
 *
 *     class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2009 by Steve Nygard.
 */

#pragma mark Named Structures

struct AlignedBuffer<128ul, 4ul> {
    char buffer[128];
};

struct BlackButtonLayer;

struct Bookmark {
    void **_field1;
    int _field2;
     :
     :
     :
struct CGRect {
    struct CGPoint _field1;
    struct CGSize _field2;
};

struct CGSize {
    float _field1;
    float _field2;
};
     :
     :
@interface BarBackground : NSView
{
    BOOL _hasTopBorder;
    BOOL _hasBottomBorder;
    BOOL _mouseDownCanMoveWindow;
    BOOL _scopeBarAppearance;
    NSColor *_backgroundColor;
    NSColor *_tintColor;
    NSColor *_bottomBorderColor;
    NSView *_firstChildKeyView;
     :
     :

最初に構造体の定義があり、@protocol, @interface と定義が続く。AppKit や Foudation.framework など Safari固有でない定義も全部書き出されている。

試しに -H オプションをつけてみた。
./class-dump -H -o headers /Applications/Safari.app/Contents/MacOS/Safari

headers/ フォルダの下に定義毎に分解されたヘッダファイルが生成される。

$ ls headers
ABAddressBook-BrowserExtras.h
ABHomePagesController.h
ABPerson-BrowserExtras.h
AcceptedCookies.h
AcceptedDatabases-FileInternal.h
AcceptedDatabases.h
AcceptedServices.h
AccessibilityButtonInfo.h
AccessibilityChildInfo.h
   :
WebFrame-HistoryTextCache.h
WebHTMLRepresentation-CreditCardAutoFillExtras.h
WebIconMenuItem-FileInternal.h
WebIconMenuItem.h
WebSearchField-FileInternal.h
WebSearchField.h
WebSearchFieldCell.h
WebSecurityOrigin-SafariExtras.h
WebStringTruncator-BrowserExtras.h
WebView-SafariSnapshotGeneration.h
WebViewPlus-FileInternal.h
WebViewPlus.h
Window.h
WindowController.h
WindowDelegate-Protocol.h
WindowReopener.h

全部で 452ファイルが作られていた。
実際に使う場合はこれらの中から必要なものだけチョイスするという感じだろうか。

とりあえずこれらを眺めてみることにする。

(続く)

2009年10月18日日曜日

[本] Mac OS X Cocoaプログラミング 第3版

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

あのヒレガス本の最新版(第3版)の日本語版が発売されるとのこと。

Mac OS X Cocoaプログラミング 第3版 - 和書 - ピアソン


日本語版の初版が出たのが 2002年というから実に 7年ぶりの改訂。アメリカでは 2008年5月に発売され3万部を売り上げる(プログラミング本では)ベストセラーになったらしい。

MacOSXのプログラミングはこの本で始めたので少々感慨深い。


ピアソンのページから内容について引用させてもらう。

Mac OS X 開発者も、iPhone開発者も必携!

2002年に発売され、Mac OS X開発者から好評を博した『Mac OS X Cocoaプログラミング』の改訂版です。今回改訂された日本語版の原著第3版は、2008年5月の発売以来3万部を売り上げ、本格的なプログラミング書としてはベストセラーになっています。
本書は、Mac OS Xアプリケーション開発において、最もよく使われる開発ツールであるXcode、Interface Builder、Instrumentsを紹介します。またObjective-Cと、Cocoaの主要なデザインパターンについても網羅しています。収録されている数多くのコードを読み進めることで、Cocoa コミュニティが使用しているイディオムにも馴染めるようになるよう構成されています。
最新のMac OS Xの機能をフルに利用したアプリケーションを開発するためのテクニックが満載されており、Mac OS Xでの開発者はもちろん、iPhoneプログラマにも必読の書といえるでしょう。

また日本語版では現状にあった変更が加えられているとのこと。
(日本語版の特徴)
 2009年8月リリースのMac OS X 10.6 Snow Leopard、およびXcode 3.2 に合わせ、本書中の解説、画面イメージをすべてXcode 3.2 に対応したものに変更しています。なお、以前のXcodeを使用している方々にも本書が役立つよう、適宜解説を付け加えています。
第16章「ローカライズ」は、アプリケーションを日本語対応に変更しました(原書ではフランス語対応)。第28章「Webサービス」は、AmazonWebサービス(AWS)を用いた検索アプリケーションの作成でしたが、2009年8月よりAmazon側の仕様が変更されたこともあり、Yahoo! JAPANのWebサービスを用いた検索アプリケーションの作成という内容に差し替えています。

発売は11月予定。価格は 4,200円(予定)。訳者は前回と同じ村上雅章さん。

楽しみだ。


(参考)
原書(第三版 - 2008年販売)


日本語版(第一版 - 2002年発売)

2009年10月17日土曜日

Safari用独自プラグインを作る(4) - Method Swizzling を試す

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク


(前回)Cocoaの日々: Objective-C 2.0 ランタイムの情報

2.0 ランタイムから追加された method_exchangeImplementations を使ってメソッドを置換してみる。

具体的な方法は下記のサイトが参考になった。
MethodReplacement
CocoaDev: MethodSwizzling

Mac Dev Center のコードがシンプルでとてもわかりやすい。

が、ここでは Cocoa Dev のたくさん方法が書いてある中で下記のコードを使ってみた。
void Swizzle(Class c, SEL orig, SEL new)
{
    Method origMethod = class_getInstanceMethod(c, orig);
    Method newMethod = class_getInstanceMethod(c, new);
    if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
        class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    else
 method_exchangeImplementations(origMethod, newMethod);
}

このコードでは最初にメソッドの追加を試みてメソッドを置き換えている。カテゴリを使う場合は常に exchangeImplementations が使われる。

実際にこれを試してみよう。


以前と同様に -[NSWindow sendEvent:] を置き換えてみる。

オリジナル: sendEvent:
新メソッド: _xc_sendEvent:

今回はサブクラスでは無くカテゴリを使ってみた。

 準備として、まず NSWindowのカテゴリを定義する。
@interface NSWindow (XCCateogry)
- (void)_xc_sendEvent:(NSEvent *)theEvent;
+ (void)_swizzleMethods;
@end

@implementation NSWindow (XCCategory)
- (void)_xc_sendEvent:(NSEvent *)theEvent;
{
 if ([theEvent type] == NSRightMouseDown) {
  NSLog(@"[1] NSRightMouseDown");
 }
 [self _xc_sendEvent:theEvent];
}
+ (void)_swizzleMethods
{
 Swizzle([NSWindow class], @selector(sendEvent:), @selector(_xc_sendEvent:));
}
@end

ポイントはオリジナルメソッドの呼び出しで自身のメソッドを呼ぶところ。
[self _xc_sendEvent:theEvent];

このメソッドが実行されている時は既にメソッドの交換が行われているので、この名前で呼び出すとオリジナルが呼ばれる。なお [seld sendMethod:theEvent] とすると Safariがクラッシュする(無限ループすることになる)。これはメッセージ sendMethod:を送ると、呼び出されるのは自分自身(_xc_sendEvent:)になるため。メソッドの交換により元の実装は _xc_sendEvent: というセレクタに紐付けられる。

イメージはこんな感じ。まず交換前の状態。



メッセージが送られるとセレクタ名でメソッドが検索され、対応する実装が呼び出される。

method_exchangeImplementations() によってこうなる。

この状態でメッセージ sendEvent: を送ると新規メソッドの実装が呼び出される。

最後に置き換え実行メソッド +[_swizzleMethods]を、エントリポイントから呼び出す。
static BOOL initialized_flag = NO;
+(void)load
{
 if (!initialized_flag) {
  NSLog(@"+[PluginController load] was called");
  [NSWindow _swizzleMethods];
  initialized_flag = YES;
 }
}


実行してみよう。

Safariのウィンドウ上で右クリックすると -[_xc_sendEvent:] が実行されているのが確認できた。また元々の動作(コンテキストメニューが表示される)も正常に働いている。

※ +[load] が2回、-[_xcsendEvent:] も1クリックあたり2回呼び出されているのが気になる。


(関連)
Cocoaの日々: Safari用独自プラグインを作る(1) - ひな形プロジェクト作成
Cocoaの日々: Safari用独自プラグインを作る(2) - Posing と Method Swizzling
Cocoaの日々: Safari用独自プラグインを作る(3) - Posing を試す



-------------
(以下、参考情報)
「SIMBLで Cocoaアプリをパワーアップ 」(PDF)
http://kirika.la.coocan.jp/archive/cocoastudy/200804/simbl-cocoa200804.pdf

【コラム】ダイナミックObjective-C (18) メソッドとは何か(1) - メソッド、セレクタ、メソッドの実装 | エンタープライズ | マイコミジャーナル

【コラム】ダイナミックObjective-C (19) メソッドとは何か(2) - メソッドを取得する | エンタープライズ | マイコミジャーナル

【コラム】ダイナミックObjective-C (20) メソッドとは何か(3) - メソッドの型を読み解く | エンタープライズ | マイコミジャーナル

【コラム】ダイナミックObjective-C (21) メソッドとは何か(4) - セレクタの実体 | エンタープライズ | マイコミジャーナル

【コラム】ダイナミックObjective-C (22) メソッドとは何か(5) - メソッドの実装 | エンタープライズ | マイコミジャーナル
そう、Objective-Cで自分に対してメッセージを送るときに利用される、あのselfだ。selfは、Objective-Cのキーワードではなく、変数なのだ。
そうだったのか。目から鱗。

【コラム】ダイナミックObjective-C (23) メッセージ送信(1) - objc_msgSendの実装 | エンタープライズ | マイコミジャーナル

【コラム】ダイナミックObjective-C (24) メッセージ送信(2) - メソッドリストからメソッドを検索する | エンタープライズ | マイコミジャーナル

2009年10月16日金曜日

Objective-C 2.0 ランタイムの情報

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

以前の投稿のコメントで Objective-C 2.0 になってからランタイムのインターフェイスが大幅に変更されたとの情報をもらった(有益な情報どうも)。

Cocoaの日々: Safari用独自プラグインを作る(2) - Posing と Method Swizzling
(下の方にコメントがある)

runtime.h にあった OBJC2_UNAVAILABLE というラベルは 2.0 ランタイム以降では使えないことを示すらしい(ただし互換性のために残してある)。
runtime.h

struct objc_method {
    SEL method_name        OBJC2_UNAVAILABLE;
    char *method_types     OBJC2_UNAVAILABLE;
    IMP method_imp         OBJC2_UNAVAILABLE;
}                          OBJC2_UNAVAILABLE;
ということは前回までで紹介していた Method構造体の中の値を直接書き換える方法は推奨されないということになる。ざっと眺めたところではそれに変わる構造体定義は見つけられなかった。実装は隠されたということか(互換性目的の opaqueインターフェイスとして上記構造体が残っている)。


また今後の 64bit 環境では 2.0 のランタイムを使わないと駄目らしい。

以下、2.0 のランタイム情報。
Mac Dev Center: Objective-C 2.0 Runtime Programming Guide: Introduction(英語)

この中の Platforms を引用すると
Platforms

iPhone applications and 64-bit programs on Mac OS X v10.5 and later use the modern version of the runtime.

Other programs (32-bit programs on Mac OS X desktop) use the legacy version of the runtime.
となっていた。"the modern version of the runtime" とは 2.0 ランタイムを指す。"legacy ..." は 1.0 ランタイムを指す。

なお 1.0 のラインタム情報は "Not Recommended" 扱いになっている。
Legacy: Objective-C 1 Runtime Reference (Not Recommended)(英語)


2.0 ランタイムの API リファレンス
Mac Dev Center: Objective-C 2.0 Runtime Reference(英語)

この中に 1.0 から 2.0 に上がるに伴って変更された構造体や関数の説明が掲載されている。
Mac Dev Center: Objective-C 2.0 Runtime Reference: Mac OS X Version 10.5 Delta

いくつか deperecated になっているものもある。


さて、
Method構造体を直接触ることが推奨されないとしたら、メソッドの置換はどうやれば良いのだろうか?


このリファレンスを眺めていると method_exchangeImplementations という関数があることに気がついた。
Mac Dev Center: Objective-C 2.0 Runtime Reference

引用させてもらうと:



ああ、これってメソッド置換の関数だ。Method構造体などを直接触ることなく API呼び出しで置換できるようになったのか。ほー。
1.0のリファレンスでは見当たらなかったので、これは 2.0から導入されたのだろうか。

これはいい。試してみよう(続く)。

2009年10月15日木曜日

Safari用独自プラグインを作る(3) - Posing を試す

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

(前回)Cocoaの日々: Safari用独自プラグインを作る(2) - Posing と Method Swizzling

+[NSObject poseAsClass:] は 10.5 以降で Deprecated 扱いになっているようだが使ったことが無いので一応試しておこう。

Posing のコードは下記が参考になる。
junk(SafariWheelTab)

前回までにつくったひな形に NSWindow のサブクラスを一つ追加する。

XCWindow.h

#import 
@interface XCWindow : NSWindow {
}
@end

XCWindow.m
@implementation XCWindow
- (void)sendEvent:(NSEvent *)theEvent
{
 if ([theEvent type] == NSRightMouseDown) {
  NSLog(@"[1] NSRightMouseDown");
 }
 [super sendEvent:theEvent];
}
@end

これを +[load]メソッドで NSWindow と置き換える。
PluginController.m
#import "PluginController.h"
#import "XCWindow.h"
@implementation PluginController
+(void)load
{
 NSLog(@"+[PluginController load] was called");
 [XCWindow poseAsClass:[NSWindow class]];
}
@end

ビルドして所定の場所へ置いて Safariを再起動する。ウィンドウの上で右クリックすると..


出た。

ところでPosingを重ねた場合どんな挙動になるんだろうか。最初の Posingが無視されると他のプラグインが同じクラスを置き換えた場合困ったことになる。試しにもう一つクラスを用意して Posingを重ねてみた。

XCWindow2.m
@implementation XCWindow2

- (void)sendEvent:(NSEvent *)theEvent
{
 if ([theEvent type] == NSRightMouseDown) {
  NSLog(@"[2] NSRightMouseDown");
 }
 [super sendEvent:theEvent];
}
@end

PluginController.m
#import "PluginController.h"
#import "XCWindow.h"
#import "XCWindow2.h"
@implementation PluginController
+(void)load
{
 NSLog(@"+[PluginController load] was called");
 [XCWindow poseAsClass:[NSWindow class]];
 [XCWindow2 poseAsClass:[NSWindow class]];
}
@end

実行してみると...


両方ともちゃんと呼び出されている。順番は後から Posingした方が先に呼び出されている。

(参考)
  【コラム】ダイナミックObjective-C (12) ポージングで乗っ取り | エンタープライズ | マイコミジャーナル

2009年10月14日水曜日

Safari用独自プラグインを作る(2) - Posing と Method Swizzling

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

(前回)Cocoaの日々: Safari用独自プラグインを作る(1) - ひな形プロジェクト作成

Safari への組み込みが確認できたので実装に取り組もう。アプリケーションプログラムの挙動を変えたり機能を追加するには2つの方法があるようだ。

CocoaReverseEngineering - Wikir

  • Posing
  • Method Swizzling

Posing

Posing は +[NSOjbect poseAsClass:] を使い、任意のクラスで既存のクラスを「乗っ取る」ことでアプリケーションの挙動を変える。

例えば独自に NSWindowのサブクラスを用意して poseAsClass:を使う場合はこんな感じでできる。
[XCWindow poseAsClass:[NSWindow class]];

参考情報)
【コラム】ダイナミックObjective-C (12) ポージングで乗っ取り | エンタープライズ | マイコミジャーナル


ただ MacOSX10.5 からは Deprecated 扱いになったようだ。
Mac Dev Center: Deprecated NSObject Methods
Posing is deprecated in Mac OS X v10.5. The poseAsClass: method is not available in 64-bit applications on Mac OS X v10.5.

残念。


Method Swizzling

一方の Method Swizzling はクラスではなくメソッドを置き換える。swizzling はカクテルを混ぜる(作る)意味のようだ。

先の Wikir からポイントとなる箇所を引用する。
        Method method = nil;
        method = class_getInstanceMethod(_class, _oldSelector);
        if (method == nil)
                return NO;
        method->method_name = _newSelector;

class_getInstanceMethod( ) は、インスタンスメソッド情報の構造体へのポインタを返す。
Objective-C プログラミング言語:class_getInstanceMethod

Method型については木下さんのコラムが詳しい。
【コラム】ダイナミックObjective-C (18) メソッドとは何か(1) - メソッド、セレクタ、メソッドの実装 | エンタープライズ | マイコミジャーナル

MacOSX10.5 環境で調べたところ Method の定義は下記にあった。
/Developer/SDKs/MacOSX10.5.sdk/usr/include/objc/runtime.h
typedef struct objc_method *Method;

objc_method の定義。
struct objc_method {
    SEL method_name               OBJC2_UNAVAILABLE;
    char *method_types            OBJC2_UNAVAILABLE;
    IMP method_imp                OBJC2_UNAVAILABLE;
}

#OBJC2_UNAVAILABLE の意味はわからず。

Method Swizzling はこの method_name を書き換えることでメソッドの置き換えを行う。
具体的には、ターゲットのメソッド(SEL)をとっておき、独自クラスのメソッドに書き換える。こうすると本来呼び出されるメソッドの代わりに独自クラスのメソッドが呼び出されるようになる。

(イメージ)NSWindow の  frame を置き換える。
  (1) -[NSWindow frame] の Method を取得し、method_name を @selector(_nswindow_frame) へ変更する
  (2) 独自クラスのメソッド @selector(special_frame) を method_name へ設定する

  これで -[NSWindow frame] が呼び出された時、独自クラスのメソッド  special_frame が呼び出される。

なお通常は独自メソッドの処理後に元のメソッドを呼び出す必要があるので、とっておいたメソッド(SEL)を最後に呼び出す。実際のコードは先のサイトを参照して欲しい(後日、検証でコードを各予定です)。

--------
(参考)メソッドのメタ情報へアクセスする関数はいろいろあってこれらも利用目的に応じて使えそうだ。
Objective-C プログラミング言語:メソッドへのアクセス

一部引用:
class_getInstanceMethod は、指定されたインスタンスメソッドについて記述しているデータ構造体へのポインタを返します。

class_getClassMethod は、指定されたクラスメソッドについて記述しているデータ構造体へのポインタを返します。

class_addMethods は、メソッドのリストをクラス定義に追加します。