Skip to content

martinhaefner/simppl

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

The simppl/dbus C++ library provides a high-level abstraction layer
for DBus messaging by using solely the C++ language and compiler for
the definition of remote interfaces. No extra tools are necessary to
generate the binding glue code.


Setup

For building the library and tests from source code you need cmake and
gtest. I use cmake 2.8 and gtest-1.7. You may need some changes in the
setup for environment variables in the contributed script file update.sh.
Development libraries of libdbus-1 are also needed for a successful build.


Compilers

Well, I only developed on GCC >=4.9 and clang/LLVM 3.4. Due to the heavy
usage of templating techniques and C++1y language features I cannot
guarantee that any of the code fragments will work with other compilers
(especially VS compiler).

For structure serialization boost::fusion can be used. See benchmark example
for more information how to serialize structures when sending remote messages.
The boost way is the formal and correct way to serialize structures. The old
way of defining serializer_type's typdefs within structures is no longer
recommended since that one makes heavy use of C++ language to memory
mappings and will definitely not work with complex data types containing
virtual functions or containing multiple inheritence.



Status

The library is currently not used in production projects and therefore
it still can be regarded as 'proof of concept'. Due to the simplicity
of the glue layer and the main functionality provided by libdbus I would
already recommend to give it a try, even in real projects.


CopyLeft

Feel free to do with the code whatever you want. It's free open-source code.


First steps

DBus uses an XML document for interface definition. This is rather tricky
to read and typically source code is generated in order to provide
these interfaces in the native programming language of a project.
simppl/dbus does provide a binding written entirely in C++ itself. 
Anyhow, introspection is available for simppl services. 

DBus provides many possibilities to map services (interfaces) to busnames
and objectpaths. Simppl currently makes some assumptions and therefore
looses some generality which is no problem for pure simppl projects but
currently may provide trouble with accessing existing services which do
not share these simplifications. But let's go on and see how a simppl
service is defined. We will recall these facts later.


Defining an interface


Let's start with a simple echo service. See the service description:


   namespace test
   {

   INTERFACE(EchoService)
   {
      Request<std::string> echo;
      Response<std::string> echo_response;
   };

   }   // namespace


Remember, this is pure C++. The header file with this definition can be
included by the server  and by the client. But, until now there is no
correlation between the request method and the response. A correct
interface definition also needs some hand-written glue, but it's simple.
The full interface looks like this:


   namespace test
   {

   INTERFACE(EchoService)
   {
      Request<std::string> echo;
      Response<std::string> echo_response;

      // constructor
      EchoService()
       : INIT(echo)
       , INIT(echo_response)
      {
         echo >> echo_response;
      }
   };

   }   // namespace


Now the interface is correct C++. You need the implement the constructor
to corretly initialize the members. The correlation of request to response
is always done in the constructor of the interface itself. For oneway
requests this can be omitted.

But, how is the service mapped to dbus? Well, have you seen the namespace
definition? Then you can already imagine how the service is mapped, can't you?
Services are typically instantiated and each instance will have its own
instance name. For now, let's say the instance name was 'myEcho'.
With this information provided in the constructor of the service instance,
the dbus busname requested will be

   test.EchoService.myEcho


The objectpath of the service is build accordingly so the service is
provided under

   /test/EchoService/myEcho


Currently, this mapping is done automatically by simppl and cannot be changed.
This also means that only one interface can be provided by a distinct
objectpath, at least other than the properties interface needed for
providing service attributes/properties. But we will ignore signals and
properties for now and continue with our EchoService. Let's instantiate the
service server by inheriting our implementation class from simppl's
Skeleton:


   class MyEcho : simppl::dbus::Skeleton<EchoService>
   {
      MyEcho()
       : simppl::dbus::Skeleton<EchoService>("myEcho")
      {
      }
   };


This service instance did not implement a handler for the echo requests
yet. But you have seen how the service's instance name is provided.
Let's provide an implementation for the echo method:


   class MyEcho : simppl::dbus::Skeleton<EchoService>
   {
      MyEcho()
       : simppl::dbus::Skeleton<EchoService>("myEcho")
      {
         echo >> std::bind(&handle_echo, this, _1);
      }

      void handle_echo(const std::string& echo_string)
      {
         std::cout << "Client says '" << echo_string << "'" << std::endl;
      }
   };


Now, simppl will receive the echo request, unmarshall the argument and
forward the request to the provided handle_echo method. But there is still
no response yet. Let's send a response back to the client:


   class MyEcho : simppl::dbus::Skeleton<EchoService>
   {
      MyEcho()
       : simppl::dbus::Skeleton<EchoService>("myEcho")
      {
         echo >> std::bind(&MyEcho::handle_echo, this, _1);
      }

      void handle_echo(const std::string& echo_string)
      {
         std::cout << "Client says '" << echo_string << "'" << std::endl;
         respond_with(echo_response(echo_string));
      }
   };


That's simppl, isn't it? Setup the eventloop and the server is finished:


   int main()
   {
      simppl::dbus::Dispatcher disp("bus::session");
      MyEcho instance;

      disp.addServer(instance);
      disp.run();

      return EXIT_SUCCESS;
   }


