プラグインアーキテクチャの作成
多くのアプリケーションはプラグインアーキテクチャ──主体となるアプリケーションのコードを変更することなく新機能をアプリケーションに追加する方法、を持つことで利益を得ます。
アーキテクチャの構築
プラグインアーキテクチャ実装の最初のステップは、プラグインの形態を決める事です。 メカニズムの決め方に関するガイドラインは、“プラグインアーキテクチャの設計”をお読み下さい。
Cocoaアーキテクチャは3つの選択肢をネィティブサポートしています:
- 形式プロトコルによる実装
- 非形式プロトコルで必要なメソッドのみ実装する方法
- 基底クラスもしくは抽象基底クラスを継承する方法
3つのケース全てにおいて、あなたはプラグイン開発者のために規定インタフェースを提供します。そして開発者はそのインタフェースに適合する、プラグインの主要クラスを記述します。 これを実現する最も便利な方法は、プラグイン開発者がリンクすべきフレームワークとしてインタフェースを提供する事です。
続く章では、3種類の異なるプラグインインタフェースの公開方法について解説します。
形式プロトコルによるプラグインインタフェースの公開
あなたが定義したプラグインプロトコルを使うには、プラグイン開発者はプロトコル定義を含むヘッダファイルのみを必要とします。 インタフェースを公開する一番簡単な方法は、単純にこのヘッダを配布する事です。
グラフィックスフィルタ用のプロトコルヘッダはリスト1のような物になるかもしれません:
リスト1 形式プロトコルによるプラグインアーキテクチャ
/* MyGreatImageApp Graphics Filter Interface version 0 MyAppBitmapGraphicsFiltering.h */ #import <Cocoa/Cocoa.h> @protocol MyGreatImageAppBitmapGraphicsFiltering // 実装に使ったインタフェースのバージョンを返す。 // ここでは0を返されたし。さもないと実装されていない機能を将来のバージョンで探す事になるかもしれない! - (unsigned)interfaceVersion; // Filterメニューに表示する文字列を返す。 - (NSString *)menuItemString; // フィルター処理本体: ビットマップにフィルターを施し、処理済みの物を返す。 - (NSBitmapImageRep *)filteredImageRep:(NSBitmapImageRep *)imageRep; // 設定変更ウィンドウのウィンドウコントローラ返す。 - (NSWindowController *)configurationWindowController; @end
使用されたインタフェースのバージョンをアプリケーションが識別できるように、本例がバージョンメソッドを含む事に注目して下さい。
ヘッダファイルを配布する場合、これはアプリケーションの将来のバージョンが、古いプラグインで扱う事の出来ないメッセージを送ってしまう事態の回避を確実にするための最も良い方法です。 “Validating Plug-ins”で解説するように、応答可能なメッセージをプラグインに問い合わせることも可能です。
非形式プロトコルによるプラグインインタフェースの公開
非形式プロトコルを用いたプラグインアーキテクチャは、形式プロトコルを用いたものに比べて幾らかトリッキーです。 なぜなら、プラグイン開発者がどのメソッドを実装するか選ぶことが出来るため、アプリケーションはメソッドを利用する前にプラグインがそれを本当に実装しているかどうか調べなければなりません。 実装の必須メソッドとオプションメソッドを混在させた場合、インタフェースのドキュメントでそれらを明確にしなければなりません。
形式プロトコル同様、非形式プロトコルを定義した1つのヘッダファイル単体で配布することが出来ます。 リスト2は、リスト1を形式プロトコルの代わりに非形式プロトコルを使って実装し直したもので、幾つかのメソッドを効果的にオプション扱いとしています。 大半の非形式プロトコルは、NSObjectのカテゴリの1つとして実装されます。
リスト2 非形式プロトコルによるプラグインアーキテクチャ
/* MyGreatImageApp Graphics Filter Interface version 0 MyAppBitmapGraphicsFiltering.h */ #import <Cocoa/Cocoa.h> @interface NSObject(MyGreatImageAppBitmapGraphicsFiltering) // 【必須】 // 実装に使ったインタフェースのバージョンを返す。 // ここでは0を返されたし。さもないと実装されていない機能を将来のバージョンで探す事になるかもしれない! - (unsigned)interfaceVersion; // 【オプション】 // Filterメニューに表示する文字列を返す。 // デフォルトはプラグインファイル名から拡張子を除いたものになる。 - (NSString *)menuItemString; // 【必須】 // フィルター処理本体: ビットマップにフィルターを施し、処理済みの物を返す。 - (NSBitmapImageRep *)filteredImageRep:(NSBitmapImageRep *)imageRep; // 【オプション】 // 設定変更ウィンドウのウィンドウコントローラを返す。 // このメソッドが実装されていなければ、設定オプションは提供されない。 - (NSWindowController *)configurationWindowController; @end
基底クラスによるプラグインインタフェースの公開
全てのプラグインに共通の実装(shared functionality)を提供したければ、プラグインの主要クラスが継承するための基底クラスを提供すればよいでしょう。 例えば、Mac OS XのスクリーンセーバーインタフェースはScreenSaverView基底クラスを含むフレームワークとして提供され、その基底クラスがスクリーンセーバープラグインのために多くの枝葉部分(management details)を扱います。 基底クラスを配布の最適な方法は、プラグイン開発者がリンクすべきフレームワークとしてパッケージにしてしまう事です。
リスト3は、仮想的な組み込みプラグインアーキテクチャのインタフェースを示し、リスト4はその実装を示します。 プロトコルと違い、インタフェースはある程度の機能を持つ基底クラスとして提供され、この例では極めて単純なNSViewのサブクラスが、バージョン情報の返却、データソースとなるURLの保持、そして白い背景の描画を行います。 このクラスが3つの予約済みポインタを持つことに注目して下さい。 これらはこのオブジェクトの大きさを変えることなく、ベースクラスへ新規メンバデータの追加を可能とし、それ故にホストアプリケーション開発者はバイナリ互換性を保ちつつ新機能を追加する事が可能となります。
リスト3 基底クラスによるプラグインアーキテクチャのインタフェース
#import <Cocoa/Cocoa.h> @interface MyAppEmbeddingView : NSView { @private NSURL *_URL; void *_reserved1; void *_reserved2; void *_reserved3; } - (id)initWithFrame:(NSRect)frameRect URL:(NSURL)URL; - (unsigned)interfaceVersion; - (NSURL *)URL; - (void)setURL:(NSURL *)URL; @end
リスト4 基底クラスによるプラグインアーキテクチャの実装
#import "MyAppEmbeddingView.h" @implementation MyAppEmbeddingView - (id)initWithFrame:(NSRect)frameRect URL:(NSURL)URL { self = [self initWithFrame:frameRect]; [self setURL:URL]; return self } - (unsigned)interfaceVersion { return 0; } - (void)drawRect:(NSRect)rect { NSEraseRect(rect); } - (NSURL *)URL { return _URL; } - (void)setURL:(NSURL *)URL { [URL retain]; [_URL release]; _URL = URL; } @end
基底クラスを作り終えたら、コンパイル済みの実装とヘッダファイルをプラグイン開発者が使うフレームワークにまとめましょう。 フレームワークを構築するには、XcodeのCocoa Frameworkプロジェクトのテンプレートを使います。 ターゲット設定ペインのビルドフェイズ>ヘッダセクションで、プライベートヘッダと公開ヘッダをあなたの意図通りに配置して下さい。 フレームワーク構築に関する更なる情報は、Xcodeのヘルプの“Creating Frameworks and Libraries”をご覧下さい。
プラグインの読込み
プラグインを使用するには、アプリケーションは以下のプロセスを経る必要があります:
- 規定の場所から利用可能なプラグインを探す
- 各プラグインの実行可能コードを読み込む
- 各プラグインのプラグインインタフェースへの準拠性を検証する
- 正当なプラグインを実体化する
プラグインの検索、読み込み、実体化の具体的な詳細は“NSBundleによるCocoaバンドルの読み込み”でも同じく解説しています。 プラグインアーキテクチャにおける追加のステップは、プラグイン開発者用に公開したインタフェースへの各プラグインの適合性を検証することです。
プラグインアーキテクチャが形式プロトコル、非形式プロトコル、基底クラスのどれに依存するかによって、適合性の検証は僅かに違います。
プラグインの検証
形式プロトコルの場合、クラスに対しプロトコルを実装しているかどうか見るよう問い合わせます。
安全のため、実装を要求されているメソッドが本当に実装されているか、実際に追加でチェックしてみるべきです。
リスト5は、プラグインの主要クラスがリスト1で定義したMyGreatImageAppBitmapFiltering
プロトコルに適合しているかを調べ、加えて全ての必須インスタンスメソッドを実装しているか確実に調べるメソッドの実装を示します。
リスト5 プラグインの検証(形式プロトコル)
- (BOOL)plugInClassIsValid:(Class)plugInClass { if([plugInClass conformsToProtocol:@protocol(MyGreatImageAppBitmapFiltering)]) { if([plugInClass instancesRespondToSelector: @selector(interfaceVersion)] && [plugInClass instancesRespondToSelector: @selector(menuItemString)] && [plugInClass instancesRespondToSelector: @selector(filteredImageRep)] && [plugInClass instancesRespondToSelector: @selector(configurationWindowController]) { return YES; } } return NO; }
非形式プロトコルの場合、クラスにどのメソッドを実装しているか問い合わせなければなりません。
典型的に、ホストアプリケーション開発者は幾つかのメソッドの実装を必須とし他は任意とするため、正当性検証メソッドはこの2つの特性を区別する必要があります。
リスト6は、プラグインの正当性検証メソッドの非形式プロトコルバージョンの実装です。
このメソッドは、プラグインがリスト2で与えられる非形式プロトコル版のMyGreatImageAppBitmapFiltering
のうち、全ての必須メソッドを実装している事を確かめます。
オプションメソッドが必要になったら、アプリケーション内のどこでも実装チェックを行う事が出来ます。
リスト6 プラグインの検証(非形式プロトコル)
- (BOOL)plugInClassIsValid:(Class)plugInClass { if([plugInClass instancesRespondToSelector: @selector(interfaceVersion)] && [plugInClass instancesRespondToSelector: @selector(filteredImageRep)]) { return YES; } return NO; }
基底クラスを継承したプラグインの検証は最も簡単です。 リスト7で示すように、基底クラスのサブクラスかどうか、そのプラグインの主要クラスに対して単純に問い合わせてみます。
リスト7 プラグインの検証(基底クラス)
- (BOOL)plugInClassIsValid:(Class)plugInClass { if([plugInClass isSubclassOfClass:[MyAppEmbeddingView class]]) { return YES; } return NO; }
プラグインの読込み: サンプルコード
リスト8はリスト1を若干修正したバージョンで、// 検証
コメントのところで、プラグインの追加前に正当性の検証を行います。
コードの動作解説の全文は、元のバージョンのコードをご覧下さい。
リスト8 プラグイン読み込みメソッドの実装
NSString *ext = @"plugin"; NSString *appSupportSubpath = @"Application Support/KillerApp/PlugIns"; // ... - (void)loadAllPlugins { NSMutableArray *instances; NSMutableArray *bundlePaths; NSEnumerator *pathEnum; NSString *currPath; NSBundle *currBundle; Class currPrincipalClass; id currInstance; bundlePaths = [NSMutableArray array]; if(!instances) { instances = [[NSMutableArray alloc] init]; } [bundlePaths addObjectsFromArray:[self allBundles]]; pathEnum = [bundlePaths objectEnumerator]; while(currPath = [pathEnum nextObject]) { currBundle = [NSBundle bundleWithPath:currPath]; if(currBundle) { currPrincipalClass = [currBundle principalClass]; if(currPrincipalClass && [self plugInClassIsValid:currPrincipalClass]) // 検証 { currInstance = [[currPrincipalClass alloc] init]; if(currInstance) { [instances addObject:[currInstance autorelease]]; } } } } } - (NSMutableArray *)allBundles { NSArray *librarySearchPaths; NSEnumerator *searchPathEnum; NSString *currPath; NSMutableArray *bundleSearchPaths = [NSMutableArray array]; NSMutableArray *allBundles = [NSMutableArray array]; librarySearchPaths = NSSearchPathForDirectoriesInDomains( NSLibraryDirectory, NSAllDomainsMask - NSSystemDomainMask, YES); searchPathEnum = [librarySearchPaths objectEnumerator]; while(currPath = [searchPathEnum nextObject]) { [bundleSearchPaths addObject: [currPath stringByAppendingPathComponent:appSupportSubpath]]; } [bundleSearchPaths addObject: [[NSBundle mainBundle] builtInPlugInsPath]]; searchPathEnum = [bundleSearchPaths objectEnumerator]; while(currPath = [searchPathEnum nextObject]) { NSDirectoryEnumerator *bundleEnum; NSString *currBundlePath; bundleEnum = [[NSFileManager defaultManager] enumeratorAtPath:currPath]; if(bundleEnum) { while(currBundlePath = [bundleEnum nextObject]) { if([[currBundlePath pathExtension] isEqualToString:ext]) { [allBundles addObject:[currPath stringByAppendingPathComponent:currBundlePath]]; } } } } return allBundles; }