この記事は編集レビューを必要としています。ぜひご協力ください。
この翻訳は不完全です。英語から この記事を翻訳 してください。
"Inter-process-communication Protocol Definition Language"の略であり、IPDLはMozilla特有の言語で、C++コードから整理されたセキュアな方法でプロセスあるいはスレッド間でメッセージを送ることを可能にします。Firefox内のすべてのタブやマルチプロセスプラグインはIPDL言語で宣言されています。
全てのIPDLメッセージは、parentエンドポイントやchildエンドポイントに送信され、 アクターと呼ばれます。IPDL protocol はどのようにアクターが通信するかを規定します:これは両方のアクターに送信が可能なメッセージの宣言や、同様にメッセージが送信された時のstate machineも記述します。
一般的には親のアクターは通信したあと変化しない側のことを言います:
親 | 子 | |
---|---|---|
IPC タブ | Chrome process | Content process |
IPC プラグイン | Content process | Plugin Process |
それぞれのプロトコルは別のファイルで宣言されています。IPDL コンパイラはいくつかのC++ヘッダーをそれぞれのIPDLプロトコルから作成します。この生成されたコードは低級で対話的なレイヤー (ソケットやパイプなど) の詳細や、メッセージの送信や構築、全てのアクターの各仕様の確実な実行の保障、そして たくさんのエラーの条件をハンドリングします。次の IPDL コードはブラウザとプラグインアクターの基本的な定義を表しています:
async protocol PPlugin { child: Init(nsCString pluginPath); Shutdown(); parent: Ready(); };
このコードではPPlugin
プロトコルを宣言しています。Init()とShutdown()の
2つのメッセージを親から子に送信できます。Ready()メソッドを使って一つのメッセージを子供から親に送信できます。
C++コードの作成
PPlugin.ipdlがコンパイルされるとビルドツリーの ipc/ipdl/_ipdlheaders/ ディレクトリ内に PPluginParent.h
と PPluginChild.h
が生成されます。PPluginParent と PPluginChild は抽象クラスでありサブクラス化されなければなりません。各々の発信メッセージはC++関数であり呼び出すことができます。各々の受信メッセージはC++の純粋仮想関数であり実装する必要があります:
class PPluginParent { public: bool SendInit(const nsCString& pluginPath) { // generated code to send an Init() message } bool SendShutdown() { // generated code to send a Shutdown() message } protected: /** * A subclass of PPluginParent must implement this method to handle the Ready() message. */ bool RecvReady() = 0; }; class PPluginChild { protected: bool RecvInit(const nsCString& pluginPath) = 0; bool RecvShutdown() = 0; public: bool SendReady() { // generated code to send a Ready() message } };
These Parent and Child abstract classes take care of all the "protocol layer" concerns: serializing data, sending and receiving messages, and checking protocol safety. It is the responsibility of the implementor to create subclasses to perform the actual work involved in each message. Here is a dirt-simple example of how a browser implementor might use PPluginParent.
class PluginParent : public PPluginParent { public: PluginParent(const nsCString& pluginPath) { // launch child plugin process SendInit(pluginPath); } ~PluginParent() { SendShutdown(); } protected: bool RecvReady() { mObservers.Notify("ready for action"); } };
Here's how the PPluginChild might be used by a C++ implementor in the plugin process:
class PluginChild : public PPluginChild { protected: void RecvInit(const nsCString& pluginPath) { mPluginLibrary = PR_LoadLibrary(pluginPath.get()); SendReady(); } void RecvShutdown() { PR_UnloadLibrary(mPluginLibrary); } private: PRLibrary* mPluginLibrary; };
Launching the subprocess and hooking these protocol actors into our IPC "transport layer" is beyond the scope of this document. See IPDL Processes and Threads for more details.
Because protocol messages are represented as C++ methods, it's easy to forget that they are in fact asynchronous messages: by default the C++ method will return immediately, before the message has been delivered.
Recv*メソッドのパラメータ (サンプルではconst nsCString& pluginPath) は一時的なオブジェクトを参照します。このデータを維持したいのであればコピーしなければなりません。
方向
それぞれのメッセージは方向を持ちます。メッセージの方向は親から子、子から親、両方向にメッセージが送信できることを示します。三つのキーワードは方向の指定方法を提供します。childラベルに続いて記述されたメッセージは親プロセスから子プロセスへ、parentラベルに続いて記述されたメッセージは子プロセスから親プロセスへのメッセージを送信できます。bothラベルに続いて記述されたメッセージは双方向に送信可能なメッセージです。The following artificial example shows how these specifiers are used and how these specifiers change the generated abstract actor classes.
// PDirection.ipdl async protocol PDirection { child: Foo(); // can be sent from-parent-to-child parent: Bar(); // can be sent from-child-to-parent both: Baz(); // can be sent both ways };
// PDirectionParent.h class PDirectionParent { protected: virtual void RecvBar() = 0; virtual void RecvBaz() = 0; public: void SendFoo() { /* 決まり文句*/ } void SendBaz() { /* 決まり文句 */ } };
// PDirectionChild.h class PDirectionChild { protected: virtual void RecvFoo() = 0; virtual void RecvBaz() = 0; public: void SendBar() { /* boilerplate */ } void SendBaz() { /* boilerplate */ } };
child、parent、bothのラベルはプロトコルの定義において何度も宣言できます。C++のpublicやprivate、protectedラベルのようにふるまいます。
パラメータ
メッセージの宣言は任意の数のパラメータを含むことができます。パラメータは送信時にシリアライズされ、受信時にデシリアライズされます。IPDLでは組み込みのプリミティブ型とカスタムプリミティブ型、共用体型、配列をサポートしています。
組み込みのプリミティブ型はC++のような数値型と論理型 (bool/char/int/double)、XPCOMの文字列型 (nsString/nsCString) が使用できます。これらの型は標準で読み込まれます。これらはありふれた型であること、ベースとしているIPCライブラリがこれらの型のシリアライズ方法を知っているからです。最新の組み込み型リストを知るには ipc/ipdl/ipdl/builtin.py を参照してください。
Actors may be passed as parameters. The C++ signature will accept a PProtocolParent* on one side and convert it to a PProtocolChild* on the other.
IPDL定義にusing宣言を追加することで他のIPDLでビルドした型を送信できます。これをカスタムプリミティブ型と呼びます。
カスタムプリミティブ型ではC++の実装でカスタムシリアライザとデシリアライザが提供されている必要があります。
using mozilla::plugins::NPRemoteEvent; sync protocol PPluginInstance { child: HandleEvent(NPRemoteEvent); };
共用体
IPDL has built-in support for declaring discriminated unions.
using struct mozilla::void_t from "ipc/IPCMessageUtils.h"; union Variant { void_t; bool; int; double; nsCString; PPluginScriptableObject; };
共用体型は上記のように定義されます。この共用体型がコンパイルされると以下のC++ソースになります:
struct Variant { enum Type { Tvoid_t, Tbool, Tint, Tdouble, TnsCString, TPPlugionScriptableObject }; Type type(); void_t& get_void_t(); bool& get_bool(); int& get_int(); double& get_double(); nsCString& get_nsCString(); PPluginScriptableObject* get_PPluginScriptableObject(); };
aUnion.type() can be used to determine the type of a union received in an IPDL message handler, with the remaining functions granting access to its contents. To initialize a union, simply assign a valid value to it, as follows:
aVariant = false;
構造体
IPDL はシリアライズ可能なデータタイプの任意のコレクションをビルトインでサポートしています。
struct NameValuePair { nsCString name; nsCString value; };
In implementation code, these structs can be created and used like so:
NameValuePair entry(aString, anotherString); foo(entry.name(), entry.value()); // Named accessor functions return references to the members
Arrays
IPDL has simple syntax for arrays:
InvokeMethod(nsCString[] args);
In C++ this is translated into a nsTArray reference:
virtual bool RecvInvokeMethod(nsTArray<nsCString>& args);
.ipdlh に分割定義し、IPDL が生成したデータ構造は、複数のプロトコルで利用可能です。これらのファイルは 通常.ipdl ファイルのような ipdl.mk メイクファイルに追加する必要があり、またその構文に従う必要があります。 Foo.ipdlh に定義された構造体を利用するために以下の様にインクルードします。
// in a .ipdl file include Foo;
同期メッセージとRPCメッセージ
ここまではすべて非同期でのメッセージ配信について述べました。ここまでで述べたメッセージでは、メッセージを送信するとすぐに処理を返却します。しかしメッセージがハンドルされるまで、あるいは値を返すまで待ちたいこともあります。
IPDLには以下の異なるセマンティクスが三つ存在します:
- 非同期セマンティクス (送信側はブロックされない)
- 受信側が応答を返すまで待つメッセージ。受信側がメッセージを受信し返事を返すまで送信側をブロックすることを同期セマンティクスと呼ぶ。メッセージは値を返してもよい。
- RPCセマンティクス (同期セマンティクスの亜種。後述する)
親プロセス子プロセスの概念は送信側受信側の概念と直行的です。つまり親プロセスも子プロセスも上記三つのケースにおいて送信側受信側どちらにもなれることに注意してください。メッセージングのセマンティクスはどちらの向きにも同じように適用されます。例えば、同期セマンティクスで子プロセスから親プロセスの場合、子プロセスは親プロセスがメッセージを受信し応答するまでブロックされます。非同期セマンティクスで親プロセスから子プロセスの場合、親プロセスはブロックされません。
When creating a plugin instance, the browser should block until instance creation is finished, and needs some information returned from the plugin:
protocol PPluginInstance { child: sync Init() returns (bool windowless, bool ok); };
この例では新しいキーワードが二つ出てきました。syncとreturnsです。syncキーワードはメッセージが同期的に送信されることを表します (asyncはセマンティクス指定がなかった場合のデフォルトです)。returnsキーワードは値のリストの前に書かれ、後に続くリストがメッセージの応答時に返却されることを表します。returnsキーワードは非同期メッセージに書くとエラーになります。
To make the blocking nature more noticeable to programmers, the C++ method names for synchronous and RPC messages are different:
sender | receiver | |
---|---|---|
async/sync | SendMessageName | RecvMessageName |
rpc | CallMessageName | AnswerMessageName |
メッセージセマンティクスの強さ
The above protocol will fail the IPDL type checker. IPDL protocols also have "semantics specifiers", just like messages. A protocol must be declared to have semantics at least as "strong" as its strongest message semantics. Synchronous semantics is called "stronger than" asynchronous. Like message declarations, the default protocol semantics is asynchronous; however, since the Plugin protocol declares a synchronous message, this type rule is violated. The fixed up Plugin protocol is shown below.
sync protocol PPluginInstance { child: sync Init() returns (bool windowless, bool ok); };
The generated C++ code for this method uses outparam pointers for the returned values:
class PPluginInstanceParent { ... bool SendInit(bool* windowless, bool* ok) { ... }; }; class PPluginInstanceChild { ... virtual bool RecvInit(bool* windowless, bool* ok) = 0; }
RPC semantics
"RPC" stands for "remote procedure call," and this third semantics models procedure call semantics. A quick summary of the difference between RPC and sync semantics is that RPC allows "re-entrant" message handlers: while an actor is blocked waiting for an "answer" to an RPC "call", it can be unblocked to handle a new, incoming RPC call.
In the example protocol below, the child actor offers a "CallMeCallYou()" RPC interface, and the parent offers a "CallYou()" RPC interface. The rpc
qualifiers mean that if the parent calls "CallMeCallYou()" on the child actor, then the child actor, while servicing this call, is allowed to call back into the parent actor's "CallYou()" message.
rpc protocol Example { child: rpc CallMeCallYou() returns (int rv); parent: rpc CallYou() returns (int rv); };
If this were instead a sync protocol, the child actor would not be allowed to call the parent actor's "CallYou()" method while servicing the "CallMeCallYou()" message. (The child actor would be terminated with extreme prejudice.)
Preferred semantics
Use async semantics whenever possible.
Blocking on replies to messages is discouraged. If you absolutely need to block on a reply, use sync semantics very carefully. It is possible to get into trouble with careless uses of synchronous messages; while IPDL can check and/or guarantee that your code does not deadlock, it is easy to cause nasty performance problems by blocking.
Please don't use RPC semantics. RPC semantics exists mainly to support remoting plugins (NPAPI), where we have no choice.
Message Delivery Order
Delivery is "in-order", that is, messages are delivered to the receiver in the order that they are sent, regardless of the messages' semantics. If an actor A sends messages M1 then M2 to actor B, B will be awoken to process M1 then M2.
Subprotocols and Protocol Management
So far we've seen a single protocol, but no real-world situation would have a single protocol in isolation. Instead, protocols are arranged in a managed hierarchy of subprotocols. A sub-protocol is bound to a "manager" which tracks its lifetime and acts as a factory. A protocol hierarchy begins with a single top-level protocol from which all subprotocol actors are eventually created. In Mozilla there are two main top-level protocols: PPluginModule for remote plugins, and PContent for remote tabs.
The following example extends the toplevel plugin protocol to manage plugin instances.
// ----- file PPlugin.ipdl include protocol PPluginInstance;
rpc protocol PPlugin { manages PPluginInstance; child: rpc Init(nsCString pluginPath) returns (bool ok); rpc PPluginInstance(nsCString type, nsCString[] args) returns (int rv);
};
// ----- file PPluginInstance.ipdl
include protocol PPlugin;
rpc protocol PPluginInstance
{
manager PPlugin;
child:
rpc __delete__();
SetSize(int width, int height);
};
This example has several new elements: `include protocol` imports another protocol declaration into this file. Note that this is not a preprocessor directive, but a part of the IPDL language. The generated C++ code will have proper #include preprocessor directives for the imported protocols.
The `manages` statement declares that this protocol manages PPluginInstance. The PPlugin protocol must declare constructor and destructor messages for PPluginInstance actors. The `manages` statement also means that PPluginInstance actors are tied to the lifetime of the Plugin actor that creates them: if this PPlugin instance is destroyed, all the PPluginInstances associated with it become invalid or are destroyed as well.
The mandatory constructor and destructor messages (PPluginInstance and __delete__ respectively) exist, confusingly, in separate locations. The constructor must be located in the managing protocol, while the destructor belongs to the managed subprotocol. These messages have syntax similar to C++ constructors, but the behavior is different. Constructors and destructors have parameters, direction, semantics, and return values like other IPDL messages. A constructor and destructor message must be declared for each managed protocol.
Each subprotocol must include a `manager` statement.
At the C++ layer, the subclasses in both the child and the parent must implement methods for allocating and deallocating the subprotocol actor. The constructor and destructor are translated into standard C++ methods for messages.
Note: __delete__ is a built-in construct, and is the only IPDL message which does not require an overridden implementation (ie. Recv/Answer__delete__). However, overridden implementations are encouraged when some action should happen on protocol destruction in lieu of using the DeallocPProtocol function.
class PPluginParent { /* Allocate a PPluginInstanceParent when the first form of CallPluginInstanceConstructor is called */ virtual PPluginInstanceParent* AllocPPluginInstance(const nsCString& type, const nsTArray<nsCString>& args, int* rv) = 0; /* Deallocate the PPluginInstanceParent after PPluginInstanceDestructor is done with it */ virtual bool DeallocPPluginInstance(PPluginInstanceParent* actor) = 0; /* constructor message */ virtual CallPPluginInstanceConstructor(const nsCString& type, const nsTArray<nsCString>& args, int* rv) { /* generated code */ } /* alternate form of constructor message: supply your own PPluginInstanceParent* to bypass AllocPPluginInstance */ virtual bool CallPPluginInstanceConstructor(PPluginInstanceParent* actor, const nsCString& type, const nsTArray<nsCString>& args, int* rv) { /* generated code */ } /* destructor message */ virtual bool Call__delete__(PPluginInstanceParent* actor) { /* generated code */ } /* Notification that actor deallocation is imminent, IPDL mechanisms are now unusable */ virtual void ActorDestroy(ActorDestroyReason why); ... }; class PPluginChild { /* Allocate a PPluginInstanceChild when we receive the PPluginInstance constructor */ virtual PPluginInstanceChild* AllocPPluginInstance(const nsCString& type, const nsTArray<nsCString>& args, int* rv) = 0; /* Deallocate a PPluginInstanceChild after we handle the PPluginInstance destructor */ virtual bool DeallocPPluginInstance(PPluginInstanceChild* actor) = 0; /* Answer the constructor message. Implementing this method is optional: it may be possible to answer the message directly in AllocPPluginInstance. */ virtual bool AnswerPPluginInstanceConstructor(PPluginInstanceChild* actor, const nsCString& type, const nsTArray<nsCString>& args, int* rv) { } /* Answer the desctructor message. */ virtual bool Answer__delete__(PPluginInstanceChild* actor) = 0; /* Notification that actor deallocation is imminent, IPDL mechanisms are now unusable */ virtual void ActorDestroy(ActorDestroyReason why); ... };
Subprotocol Actor Lifetime
AllocPProtocol and DeallocPProtocol are a matched pair of functions. The typical implementation of these functions uses `new` and `delete`:
class PluginChild : PPluginChild { virtual PPluginInstanceChild* AllocPPluginInstance(const nsCString& type, const nsTArray<nsCString>& args, int* rv) { return new PluginInstanceChild(type, args, rv); } virtual bool DeallocPPluginInstanceChild(PPluginInstanceChild* actor) { delete actor; // actor destructors are always virtual, so it's safe to call delete on them! return true; } ... };
In some cases, however, external code may hold references to actor implementations which require refcounting or other lifetime strategies. In this case, the alloc/dealloc pairs can perform different actions. Here is an example of refcounting:
class ExampleChild : public nsIObserver, public PExampleChild { ... }; virtual PExampleChild* TopLevelChild::AllocPExample() { nsRefPtr<ExampleChild*> actor = new ExampleChild(); return actor.forget(); } virtual bool TopLevelChild::DeallocPExample(PExampleChild* actor) { NS_RELEASE(static_cast<ExampleChild*>(actor)); return true; }
If an object that implements a protocol can't be constructed inside AllocPFoo, or has been previously constructed and doesn't require an IPDL connection throughout its lifetime, there is a second form of SendPFooConstructor which can be used:
class ExampleChild { public: void DoSomething() { aManagerChild->SendPExampleConstructor(this, ...); } };
Internally, the first constructor form simply calls
PExample(Parent|Child)* actor = AllocPExample(...); SendPExampleConstructor(actor, ...); return actor;
with the same effect.
Subprotocol Deletion
It is worth understanding the protocol deletion process. Given the simple protocols:
// --- PExample.ipdl include protocol PSubExample; async protocol PExample { manages PSubExample; parent: PChild(); }; // --- PSubExample.ipdl include protocol PExample; async protocol PSubExample { manager PExample; child: __delete__(); };
We assume that there is a PSubExampleParent/Child pair in existence, such that some element now wishes to trigger the protocol's deletion from the parent side.
aPSubExampleParent->Send__delete__();
will trigger the following ordered function calls:
PSubExampleParent::ActorDestroy(Deletion) /* Deletion is an enumerated value indicating that the destruction was intentional */ PExampleParent::DeallocPSubExample()
PSubExampleChild::Recv__delete__() PSubExampleChild::ActorDestroy(Deletion) PExampleChild::DeallocPSubExample()
ActorDestroy is a generated function that allows code to run with the knowledge that actor deallocation is imminent. This is useful for actors with lifetimes outside of IPDL - for instance, a flag could be set indicating that IPDL-related functions are no longer safe to use.
Accessing the protocol tree from C++
The IPDL compiler generates methods that allow actors to access their manager (if the actor isn't top-level) and their managees (if any) from C++. For a protocol PFoo managed by PManager, that manages PManagee, the methods are
PManager* PFoo::Manager() const InfallibleTArray<PManagee*> PFoo::ManagedPManagee(); void PFoo::ManagedPManagee(InfallibleTArray<PManagee*>&);
Shutdown and Error Handling
The C++ methods which implement IPDL messages return bool
: true for success, and false for catastrophic failure. Message implementations should return false from a message implementation if the data is corrupted or otherwise malformed. Any time a message implementation returns false, IPDL will immediately begin catastrophic error handling: the communication channels for the child process (tab or plugin) will be disconnected, and the process will be terminated. Do not return false from message handlers for "normal" error conditions such as inability to load a network request! Normal errors should be signaled with a message or return value.
Note: the following paragraphs are not yet implemented. IPDL tracks all active protocols between two endpoints. If if the child side crashes or becomes hung:
- any synchronous or RPC messages currently active will return false
- no further messages will be accepted (C++ methods will return false)
- each IPDL actor will receive an OnError message
- DeallocPSubprotocol will be called on each manager protocol to deallocate any active subprotocols.
When a manager protocol is destroyed, any subprotocols will be notified:
- no further messages will be accepted
- DeallocPSubprotocol will be called on the manager protocol to deallocate any active subprotocols
When the toplevel protocol is destroyed, this is equivalent to shutting down the entire IPDL machinery for that connection, because no more messages can be sent and all subprotocols are destroyed.
Protocol state machines
The astute reader might question why IPDL includes the word "protocol" when all that has been introduced so far are unstructured grab-bags of messages. IPDL allows protocol authors to define the order and structure of how messages may be sent/received by defining protocol state machines (finite state machines).
[Note that the state machine portion of the IPDL compiler is not complete as of this writing, 22 October 2009. IPDL code for state machines is accepted by the compiler, but it does not affect the generated C++, yet.]
IPDL parent and child actors communicating via a protocol are paired. Each actor in the pair follows the same state machine. The pair attempts to keep their single collective state synchronized. Though, it is possible that the parent and child actors may be momentarily out of sync while messages are transmitted.
IPDL (arbitrarily) requires state machines to be defined from the perspective of the parent side of the protocol. For example, when you see the send Msg
syntax, it means "when the parent actor sends Msg".
The following example shows one such state machine for the Plugin protocol.
include protocol PPluginInstance; sync protocol PPlugin { manages PPluginInstance; child: sync Init() returns (int rv); Deinit(); sync PPluginInstance(String type, StringArray args) returns (int rv); // NOTE: state machine follows state START: send Init goto IDLE; state IDLE: send PPluginInstance goto ACTIVE; state ACTIVE: send PPluginInstance goto ACTIVE; send ~PPluginInstance goto ACTIVE; send Deinit goto DYING; state DYING: send ~PPluginInstance goto DYING; };
There are three new syntactic elements, above. First are "state declarations": the code state FOO:
declares a state "FOO". (States are capitalized by convention, not because of syntactic rules.) The first state to be declared is the protocol's "start state"; when an actor is created, its initial state is the "start state."
The second new syntactic element is the trigger. The syntax send MsgDecl
defines a trigger for a state transition; in this case, the trigger is send
ing the async or sync message "MsgDecl." The triggers are:
send
ing an async or sync messagerecv
ing an async or sync messagecall
ing an RPCanswer
ing an RPC
Aside: this is why actor ctors/dtors act like normal messages, with directions etc.: this allows them to be checked against the protocol state machine like any other message.
The third new syntactic element is a state transition. The syntax is: goto NEXT_STATE
. When the trigger preceding this transition occurs, the protocol actor's internal state is changed to, in this case, "NEXT_STATE."
Another example state machine, for PluginInstance, follows.
sync protocol PluginInstance { manager Plugin; child: SetWindow(PluginWindow window); Paint(); parent: sync GetBrowserValue(String key) returns (String value); state START: send SetWindow goto SENT_WINDOW; recv GetBrowserValue goto START; state SENT_WINDOW: send SetWindow goto SENT_WINDOW; send Paint goto SENT_WINDOW; recv GetBrowserValue goto SENT_WINDOW; };
Note:
- The following points will apply when the IPDL compiler fully supports states. It is incomplete as of this writing, on 22 October 2009.
- Protocol state machines are optional, but strongly encouraged. Even simple state machines are useful.
- All actor states, trigger matching, and transitions are managed by IPDL-generated code. Your C++ code need not manage these things.
- All messages sent and received are checked against the protocol's state machine. If a message violates the state machine semantics defined in the IPDL code, the child process containing the child actor will be terminated with extreme prejudice, and all parent actors will be made invalid! It is up to the developer to make sure that this never happens.
- Lots of syntactic sugar is possible for state machine definitions. Ping the Electrolysis team if you have a good proposal