Now it's time to implement a client. Clients are implemented by instantiation
of a Stub. One can either write blocking clients which is only recommended
for simple tools or write a full featured event-driven client. Let's start
with a simple blocking client:


   int main()
   {
      simppl::dbus::Dispatcher disp("bus:session");

      simppl::dbus::Stub<EchoService> stub("myEcho");
      disp.addClient(stub);

      std::string echoed;
      stub.echo("Hello world!") >> echoed;

      return EXIT_SUCCESS;
   }


That's it. The request call blocks until the response is received and
stored in the echoed variable. In case of an error, an exception is thrown.
Note that even if there is no event loop visible in the main function,
it is necessary though and will be started in the background. That's the
reason why the stub must also be registered to a Dispatcher.

But have a look on the message loop driven client. The best way to implement
such a client is to derive from the stub base template and delegate the
callbacks to member functions.


   class MyEchoClient : simppl::dbus::Stub<EchoService>
   {
      MyEchoClient()
       : simppl::dbus::Stub<EchoService>("myEcho")
      {
         connected >> std::bind(&MyEchoClient::handle_connected, this, _1);
         echo_response >> std::bind(&MyEchoClient::handle_echo, this, _1);
      }

      void handle_connected(simppl::dbus::ConnectionState st)
      {
         if (st == simppl::dbus::ConnectionState::Connected)
         {
            echo("Hello World!);
         }
      }

      void handle_echo(const simppl::cbus::Callstate st, const std::string& echo_string)
      {
         if (st)
         {
            std::cout << "Server says '" << echo_string << "'" << std::endl;
            respond_with(echo_response(echo_string));
         }
         else
            std::cout << "Got error: " << st.what() << std::endl;
      }
   };


Event loop driven clients always get callbacks called from the dbus runtime
when any event occurs. The initial event for a client is the connected event
which will be emitted when the server registers itself on the bus (remember
the busname). After being connected, the client may start a sequence of
request/response pairs like in the example above. These callbacks can be
any function object fullfilling the response signature. The main program
is as simple as in the blocking example:


   int main()
   {
      simppl::dbus::Dispatcher disp("bus:session");

      MyEchoClient client;
      disp.addClient(client);

      disp.run();

      return EXIT_SUCCESS;
   }


But with simppl/dbus it is also possible to model signals
and attributes. Moreover, any complex data can be passed between client
and server. See the following example:


   namespace test
   {
      struct Data
      {
         typedef make_serializer<int, std::string, std::vector<std::tuple<int, double>>>::type serializer_type;

         int i;
         std::string str;
         std::vector<std::tuple<int, double>> vec;
      };


      INTERFACE(ComplexTest)
      {
         Request<Data> eval;
         Response<int, std::string> eval_response;

         Signal<std::map<int,int>> sig;

         Attribute<Data> data;

         ComplexTest()
          : INIT(eval)
          , INIT(eval_response)
          , INIT(sig)
          , INIT(data)
         {
            eval >> eval_response;
         }
      };
   }


The Data structure is any C/C++ struct but must not include virtual functions.
For structures with complexer memory layout it is adviced to use boost::fusion
to map the struct to a template-iteratable type sequence. For simpler types
like above, the make_serializer generates an adequate serializing code for
the structure, i.e. the structure can be used as any simple data type:


   int main()
   {
      simppl::dbus::Dispatcher disp("bus:session");

      simppl::dbus::Stub<ComplexTest> teststub("test");
      disp.addClient(teststub);

      Data d({ 42, "Hallo", ... });

      int i_ret;
      std::string s_ret;

      teststub.eval(d) >> std::tie(i_ret, s_ret);

      return EXIT_SUCCESS;
   }


Have you notices how methods with more than one return value are mapped
to a tuple out parameter which can be tie'd to the local variables in
this blocking call? Also, functions with no output parameters on their
response can be called in a blocking context like the following:


   ...
   // calling a function without response, just wait for flushing the
   // outgoing datastream 
   stub.my_oneway_func(42);
   
   // wait for the response 
   teststub.my_func(d) >> std::nullptr_t();
   ...


The signal and attribute in the example above is only sensefully usable
in an event loop driven client. There is a slight difference between
the attributes concept of simppl and the properties concept of dbus.
This is due to the history of simppl which was not designed having
dbus as the native transport in mind. But it's nothing that cannot be
changed in future. simppl attributes are not writable, from the clients
perspective attributes are just readable. Clients typically register
for update notifications in order to receive changes on the attributes
on server side. See the clients connected callback:


   class MyComplexClient : simppl::dbus::Stub<ComplexTest>
   {
      MyComplexClient()
       : simppl::dbus::Stub<ComplexTest>("myEcho")
      {
         connected >> std::bind(&MyComplexClient::handle_connected, this, _1);
      }

      void handle_connected(simppl::dbus::ConnectionState st)
      {
         if (st == simppl::dbus::ConnectionState::Connected)
         {
            sig.attach() >> std::bind(&MyComplexClient::handle_signal, this, _1);
            data.attach() >> std::bind(&MyComplexClient::handle_data_change, this, _1);
         }
         else
         {
            sig.detach();
            data.detach();
         }
      }

      void handle_signal(const std::map<int,int>& m)
      {
         ...
      }

      void handle_data_change(const Data& d)
      {
         // first callback is due to the attach!
         ...
      }
   };


This was a short introduction to simppl/dbus. I hope you like this easy 
way of developing client/server applications with the means of C++ and 
without the need of a complex tool chain for glue-code generation.

About

simppl::dbus - an easy-to-use C++ D-Bus wrapper

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 13

Languages