IPDL/Five minute example: Difference between revisions

From MozillaWiki
Jump to navigation Jump to search
No edit summary
 
(14 intermediate revisions by the same user not shown)
Line 1: Line 1:
This page walks you through the steps of creating an IPDL protocol and writing the C++ code that implements the required interfaces.  This guide walks you through the steps taken to implement the testing code in <code>ipc/test-harness</code>.  You are encouraged to follow this guide step-by-step and change the code in <code>ipc/test-harness</code> as you follow along.
This page walks you through the steps of creating an IPDL protocol and writing the C++ code that implements the required interfaces.  It is intended to be a practical complement to the more abstract [[IPDL/Getting started]] guide.


== Write the IPDL specification ==
== Broadly understand IPDL's C++ environment ==


Put this in the file <code>ipc/test-harness/Test.ipdl</code>.
IPDL "describes" the communication of two threads, whether in the same process or not.  For two threads to communicate using IPDL, at least two things are required
# two threads
# an OS-level communication mechanism (transport layer)
Ramping these up in C++ is a rather complicated process, and is likely to be uncommon.  If you really do need this, see the guide [[IPDL/Low level setup]].  This "five minute example" will assume that you can glom onto an existing IPDL environment.
 
Not coincidentally, we provide a bare-bones IPDL environment in the directory <code>ipc/test-harness</code>.  This guide will assume that you're using this environment.  The test-harness environment is set up for "inter-process communication" --- the parent and child actors' threads live in different address spaces.  It contains the following classes
* <code>TestProcessParent</code>: represents the "child" from inside the "parent" process.  Akin to <code>nsIProcess</code>.  TestProcessParent also holds the raw socket fd over which IPDL messages are sent.
* <code>TestThreadChild</code>: the "main thread" of the child process.  Holds the other end of the raw socket over which IPDL messages are sent.
* <code>TestParent</code>: the "parent actor" --- concrete C++ implementation of the IPDL parent actor abstract class.
* <code>TestChild</code>: the "child actor" --- concrete C++ implementation of the IPDL child actor abstract class.  Its code executes on the TestThreadChild thread in the subprocess.
All the necessary code to create the subprocess, set up the communication socket, etc. is already implemented for you.  After all the setup code is run, whatever code you write will be invoked through the method <code>ParentActor::DoStuff</code>.  You can think of this as the test-harness's <code>main()</code> function.
 
== Write the IPDL "Hello, world" protocol ==
 
(No, this isn't the traditional "Hello, world" example.  IPDL lacks facilities for printing.  I'm taking some poetic license.)
 
