Elite software development.

Understanding The C++ Code Generated By Mozilla's IPDL Compiler

Mozilla's "Electrolysis" project is re-structuring Firefox into a multi-process architecture. A side effect is that Mozilla developers must learn to program in an environment where inter-process communication (IPC) is necessary. To ease this development burden, Mozilla has devised "IPDL", the Inter-process Protocol Definition Language. Developers are meant to specify their IPC protocol via IPDL, and have the IPDL compiler provide C++ code for their use.

This article investigates the functionality provided by the C++ classes generated by the IPDL compiler. Developers should never have to modify the generated code, but understanding how it works will help to explain what responsibilities developers of IPDL protocols have.

The IPDL code defines the IPC protocol. It describes and restricts the messages that can be sent by the actors in the protocol. Let's begin by considering a "Hello, World" example of IPDL code:

PTest.ipdl

Here is the IPDL code that will be compiled by the IPDL compiler.

namespace mozilla {
namespace test {

protocol PTest
{
child:
    Hello();

parent:
    World();
};

} // namespace test
} // namespace mozilla

This defines a protocol named "PTest" in which the child actor can receive (and the parent actor can send) a message called "Hello", and in which the parent actor can receive (and the child actor can send) a message called "World".

The absence of the "sync" and "rpc" modifier keywords on the message declarations, above, means that this is an asynchronous protocol - all the messages are sent asynchronously. For an introduction to IPDL, and description of "sync" vs. "async" vs. "rpc", see https://wiki.mozilla.org/IPDL/Getting_started#Semantics. Or, just ignore this detail for now.

From this IPDL spec, the IPDL compiler produces 3 output files: PTest.h, PTestParent.h, and PTestChild.h. Let's investigate each of these, one at a time.

PTest.h

PTest.h defines the message object types that can be sent and received in the PTest protocol. Here is a tour of the important parts of that code:

#include "base/basictypes.h"
#include "prtime.h"
#include "nscore.h"
#include "IPC/IPCMessageUtils.h"
#include "nsStringGlue.h"
#include "nsTArray.h"
#include "mozilla/ipc/ProtocolUtils.h"

These included header files provide the C++ functionality for IPDL's built-in types.

