Articles

XPC services on macOS app

Lê Điền Phúc
Lê Điền Phúc

Follow

9/4, 2020 – 9 min read

XPC以前はSocketとMach Messages (Mach Ports)を拾っていた。

XPC メカニズムは、IPC のためのソケット (または MIG を使用した Mach サービス) に代わるものを提供します。 たとえば、「サーバー」として動作し、クライアントがその API にアクセスし、何らかのサービスを提供するのを待つプロセスを持つことができます。 Apple のエコシステムにおけるバンドルは、特定のディレクトリ構造で表される実体を指します。 最もよく遭遇するバンドルは、Application Bundlesです。 アプリケーション(例えばChess.app)を右クリックして、「内容を表示」を選択すると、ディレクトリ構造が表示されます。 XPCに話を戻すと、アプリケーションはXPCサービスバンドルを持っていることがあります。 アプリケーションバンドル内のContents/XPCServices/ディレクトリの中に、それらのサービスがあります。 アプリケーション ディレクトリを検索して、どれだけのアプリケーションが XPC サービスに依存しているかを確認できます。

また、フレームワーク (これは別の種類のバンドル) 内に XPC サービスを持つことができます。

XPC サービスのその他の利点

アプリケーションで XPC サービスを使用すると、いくつかの機能を別々のモジュール (XPC サービス) に分けることができます。 高価だが頻度の低いタスクを実行する XPC サービスを作成することができます。 たとえば、乱数を生成する暗号化タスクなどです。

もう 1 つの利点は、XPC サービスが独自のプロセスで実行されるということです。 そのプロセスがクラッシュしたり終了したりしても、メイン アプリケーションには影響しません。 アプリケーションがユーザー定義のプラグインをサポートすると想像してください。 そして、そのプラグインはXPCサービスを使って構築されています。 それらが不完全にコード化され、クラッシュしても、メイン アプリケーションの整合性には影響しません。

XPC サービスのさらなる利点は、それら独自のエンタイトルメントを持つことができるということです。 アプリケーションは、エンタイトルメントを必要とする XPC サービスによって提供されるサービスを利用するときだけ、エンタイトルメントを必要とします。 例えば、位置情報を利用するアプリがあるが、特定の機能にしか利用しない場合を想像してください。 その機能をXPCサービスに移動し、そのXPCサービスのみに位置情報エンタイトルメントを追加することができます。

XPC と私たちの友人 launchd

launchd は、システム上で実行される最初のプロセスです。 他のプロセス、サービス、デーモンを起動し、管理します。

XPC サービスは、長い間アイドルであった場合に停止したり、要求に応じて起動したりすることができます。 すべての管理は launchd によって行われ、私たちは何もする必要がありません。

launchd はシステム全体のリソースの可用性とメモリの圧迫に関する情報を持っており、私たちのシステムのリソースを最も効果的に使用する方法について決定を下すには launchd よりも誰が最適でしょうか

Implement XPC Services

XPC サービスはメイン アプリケーション バンドル内の Contents/XPCServices ディレクトリのバンドルで、XPC サービスのバンドルは Info.plist ファイル、実行ファイル、サービスによって必要なリソースから構成されます。 XPC サービスは、その main 関数から xpc_main(3) Mac OS X Developer Tools Manual Page を呼び出すことにより、サービスがメッセージを受信したときに呼び出す関数を示します。