You should be comfortable with the following IPDL specification.  If not, you should re-read the [[IPDL/Getting started]] guide.  Copy the code into <code>ipc/test-harness/Test.ipdl</code>.


  namespace mozilla {
  namespace mozilla {
  namespace test {
  namespace test {
   
   
  sync protocol Test
  protocol Test
  {
  {
  both:
  child:
     sync Ping() returns (int status);
     Hello();
   
   
  parent:
  parent:
     GetValue(String key);
     World();
    GetValues(StringArray keys);
    sync SetValue(String key, String val) returns (bool ok);
child:
    TellValue(String key, String val);
    TellValues(StringArray keys, StringArray vals);
state START:
    recv Ping goto START;
    send Ping goto START;
    recv SetValue goto HAVE_VALUES;
state HAVE_VALUES:
    recv Ping goto HAVE_VALUES;
    send Ping goto HAVE_VALUES;
    recv SetValue goto HAVE_VALUES;
    recv GetValue goto TELLING_VALUE;
    recv GetValues goto TELLING_VALUES;
state TELLING_VALUE:
    send TellValue goto HAVE_VALUES;
state TELLING_VALUES:
    send TellValues goto HAVE_VALUES;
  };
  };
   
   
Line 46: Line 36:
  } // namespace mozilla
  } // namespace mozilla


== Hook the IPDL file into our build system ==
We will use this specification to make a C++ program in which the parent sends the child the Hello() message, and the child prints "Hello, " to stdout.  The child will then send the parent the World() message, and the parent will print "world!" to stdout.


* Add <code>ipc/test-harness</code> to the IPDLDIRS variable in <code>ipc/ipdl/Makefile.in</code>
== Run the IPDL compiler ==
* Create the file <code>ipc/test-harness/ipdl.mk</code> and add the following text to it.


  IPDLSRCS = \
The IPDL compiler is a python script wrapped around an IPDL module. It can be invoked in two different ways: directly, by calling the IPDL script; and indirectly, through the Mozilla build system.
  Test.ipdl \
  $(NULL)


== Create the C++ implementation stubs ==
To invoke it directly, use a command such as
$ python $ELECTROLYSIS/ipc/ipdl/ipdl.py -d /tmp Test.ipdl
This will run the compiler on "Test.ipdl" and spit out the generated headers into /tmp.  (Try <code>python ipdl.py --help</code> for a list of all options.)


* Run
The second, recommended way is to use the build systemAssuming your working directory is set up correctly (<code>ipc/test-harness</code> is), you only need to add your new protocol specification file to the <code>ipdl.mk</code> file in your working directory, then invoke
ipc/test-harness$ python ../ipdl.py -d /tmp Test.ipdl
$ make -C $OBJDIR/ipc/ipdl
* Open <code>/tmp/mozilla/test/TestProtocolParent.h</code>Look for the text "Skeleton implementation of abstract actor class."
(If your working directory is not set up correctly, see [[IPDL/Low level setup]] for guidance.)
** copy the "Header file contents" into the file <code>ipc/test-harness/TestParent.h</code>
** make the TestParent constructor and destructor public
** copy the "C++ file contents" into <code>ipc/test-harness/TestParent.cpp</code>
** globally replace the text <code>ActorImpl</code> with <code>TestParent</code> in both files.
** set up namespaces as you wish.  The checked-in example puts TestParent in the mozilla::test namespace.
* Repeat the above step for <code>TestProtocolChild.h</code> and <code>TestChild.(h, cpp)</code>


== Hook the C++ stubs into your build configuration ==
After invoking IPDL on the file <code>Test.ipdl</code>, you should see three new headers generated (or updated) in the output directory (-d DIR when invoked directory, $OBJDIR/ipc/ipdl/_ipdlheaders when invoked through the build system).  The headers are output in directories corresponding to the namespace(s) the protocol was defined within.  So invoking the IPDL compiler on the Test protocol above, through the build system, will result in these files being (re)generated: <code>$OBJDIR/ipc/ipdl/_ipdlheaders/mozilla/test/TestProtocol.h</code>, <code>$OBJDIR/ipc/ipdl/_ipdlheaders/mozilla/test/TestProtocolParent.h</code>, <code>$OBJDIR/ipc/ipdl/_ipdlheaders/mozilla/test/TestProtocolChild.h</code>.  Don't worry, the build system also sets up the C++ compiler's include path appropriately.


This is beyond the scope of this guide.  See <code>ipc/test-harness/Makefile.in</code> for an example.
== Implement your protocol's generated C++ interface ==


It's a good idea to check now that everything compiles.
Like Mozilla's IDL compiler, the IPDL compiler generates skeleton, concrete C++ implementations for the "abstract" classes it spits out.  We'll add our "Hello, world!" printing code to those skeleton implementations in this step.


== Create the subprocess class ==
Open <code>$OBJDIR/ipc/ipdl/_ipdlheaders/mozilla/test/TestProtocolChild.h</code> and <code>$OBJDIR/ipc/ipdl/_ipdlheaders/mozilla/test/TestProtocolParent.h</code>.  Look for the sections marked <code>// Skeleton implementation of abstract actor class</code>; you should see something like the following.


This class is instantiated in the main process, and launches and tracks the actual child process.
// ----- [in TestProtocolChild.h] -----
 
// Header file contents
Create the files <code>ipc/test-harness/TestProcessParent.h</code> and <code>ipc/test-harness/TestProcessParent.cpp</code> with the following content.
class ActorImpl :
 
    public TestProtocolChild
  // TestProcessParent.h
  {
#ifndef mozilla_test_TestProcessParent_h
    virtual nsresult RecvHello();
  #define mozilla_test_TestProcessParent_h 1
    ActorImpl();
    virtual ~ActorImpl();
  };
   
   
#include "mozilla/ipc/GeckoChildProcessHost.h"
   
   
  namespace mozilla {
  // C++ file contents
  namespace test {
nsresult ActorImpl::RecvHello()
{
    return NS_ERROR_NOT_IMPLEMENTED;
  }
   
   
  class TestProcessParent : mozilla::ipc::GeckoChildProcessHost
  ActorImpl::ActorImpl()
  {
  {
  public:
  }
    TestProcessParent();
    ~TestProcessParent();
   
   
    /**
ActorImpl::~ActorImpl()
      * Asynchronously launch the plugin process.
{
      */
}
     // Could override parent Launch, but don't need to here
 
     //bool Launch();
// ----- [in TestProtocolParent.h] -----
// Header file contents
private:
class ActorImpl :
     DISALLOW_EVIL_CONSTRUCTORS(TestProcessParent);
     public TestProtocolParent
{
     virtual nsresult RecvWorld();
    ActorImpl();
     virtual ~ActorImpl();
  };
  };
   
   
} // namespace plugins
} // namespace mozilla
#endif // ifndef mozilla_test_TestProcessParent_h
// TestProcessParent.cpp
#include "mozilla/test/TestProcessParent.h"
using mozilla::ipc::GeckoChildProcessHost;
   
   
  namespace mozilla {
  // C++ file contents
  namespace test {
nsresult ActorImpl::RecvWorld()
{
    return NS_ERROR_NOT_IMPLEMENTED;
  }
   
   
  TestProcessParent::TestProcessParent() :
  ActorImpl::ActorImpl()
    GeckoChildProcessHost(GeckoChildProcess_TestHarness)
  {
  {
  }
  }
   
   
  TestProcessParent::~TestProcessParent()
  ActorImpl::~ActorImpl()
  {
  {
  }
  }
} // namespace test
} // namespace mozilla


Now open <code>nsXULAppAPI.h</code> and add the new value <code>GeckoChildProcess_TestHarness</code> to the enumeration <code>GeckoChildProcessType</code>(Yes, this sucks. Sorry.)
Looking past the odd fact that both skeletons are named "ActorImpl," note the substantial difference between the two skeletons: the ProtocolChild requires your C++ code to implement the code that acts on receiving a "Hello()" message, whereas the ProtocolParent requires your C++ code to implement the "World()" handlerIPDL generates boilerplate code to ''send'' these messages, but it has no idea what you want to do upon ''receiving'' them.


Hook this new code into your build system and verify that everything still compiles.
'''NOTE''': if you don't implement these functions, libxul will not link.


== Create the child process's main thread ==
Now we'll make these skeletons do something useful.  Take the code marked "// Header contents" in the ProtocolChild.h header and put it in a file called <code>ipc/test-harness/TestChild.h</code>.  Rename "ActorImpl" to "TestChild," and make the constructor and destructor public.  Put it inside the mozilla::test namespace.  Next, take the code marked "// C++ file contents", put it in a file called <code>ipc/test-harness/TestChild.cpp</code>, and s/ActorImpl/TestChild/g again.  Finally, repeat for the ProtocolParent.h header, creating TestParent.h and .cpp files.


The child process's main thread will hook up the TestChild actor to an IPC channel.
As mentioned above, the existing test-harness calls into <code>TestParent::DoStuff</code> when it has finished low-level initialization.  Let's implement that method now.


Create the files <code>ipc/test-harness/TestThreadChild.h</code> and <code>ipc/test-harness/TestThreadChild.cpp</code> with the following content.
void TestParent::DoStuff()
{
    puts("[TestParent] in DoStuff()");
    SendHello();
}


// TestThreadChild.h
This code will print the string above to stdout, then send the "Hello()" message to the TestChild actorWhen the TestChild actor receives that message, its <code>RecvHello()</code> method will be called. Let's next implement that. Change its skeleton definition to
#ifndef mozilla_test_TestThreadChild_h
#define mozilla_test_TestThreadChild_h
#include "mozilla/ipc/GeckoThread.h"
#include "mozilla/test/TestChild.h"
   
namespace mozilla {
namespace test {
class TestThreadChild : public mozilla::ipc::GeckoThread
{
public:
    TestThreadChild();
    ~TestThreadChild();
protected:
    virtual void Init();
    virtual void CleanUp();
private:
    TestChild mChild;
};
} // namespace test
  } // namespace mozilla
   
#endif // ifndef mozilla_test_TestThreadChild_h


  // TestThreadChild.cpp
  nsresult TestChild::RecvHello()
#include "mozilla/test/TestThreadChild.h"
using mozilla::test::TestThreadChild;
using mozilla::ipc::GeckoThread;
TestThreadChild::TestThreadChild() :
    GeckoThread()
  {
  {
    puts("[TestChild] Hello, ");
    SendWorld();
    return NS_OK;
  }
  }
   
 
  TestThreadChild::~TestThreadChild()
As you might guess, this will print "Hello, " to stdout and then send the "World()" message back to the TestParent. And, you got it, this will in turn cause the parent actor's <code>RecvWorld()</code> handler to be invoked.  Let's finally implement that.
 
  nsresult TestParent::RecvWorld()
  {
  {
    puts("[TestParent] world!");
    return NS_OK;
  }
  }
void
TestThreadChild::Init()
{
    GeckoThread::Init();
    mChild.Open(channel(), owner_loop());
}
void
TestThreadChild::CleanUp()
{
    GeckoThread::CleanUp();
    mChild.Close();
}


Hook these into your build and check that everything still compiles.
Now we're ready to compile and run the C++ code.
 
== Put it all together ==
 
First, compile the C++ actor implementations.  This is beyond the scope of this guide; see the Mozilla build documentation.
 
If you put your code in the <code>ipc/test-harness</code>, you can run it by invoking the test-harness binary.  Navigate to your dist/bin directory and run
$ ./ipctestharness
If all goes well, you should see the output
[TestParent] in DoStuff()
[TestChild] Hello,
[TestParent] world!
fly by eventually.  Remember, the first and third messages are printed by the parent process, and the second is printed by the child process.
 
You can enable more detailed logging by setting the <code>MOZ_IPC_MESSAGE_LOG</code> environment variable in DEBUG builds.  For this small example, you will only see something like the following
$ MOZ_IPC_MESSAGE_LOG=1 ./ipctestharness
// [...SNIP...]
[TestParent] in DoStuff()
[time:1248148008902152][TestProtocolParent] SendHello()
[time:1248148008902898][TestProtocolChild] RecvHello()
[TestChild] Hello,
[time:1248148008902939][TestProtocolChild] SendWorld()
[time:1248148008903080][TestProtocolParent] RecvWorld()
[TestParent] world!
Note, however, this code also logs the parameters and returned values of messages that have them (neither message in this little example do), so it can be a valuable debugging asset.
 
== Exercise: write a slightly less trivial protocol ==
 
Try writing a system that behaves as follows.
* the parent keeps a map of String keys to String values
* the parent allows the child to:
** synchronously map a key to a new value
** asynchronously query the current value of a key
** asynchronously query the values of an array of keys


Open <code>xre/toolkit/nsEmbedFunctions.cpp</code>.  See that
After writing your own protocol, take a look at the code in <code>ipc/test-harness</code> that implements the IPDL part of this exerciseDepending on your interpretation of the problem statement this code may not look much like yoursBut it's worth refreshing your knowledge of the language features the example code employs.
  #include "mozilla/test/TestThreadChild.h"
  using mozilla::test::TestThreadChild;
get added to the file somewhereGo to the function <code>XRE_InitChildProcess</code>Add the following case to the switch.
    case GeckoChildProcess_TestHarness:
      mainThread = new TestThreadChild();
      break;
(Yes, this sucks. Sorry)


== Get the subprocess to launch ==
Happy piddling!

Latest revision as of 03:51, 21 July 2009

This page walks you through the steps of creating an IPDL protocol and writing the C++ code that implements the required interfaces. It is intended to be a practical complement to the more abstract IPDL/Getting started guide.

Broadly understand IPDL's C++ environment

IPDL "describes" the communication of two threads, whether in the same process or not. For two threads to communicate using IPDL, at least two things are required

  1. two threads
  2. an OS-level communication mechanism (transport layer)

Ramping these up in C++ is a rather complicated process, and is likely to be uncommon. If you really do need this, see the guide IPDL/Low level setup. This "five minute example" will assume that you can glom onto an existing IPDL environment.

Not coincidentally, we provide a bare-bones IPDL environment in the directory ipc/test-harness. This guide will assume that you're using this environment. The test-harness environment is set up for "inter-process communication" --- the parent and child actors' threads live in different address spaces. It contains the following classes

  • TestProcessParent: represents the "child" from inside the "parent" process. Akin to nsIProcess. TestProcessParent also holds the raw socket fd over which IPDL messages are sent.
  • TestThreadChild: the "main thread" of the child process. Holds the other end of the raw socket over which IPDL messages are sent.
  • TestParent: the "parent actor" --- concrete C++ implementation of the IPDL parent actor abstract class.
  • TestChild: the "child actor" --- concrete C++ implementation of the IPDL child actor abstract class. Its code executes on the TestThreadChild thread in the subprocess.

All the necessary code to create the subprocess, set up the communication socket, etc. is already implemented for you. After all the setup code is run, whatever code you write will be invoked through the method ParentActor::DoStuff. You can think of this as the test-harness's main() function.

Write the IPDL "Hello, world" protocol

(No, this isn't the traditional "Hello, world" example. IPDL lacks facilities for printing. I'm taking some poetic license.)

You should be comfortable with the following IPDL specification. If not, you should re-read the IPDL/Getting started guide. Copy the code into ipc/test-harness/Test.ipdl.

namespace mozilla {
namespace test {

protocol Test
{
child:
    Hello();

parent:
    World();
};

} // namespace test
} // namespace mozilla

We will use this specification to make a C++ program in which the parent sends the child the Hello() message, and the child prints "Hello, " to stdout. The child will then send the parent the World() message, and the parent will print "world!" to stdout.

Run the IPDL compiler

The IPDL compiler is a python script wrapped around an IPDL module. It can be invoked in two different ways: directly, by calling the IPDL script; and indirectly, through the Mozilla build system.

To invoke it directly, use a command such as

$ python $ELECTROLYSIS/ipc/ipdl/ipdl.py -d /tmp Test.ipdl

This will run the compiler on "Test.ipdl" and spit out the generated headers into /tmp. (Try python ipdl.py --help for a list of all options.)

The second, recommended way is to use the build system. Assuming your working directory is set up correctly (ipc/test-harness is), you only need to add your new protocol specification file to the ipdl.mk file in your working directory, then invoke

$ make -C $OBJDIR/ipc/ipdl

(If your working directory is not set up correctly, see IPDL/Low level setup for guidance.)

After invoking IPDL on the file Test.ipdl, you should see three new headers generated (or updated) in the output directory (-d DIR when invoked directory, $OBJDIR/ipc/ipdl/_ipdlheaders when invoked through the build system). The headers are output in directories corresponding to the namespace(s) the protocol was defined within. So invoking the IPDL compiler on the Test protocol above, through the build system, will result in these files being (re)generated: $OBJDIR/ipc/ipdl/_ipdlheaders/mozilla/test/TestProtocol.h, $OBJDIR/ipc/ipdl/_ipdlheaders/mozilla/test/TestProtocolParent.h, $OBJDIR/ipc/ipdl/_ipdlheaders/mozilla/test/TestProtocolChild.h. Don't worry, the build system also sets up the C++ compiler's include path appropriately.

Implement your protocol's generated C++ interface

Like Mozilla's IDL compiler, the IPDL compiler generates skeleton, concrete C++ implementations for the "abstract" classes it spits out. We'll add our "Hello, world!" printing code to those skeleton implementations in this step.

Open $OBJDIR/ipc/ipdl/_ipdlheaders/mozilla/test/TestProtocolChild.h and $OBJDIR/ipc/ipdl/_ipdlheaders/mozilla/test/TestProtocolParent.h. Look for the sections marked // Skeleton implementation of abstract actor class; you should see something like the following.

// ----- [in TestProtocolChild.h] -----
// Header file contents
class ActorImpl :
    public TestProtocolChild
{
    virtual nsresult RecvHello();
    ActorImpl();
    virtual ~ActorImpl();
};


// C++ file contents
nsresult ActorImpl::RecvHello()
{
    return NS_ERROR_NOT_IMPLEMENTED;
}

ActorImpl::ActorImpl()
{
}

ActorImpl::~ActorImpl()
{
}
// ----- [in TestProtocolParent.h] -----
// Header file contents
class ActorImpl :
    public TestProtocolParent
{
    virtual nsresult RecvWorld();
    ActorImpl();
    virtual ~ActorImpl();
};


// C++ file contents
nsresult ActorImpl::RecvWorld()
{
    return NS_ERROR_NOT_IMPLEMENTED;
}

ActorImpl::ActorImpl()
{
}

ActorImpl::~ActorImpl()
{
}

Looking past the odd fact that both skeletons are named "ActorImpl," note the substantial difference between the two skeletons: the ProtocolChild requires your C++ code to implement the code that acts on receiving a "Hello()" message, whereas the ProtocolParent requires your C++ code to implement the "World()" handler. IPDL generates boilerplate code to send these messages, but it has no idea what you want to do upon receiving them.

NOTE: if you don't implement these functions, libxul will not link.

Now we'll make these skeletons do something useful. Take the code marked "// Header contents" in the ProtocolChild.h header and put it in a file called ipc/test-harness/TestChild.h. Rename "ActorImpl" to "TestChild," and make the constructor and destructor public. Put it inside the mozilla::test namespace. Next, take the code marked "// C++ file contents", put it in a file called ipc/test-harness/TestChild.cpp, and s/ActorImpl/TestChild/g again. Finally, repeat for the ProtocolParent.h header, creating TestParent.h and .cpp files.

As mentioned above, the existing test-harness calls into TestParent::DoStuff when it has finished low-level initialization. Let's implement that method now.

void TestParent::DoStuff()
{
    puts("[TestParent] in DoStuff()");
    SendHello();
}

This code will print the string above to stdout, then send the "Hello()" message to the TestChild actor. When the TestChild actor receives that message, its RecvHello() method will be called. Let's next implement that. Change its skeleton definition to

nsresult TestChild::RecvHello()
{
    puts("[TestChild] Hello, ");
    SendWorld();
    return NS_OK;
}

As you might guess, this will print "Hello, " to stdout and then send the "World()" message back to the TestParent. And, you got it, this will in turn cause the parent actor's RecvWorld() handler to be invoked. Let's finally implement that.

nsresult TestParent::RecvWorld()
{
    puts("[TestParent] world!");
    return NS_OK;
}

Now we're ready to compile and run the C++ code.

Put it all together

First, compile the C++ actor implementations. This is beyond the scope of this guide; see the Mozilla build documentation.

If you put your code in the ipc/test-harness, you can run it by invoking the test-harness binary. Navigate to your dist/bin directory and run

$ ./ipctestharness

If all goes well, you should see the output

[TestParent] in DoStuff()
[TestChild] Hello, 
[TestParent] world!

fly by eventually. Remember, the first and third messages are printed by the parent process, and the second is printed by the child process.

You can enable more detailed logging by setting the MOZ_IPC_MESSAGE_LOG environment variable in DEBUG builds. For this small example, you will only see something like the following

$ MOZ_IPC_MESSAGE_LOG=1 ./ipctestharness
// [...SNIP...]
[TestParent] in DoStuff()
[time:1248148008902152][TestProtocolParent] SendHello()
[time:1248148008902898][TestProtocolChild] RecvHello()
[TestChild] Hello, 
[time:1248148008902939][TestProtocolChild] SendWorld()
[time:1248148008903080][TestProtocolParent] RecvWorld()
[TestParent] world!

Note, however, this code also logs the parameters and returned values of messages that have them (neither message in this little example do), so it can be a valuable debugging asset.

Exercise: write a slightly less trivial protocol

Try writing a system that behaves as follows.

  • the parent keeps a map of String keys to String values
  • the parent allows the child to:
    • synchronously map a key to a new value
    • asynchronously query the current value of a key
    • asynchronously query the values of an array of keys

After writing your own protocol, take a look at the code in ipc/test-harness that implements the IPDL part of this exercise. Depending on your interpretation of the problem statement this code may not look much like yours. But it's worth refreshing your knowledge of the language features the example code employs.

Happy piddling!