namespace mozilla {
namespace test {
namespace PTest {

These namespace declarations correspond to those in the IPDL file.

enum State {
    StateStart = 0,
    StateError,
    StateLast
};  

These are the legal states for the PTest protocol. As described in The IPDL Getting Started Guide, protocol actors are always in one of several well-defined states. An IPDL protocol without any state declarations results in a protocol with actors which can nonetheless be in one of the three states defined by the State enum, above.

enum MessageType {
    PTestStart = PTestMsgStart << 10,
    PTestPreStart = (PTestMsgStart << 10) - 1,
    Msg_Hello__ID,
    Msg_World__ID,
    PTestEnd
}; 

The purpose of the MessageType enum is to create unique 16-bit message identifiers. The IPC machinery requires that unique messages types be discriminated by unique IDs. The numeric value of PTestStart is dependent upon that of PTestMsgStart, which is an element of the IPCMessageStart enum. The latter is found in the file IPCMessageStart.h, which is generated by the IPDL compiler.

The Msg_Hello__ID and Msg_World__ID are the unique message type IDs used in the construction of the messages sent by the PTest protocol, as we will see, below.

class Msg_Hello :
    public IPC::Message

The IPC::Message class is what does the "work" of serializing messages - transforming them into a series of bytes. Understanding IPDL messages requires understanding IPC::Message, defined in ipc/chromium/src/chrome/common/ipc_message.h.

{       
public:
    enum {
        ID = Msg_Hello__ID
    };
    Msg_Hello() :
        IPC::Message(MSG_ROUTING_NONE, ID, PRIORITY_NORMAL)
    {       
    }       

The only constructor for Msg_Hello does nothing except construct an instance of its IPC::Message base class. The constructor for IPC::Message is passed three arguments. The first, MSG_ROUTING_NONE, indicates to the IPC subsystem that we do not yet have a routing ID. The second, ID, specifies the user-defined message type. Notice that ID has the value Msg_Hello__ID, which is defined above as a unique message type identifier. It is up to the IPDL system to ensure that this value is unique among all the IPC processes. The third, PRIORITY_NORMAL, indicates the message priority. The IPC::Message class exists primarily to house these three values, some flags, and to inherit from Pickle, a class whose purpose is to serialize data for IPC.

    static bool Read(const Message* msg) 
    {   
        return true;
    }

In general, the Read() member function reads the parameters of the message, but there are no parameters in this case. So, it always succeeds (returns true). If the Hello message had parameters, then they would be deserialized here by Read().

The final member function of Msg_Hello generated by the IPDL compiler is the logging code, which I omit for brevity.

Similar techniques are used in the generation of the class for the "World" message. See if it makes sense to you:

class Msg_World :
    public IPC::Message
{
public:
    enum {
        ID = Msg_World__ID
    };
    Msg_World() :
        IPC::Message(MSG_ROUTING_NONE, ID, PRIORITY_NORMAL)
    {
    }
    static bool Read(const Message* msg)
    {
        return true;
    }
    // Log() goes here.
};

PTestChild.h

PTestChild.h defines the PTestChild class, which is intended to be inherited by the class which implements the child actor. Here is a tour of the code, with explanations:

#include "mozilla/test/PTest.h"

This imports the code for the two message classes, Msg_Hello and Msg_World.

#include "base/id_map.h"

This is for the IDMap mActorMap. See below.

#include "mozilla/ipc/AsyncChannel.h"

class /*NS_ABSTRACT_CLASS*/ PTestChild :
    public mozilla::ipc::AsyncChannel::AsyncListener

The AsyncListener class, which is nested in the AsyncChannel class, assures that classes that derive from it implement the OnMessageReceived member function. PTestChild does this, as you will see below.

{
protected:
    virtual bool RecvHello() = 0;

RecvHello is the function that must be implemented by the actor code which inherits PTestChild.

private:
    typedef IPC::Message Message;
    typedef mozilla::ipc::AsyncChannel Channel;
    typedef mozilla::ipc::AsyncChannel::AsyncListener ChannelListener;

    Channel mChannel;
    IDMap mActorMap;
    int32 mLastRouteId;

mChannel is what provides the ability to transmit and receive messages. It is an AsyncChannel. That class, in turn, gets much of its functionality from its base class, IPC::Channel.

public:
    PTestChild() :
        mChannel(ALLOW_THIS_IN_INITIALIZER_LIST(this)),
        mLastRouteId(0)
    {
        MOZ_COUNT_CTOR(PTestChild);
    }

    virtual ~PTestChild()
    {
        MOZ_COUNT_DTOR(PTestChild);
    }

Note that MOZ_COUNT_?TOR are macros which inject allocation tracking code into debug builds. (See here.)

    bool Open(
            Channel::Transport* aTransport,
            MessageLoop* aThread = 0)
    {
        return (mChannel).Open(aTransport, aThread);
    }

    void Close()
    {
        (mChannel).Close();
    }

Open() and Close() are implemented with the Channel.

    bool SendWorld()
    {
        PTest::Msg_World* msg;

        msg = new PTest::Msg_World();
        (msg)->set_routing_id(MSG_ROUTING_CONTROL);

        return (mChannel).Send(msg);
   }

This is how the "World" message is actually sent. A memory allocation occurs here — a PTest::Msg_World object. Where is that object deallocated? It is deleted by the Channel after the message is sent. That is the convention of the IPC machinery.

Message sending is expensive for many reasons. One reason is because the memory for the message must be dynamically allocated and later deallocated. Another reason is because the message must be copied (by the OS kernel) into the address space of the receiving process.

    virtual Result OnMessageReceived(const Message& msg)
    {
        switch ((msg).type()) {
        case PTest::Msg_Hello__ID:
            {
                bool __readok = PTest::Msg_Hello::Read((&(msg)));
                if ((!(__readok))) {
                    return MsgPayloadError;
                }

                if ((!(RecvHello()))) {
                    return MsgValueError;
                }

                return MsgProcessed;
            }
        default:
            {
                return MsgNotKnown;
            }
        }
    }

Here is the function that is called whenever a message is received on the Channel.

    virtual int32 Register(ChannelListener* aRouted)
    {
        int32 tmp = (--(mLastRouteId));
        (mActorMap).AddWithID(aRouted, tmp);
        return tmp;
    }
    virtual int32 RegisterID(
            ChannelListener* aRouted,
            int32 aId)
    {
        (mActorMap).AddWithID(aRouted, aId);
        return aId;
    }
    virtual ChannelListener* Lookup(int32 aId)
    {
        return (mActorMap).Lookup(aId);
    }
    virtual void Unregister(int32 aId)
    {
        return (mActorMap).Remove(aId);
    }
};

These 4 functions make PTestChild into a class which implements IProtocolManager. That functionality is unused, as of this writing, on 23 October 2009.

PTestParent.h

The final class generated by the IPDL compiler, PTestParent is the mirror of PTestChild. The code is similar to what follows.

#include "mozilla/test/PTest.h"
#include "base/id_map.h"
#include "mozilla/ipc/AsyncChannel.h"

class /*NS_ABSTRACT_CLASS*/ PTestParent :
    public mozilla::ipc::AsyncChannel::AsyncListener
{
protected:
    typedef mozilla::ipc::ActorHandle ActorHandle;
    
    virtual bool RecvWorld() = 0;
    
private:
    typedef IPC::Message Message;
    typedef mozilla::ipc::AsyncChannel Channel;
    typedef mozilla::ipc::AsyncChannel::AsyncListener ChannelListener;
    
public:
    PTestParent() :
        mChannel(ALLOW_THIS_IN_INITIALIZER_LIST(this)),
        mLastRouteId(0)
    {
        MOZ_COUNT_CTOR(PTestParent);
    }

    virtual ~PTestParent()
    {
        MOZ_COUNT_DTOR(PTestParent);
    }
    
    bool Open(
            Channel::Transport* aTransport,
            MessageLoop* aThread = 0)
    {   
        return (mChannel).Open(aTransport, aThread);
    }
    
    void Close()
    {
        (mChannel).Close();
    }
    
    bool SendHello()
    {
        PTest::Msg_Hello* msg;

        msg = new PTest::Msg_Hello();
        (msg)->set_routing_id(MSG_ROUTING_CONTROL);

        return (mChannel).Send(msg);
    }

    virtual Result OnMessageReceived(const Message& msg)
    {
        switch ((msg).type()) {
        case PTest::Msg_World__ID:
            {
                bool __readok = PTest::Msg_World::Read((&(msg)));
                if ((!(__readok))) {
                    return MsgPayloadError;
                }

                if ((!(RecvWorld()))) {
                    return MsgValueError;
                }

                return MsgProcessed;
            }
        default:
            {
                return MsgNotKnown;
            }
        }
    }

    virtual int32 Register(ChannelListener* aRouted)
    {
        int32 tmp = (++(mLastRouteId));
        (mActorMap).AddWithID(aRouted, tmp);
        return tmp;
    }
    virtual int32 RegisterID(
            ChannelListener* aRouted,
            int32 aId)
    {
        (mActorMap).AddWithID(aRouted, aId);
        return aId;
    }
    virtual ChannelListener* Lookup(int32 aId)
    {
        return (mActorMap).Lookup(aId);
    }
    virtual void Unregister(int32 aId)
    {
        return (mActorMap).Remove(aId);
    }

private:
    Channel mChannel;
    IDMap mActorMap;
    int32 mLastRouteId;
};

Implementing PTestParent

The code for Implementing the parent actor in the protocol might look something like these two C++ files.

TestParent.h

class TestParent :
    public PTestParent
{
    virtual bool RecvWorld();

    TestParent();
    virtual ~TestParent();
};

TestParent.cpp

bool TestParent::RecvWorld()
{
    // Do stuff that happens whenever the "World" message is received.
    return true;
}

TestParent::TestParent()
{
    MOZ_COUNT_CTOR(TestParent);
    // Do stuff to construct TestParent...
}

TestParent::~TestParent()
{
    MOZ_COUNT_DTOR(TestParent);
    // ...
}

Of course, the pattern is similar for TestChild.

I hope that this has been a good introduction to the functionality provided by the C++ classes generated by the IPDL compiler.