Xcode で XPC サービスを作成するには、次の操作を行います。

  • Add the Copy Files phase to your application’s build settings, which copies the XPC service into the Contents/XPCServices directory of the main application bundle.
  • Add the dependency to your application’s build settings, indicating it depends on the XPC service bundle.
  • If you are writing a low-level (C-based) XPC service, implement the minimal main function to register your event handler, as shown in the following code listing.も参照ください。 my_event_handler をイベント ハンドラ関数の名前に置き換えてください。
  • int main(int argc, const char *argv) {
    xpc_main(my_event_handler);
    // The xpc_main() function never returns.
    exit(EXIT_FAILURE);
    }

    NSXPCConnection を使用して高レベル (Objective-C ベース) のサービスを書いている場合、まず NSXPCListenerDelegate プロトコルに準拠した接続デリゲート クラスを作成します。 次に、次のコード一覧に示すように、リスナー オブジェクトを作成および設定する最小限のメイン関数を実装します。

    int main(int argc, const char *argv) {
    MyDelegateClass *myDelegate = ...
    NSXPCListener *listener =
    ;
    listener.delegate = myDelegate;
    ;
    // The resume method never returns.
    exit(EXIT_FAILURE);
    }

    Using the Service

    XPC サービスの使用方法は、C API (XPC Services) または Objective-C API (NSXPCConnection) で作業しているかどうかに依存します。

    Objective-C NSXPCConnection API の使用 Objective-C NSXPCConnection API は、あるプロセス内のオブジェクトのメソッドを別のプロセス (通常は XPC サービス内のメソッドを呼び出すアプリケーション) から呼び出すことができる高レベルのリモート プロシージャ コール インターフェイスを提供します。 NSXPCConnection API は、データ構造とオブジェクトを送信用に自動的にシリアライズし、相手側でデシリアライズします。 その結果、リモート オブジェクトのメソッドを呼び出すと、ローカル オブジェクトのメソッドを呼び出すのと同じように動作します。

    NSXPCConnection API を使用するには、次のものを作成する必要があります:

    • インターフェース。 これは主に、リモートプロセスから呼び出し可能なメソッドを記述するプロトコルから構成されます。 これは、インターフェイスの設計
    • 両側の接続オブジェクトで説明されています。 サービス側では、これは以前にサービスの作成で説明しました。 クライアント側では、「インターフェイスへの接続と使用」
    • リスナーで説明されています。 XPC サービス内のこのコードは、接続を受け付けます。 これは、ヘルパーの接続を受け入れるで説明されています。 Messages.

    Overall Architecture

    Overall architecture

    NSXPCConnection ベースのヘルパーアプリで作業する場合、メインアプリケーションとヘルパーは両方ともNSXPCConnectionのインスタンスを持つことができます。 メインアプリケーションはその接続オブジェクト自体を作成し、これによりヘルパーが起動します。 ヘルパー内のデリゲートメソッドは、接続が確立されたときにその接続オブジェクトを渡されます。

    各 NSXPCConnection オブジェクトは 3 つの主要な機能を提供します。

    • 接続の反対側で利用可能にすべきメソッドを記述する exportedInterface プロパティ。
    • 接続の反対側から入ってくるメソッド呼び出しを処理するローカル オブジェクトを含む exportedObject プロパティ。

    メイン アプリケーションがプロキシ オブジェクトのメソッドを呼び出すと、XPC サービスの NSXPCConnection オブジェクトはその exportedObject プロパティに格納されているオブジェクトのメソッドを呼び出します。

    同様に、XPC サービスがプロキシ オブジェクトを取得し、そのオブジェクトのメソッドを呼び出す場合、メイン アプリケーションの NSXPCConnection オブジェクトは、その exportedObject プロパティに格納されているオブジェクトのそのメソッドを呼び出します

    Designing an Interface

    NSXPCConnection API は、呼び出し側アプリケーションとサービス間のプログラム上のインターフェイスを定義するのに Objective-C プロトコルを利用します。 接続の反対側から呼び出したいインスタンスメソッドは、正式なプロトコルで明示的に定義する必要があります。 例えば、

    @protocol FeedMeACookie
    - (void)feedMeACookie: (Cookie *)cookie;
    @end

    XPC 上の通信は非同期なので、プロトコル内のすべてのメソッドの戻り値の型は void である必要があります。 データを返す必要がある場合は、次のように reply ブロックを定義します:

    @protocol FeedMeAWatermelon
    - (void)feedMeAWatermelon: (Watermelon *)watermelon
    reply:(void (^)(Rind *))reply;
    @end

    1つのメソッドは1つだけ reply ブロックを持つことができます。 ただし、接続は双方向なので、XPCサービスヘルパーは、必要に応じて、メインアプリケーションが提供するインターフェイスのメソッドを呼び出して返信することも可能である。

    各メソッドの戻り値の型は void でなければならず、メソッドまたはリプライブロックへのすべてのパラメーターはどちらかでなければなりません。

    • 算術型 (int, char, float, double, uint64_t, NSUInteger, and so on)
    • BOOL
    • C string
    • C structures and arrays containing only the types listed above
    • NSSecureCoding protocol を実装する Objective-C object.

    重要: メソッド (またはその応答ブロック) に Objective-C コレクション クラス (NSDictionary, NSArray など) であるパラメーターがあり、コレクション内で独自のカスタム オブジェクトを渡す必要がある場合、そのコレクションパラメーターのメンバーとしてクラスを許可するように XPC に明示的に伝える必要があります。

    インターフェイスへの接続と使用

    一度プロトコルを定義した後は、それを記述するインターフェイス オブジェクトを作成する必要があります。 これを行うには、NSXPCInterface クラスの interfaceWithProtocol: メソッドを呼び出します。 例:

    NSXPCInterface *myCookieInterface =
    ;

    インターフェイス オブジェクトを作成したら、メイン アプリ内で initWithServiceName: メソッドを呼び出して、そのオブジェクトとの接続を構成する必要があります。 例:

    NSXPCConnection *myConnection = 
    initWithServiceName:@"com.example.monster"];
    myConnection.remoteObjectInterface = myCookieInterface;
    ;

    Note: アプリバンドル外の XPC サービスと通信するために、initWithMachServiceName: メソッドを使用して XPC 接続を構成することもできます。

    この時点で、メインアプリケーションは myConnection オブジェクトの remoteObjectProxy または remoteObjectProxyWithErrorHandler: メソッドを呼び出し、プロキシ オブジェクトを取得することができます。

    このオブジェクトは、XPC サービスが (exportedObject プロパティを設定することにより) そのエクスポート オブジェクトとして設定したオブジェクトのプロキシとして機能します。 このオブジェクトは、remoteObjectInterface プロパティで定義されたプロトコルに準拠する必要があります。

    アプリケーションがプロキシ オブジェクトのメソッドを呼び出すと、対応するメソッドが XPC サービス内部のエクスポートされたオブジェクトで呼び出されます。 サービスのメソッドが reply ブロックを呼び出すと、パラメータ値がシリアライズされてアプリケーションに返され、そこでパラメータ値がデシリアライズされて reply ブロックに渡されます。 (応答ブロックはアプリケーションのアドレス空間内で実行されます。)

    Note: ヘルパー プロセスがアプリケーション内のオブジェクトのメソッドを呼び出すことを許可したい場合、exportedInterface および exportedObject プロパティを resume の呼び出し前に設定する必要があります。 これらのプロパティについては、次のセクションでさらに説明します。

    Accepting a Connection in the Helper

    NSXPCConnection ベースのヘルパーが接続から最初のメッセージを受信すると、リスナー デリゲートの listener:shouldAcceptNewConnection: メソッドがリスナー オブジェクトと接続オブジェクトを使用して呼び出されます。 このメソッドは、接続を受け入れるかどうかを決定します。接続を受け入れる場合は YES、接続を拒否する場合は NO を返す必要があります。

    Note: 最初の実際のメッセージが送信されると、ヘルパーは接続要求を受信します。 接続オブジェクトの再開メソッドはメッセージを送信させない。

    ポリシー決定を行うことに加えて、このメソッドは接続オブジェクトを構成する必要がある。 特に、ヘルパーが接続を受け入れることを決定したと仮定すると、接続に次のプロパティを設定する必要があります:

    • exportedInterface – エクスポートしたいオブジェクトのプロトコルを記述したインターフェイスオブジェクトです。 (このオブジェクトの作成については、「インターフェイスへの接続と使用」で以前に説明しました。)
    • exportedObject – リモート クライアントのメソッド呼び出しが配信されるべきローカル オブジェクト (通常はヘルパー内) です。 接続の反対側 (通常はアプリケーション内) が接続のプロキシ オブジェクト上のメソッドを呼び出すときはいつでも、対応するメソッドは exportedObject プロパティによって指定されたオブジェクト上で呼び出されます。

    それらのプロパティを設定した後、YES を返す前に接続オブジェクトの再開メソッドを呼び出す必要があります。

    メッセージの送信

    NSXPCでのメッセージの送信は、メソッド呼び出しと同じくらい簡単です。 たとえば、XPC 接続オブジェクト myConnection 上のインターフェイス myCookieInterface (前のセクションで説明) がある場合、次のように feedMeACookie メソッドを呼び出すことができます:

    Cookie *myCookie = ...
    feedMeACookie: myCookie];

    このメソッドを呼び出すと、XPC ヘルパー内の対応メソッドは自動的に呼び出されます。 そのメソッドは、今度は、メイン アプリケーションによってエクスポートされたオブジェクトのメソッドを呼び出すために、同様に XPC ヘルパーの接続オブジェクトを使用できます。

    Handling Errors

    任意のヘルパーのタスクに固有のエラー処理メソッドに加えて、XPC サービスとメイン アプリケーションの両方は以下の XPC エラー ハンドラ ブロックを提供するべきです。 ローカル接続オブジェクトは通常まだ有効で、今後の呼び出しは、それが不可能でない限り、自動的に新しいヘルパー インスタンスを生成しますが、ヘルパーが保持していたであろう状態をリセットする必要があるかもしれません。

    • Invalidation handler – invalidate メソッドが呼び出されたとき、または XPC ヘルパーが開始できなかったときに呼び出されます。 このハンドラが呼び出されると、ローカル接続オブジェクトはもはや有効ではなく、再作成されなければなりません。 これは、常に接続オブジェクト上で呼び出される最後のハンドラです。 このブロックが呼ばれたとき、コネクションオブジェクトは破棄されたことになります。 ハンドラー内またはコードの他の場所であろうと、その時点で接続にさらにメッセージを送信することはできません。

    いずれの場合も、ブロック スコープの変数を使用して、保留中の操作のキューや接続オブジェクト自体などの十分なコンテキスト情報を提供し、ハンドラー コードが、保留中の操作を再試行する、接続を破棄する、エラー ダイアログを表示するなど、特定のアプリで意味のある何らかのアクションができるようにしなければなりません。