MACE  1.0.0
 All Classes Namespaces Files Functions Variables Enumerations Defines
Stub Library
MACE - Massively Asynchronous Coding Environment

As defined by wikipedia, a stub is a piece of code used for converting parameters passed durring a Remote Procedule Call (RPC). The MACE Stub library provides a means to automatically and dynamically generate Stubs for C++ class interface without resorting to code generation or an interface description language (IDL).

The definition of RPC traditionally implies over the network, however, for the purposes of this library 'remote' could also mean a different thread, process, a scripting engine or any other non-native calling system. Examples are provided for a simple RPC over UDP system and an actor paraidigm via Boost.ASIO.

Using stubs you can achieve a level of abstraction in your code that is not possible by other means. Your objects no long care about where they are run: which thread, which process, or which machine, or which protocol or transport is used.

Stub Basic Example (Type Erasure)

The first step is to define your stub interface. This could be an interface lacking implementation, or it could be any other class in you wish to expose on the network.

Given the following interfaces:

struct service {
  std::string name()const;
  int         exit();
};
struct calculator : service {
  double add( double v, double v2 );
  double sub( double v, double v2 );           
};

You would use the following macros to define an appropriate stub:

MACE_STUB( service, (name)(exit) )
MACE_STUB_DERIVED( calculator, (service), (add)(sub) )

Once you have defined your stub you can use mace::stub::ptr<calculator> to refer to any object with that interface. For example consider the following class:

class CalculatorServer {
  public:
    std::string name()const            { return "CalculatorServer"; }
    int    exit()                      { ::exit(0);                  }
    double add( double v, double v2 )  { return m_result += v + v2;  }
    double sub( double v, double v2 )  { return m_result -= v - v2;  }
  private:
    double m_result;
};

It provides all of the methods of calculator but does not share the same inheritance nor a common base class. Even though there is no relationship between CalculatorServer and calculator, we can still use it generically in any algorithm that operates on a mace::stub::ptr<calculator>.

    stub::ptr<calculator> calc( boost::make_shared<CalculatorServer>() );

RPC Client Example

The real power of stubs goes beyond simple type erasure, to providing automatic conversion of parameters passed durring a remote procedure call whether 'remote' means a different Boost.ASIO strand, or a different computer running software written in a different language. Normally this task is performed via code generation that takes an interface description language and generates a stub class; however, the MACE Stub Library utilizes the power of C++ templates to achieve the same transformation natively and as much run-time effeciency as code generation schemes.

This example will demonstrate how to initialize a stub::ptr<calculator> that will perform RPC calls over UDP using Boost.Serialization to serialize the arguments and deserialize the results. For the sake of simplicity, this example uses blocking synchronous network calls.

The primary thing to understand is that the default mace::stub::ptr<T> is essentially a virtual table comprized of boost::function<> objects. To define an RPC client we must merely generate a functor for each method. This can be done using a visitor.

RPC Client Functor

First, lets define the function object.

     template<typename Seq, typename ResultType>
     struct rpc_functor {
       rpc_functor( rpc_client& c, const char* name )
       :m_client(c),m_name(name){}

       ResultType operator()( const Seq& params )const {
          // serialize the parameters
          std::ostringstream os; 
          boost::archive::binary_oarchive oa(os);
          serialize_fusion_vector(oa, params);
          // make a call and store the result into the input stream; 
          std::istringstream is(m_client.invoke( m_name, os.str() ) );

          // unpack the result type
          ResultType  ret_val;
          boost::archive::binary_iarchive ia(is);
          ia >> ret_val;
          return ret_val;
       }
       const char* m_name;
       rpc_client& m_client;
     };

In the example, the functor takes two template parameters, a Boost.Fusion Sequence (parameter types) and a ResultType. The constructor takes a pointer to our rpc_client which handles communication and a function name which is used to identify the method when it is received by the RPC server.

The body of the functor takes a Sequence of parameters and returns a Result. It uses boost serialization to serialize the parameters, sends the serialized string out over the network and then dezerializes the result.

RPC Client Visitor

Next we must assign each method on our stub::ptr<T> with a properly instantiated instance of our rpc_functor<Seq,Result>. This is achieved by creating a visitor:

     struct set_visitor {
       set_visitor( rpc_client& ci )
       :c(ci){}

       template<typename MemberPtr, MemberPtr m>
       void operator()( const char* name )const {
         typedef typename boost::function_types::result_type<MemberPtr>::type member_ref;
         typedef typename boost::remove_reference<member_ref>::type member;
         ((*c).*m) = rpc_functor<typename member::fused_params, 
                                 typename member::result_type>( c, name );
       }
       rpc_client& c;
     };

This visitor will be called for each method on the interface. MemberPtr will have a type of something like: MemberType (stub::vtable<Interface,InterfaceDelegate>::*) and m will be a compile-time constant pointing to the actual member. By passing the member pointer as a template parameter instead of an argument the compiler is given the opportunity to inline more effectively as well as reduce unnecessary indirection.

MemberType depends entirely upon which InterfaceDelegate was used to define your stub. The InterfaceDelegate enables the stub interface to be transformed. The default delegate simply 'mirrors' the actual interface with the addition of the ability to make fused calls. Lets stop to take a look at how mace::stub::mirror_interface defines MemberType.

template<typename R, typename Class, PARAM_TYPE_NAMES>
struct mirror_member<R(Class::*)(PARAM_TYPES)const> {
  typedef typename adapt_void<R>::result_type                    result_type;
  typedef mirror_member                                          self_type;
  typedef boost::fusion::vector<PARAM_TYPES>                     fused_params;
  typedef boost::fusion::vector<DEDUCED_PARAM_TYPES>             deduced_params;
  typedef boost::function_traits<result_type(PARAM_TYPES)>       traits;
  static const bool                                              is_const = true;

  typedef typename boost::remove_pointer<result_type(*)(PARAM_TYPES)>::type   signature;

  result_type operator()( PARAM_ARGS )const {
    return m_delegate( boost::fusion::make_vector(PARAM_NAMES) );
  }
  result_type operator() ( const fused_params& fp )const {
    return m_delegate( fp );
  }
  template<typename T>
  mirror_member& operator=( const T& d )  {
    m_delegate = adapt_void<R,T>(d);
    return *this;
  }
  template<typename C, typename M>
  void set_delegate(  C* s, M m );
  private:
    boost::function<result_type(const fused_params&)> m_delegate; 
};

As you can see the MemberType provided by mace::stub::mirror_interface provides many helpful template-meta-programming typedefs that we can leverage within our visitor. In particular, knowing the fused parameter type and the result type is critical in initializing the proper rpc_functor<Seq,Result>.

Because MemberPtr is the type of a pointer, we must enlist Boost.FunctionTypes to get the type of the member before we can access type result_type and fused_params type.

Once we have the result_type and fused_params we create an rpc_functor<member::fused_params,member::result_type> and give it the name of the function which is passed with our visitor along with a pointer to the rpc_client which ultimately dispatches the remote procedure call after the parameters are serailized by rpc_functor<>.

Visiting your Interface

After you have defined your functor type and visitor type all that remains is to visit your interface.

namespace mace { namespace stub {

template<typename InterfaceType,typename InterfaceDelegate=mirror_interface>
class rpc_client : public stub::ptr<InterfaceType,InterfaceDelegate> {
  public:
    rpc_client()
    :m_ios(),m_sock(m_ios) {
      vtable_reflector<InterfaceType,InterfaceDelegate>::visit( set_visitor( *this ) );
    }

This is done using the mace::stub::vtable_reflector<InterfaceType,InterfaceDelegate>::visit method which you will pass a copy of your visitor. This method will then call your visitor for each member on the interface.

RPC Client Invoke

The last detail of our RPC client is the method that actually sends the call and waits for a response. This code is dirt simple:

   std::string invoke( const char* name, const std::string& params ) {
     std::ostringstream os;
     boost::archive::binary_oarchive oa(os);
     std::string n(name);
     oa << n;
     oa << params;

     m_sock.send_to( boost::asio::buffer( os.str() ), m_ep );

     boost::asio::ip::udp::endpoint rep;
     std::vector<char> recv_buf(2048);
     size_t len = m_sock.receive_from( boost::asio::buffer(recv_buf), rep );
     return std::string(&recv_buf.front(),len);
   }

This is the method referenced from the RPC Client Functor.

Using our RPC Client

Now that we have defined how to convert an interface to an RPC over UDP interface, we can use this interface with a natural C++ calling syntax. Algorithms that are designed around mace::stub::ptr<calculator> would not be able to tell the difference between a RPC client or a simple local pointer.

  double r  = calc->add( 5, 5 );
  double r2 = generic_calc->add( 5, 5 );

Defining an RPC Server

The RPC Server would use a similar pattern, except its visitor would store a functor in a map indexed by method name and the functor would deserialize parameters, call the original method, and serialize the result.

      // rpc_server
       template<typename Seq, typename Functor>
       struct rpc_functor {
           rpc_functor( Functor f )
           :m_func(f){}

           std::string operator()( const std::string& params )const {
                Seq paramv;
                std::istringstream is(params);
                boost::archive::binary_iarchive ia(is);
                deserialize_fusion_vector(ia,paramv);                    

                std::ostringstream os;
                boost::archive::binary_oarchive oa(os);
                typename boost::remove_reference<Functor>::type::result_type r = m_func(paramv);
                oa << r;
                return os.str();
           }
           Functor m_func;
       };


       template<typename T>
       struct get_visitor {
          get_visitor( vtable<T>& vt, rpc_server& si )
          :v(vt),s(si){}

          template<typename MemberPtr, MemberPtr m>
          void operator()( const char* name )const {
            typedef typename boost::function_types::result_type<MemberPtr>::type member_ref;
            typedef typename boost::remove_reference<member_ref>::type member;
            s.methods[name] = rpc_functor<typename member::fused_params, 
                                BOOST_TYPEOF( v.*m )&>(v.*m);
          }
          vtable<T>& v;
          rpc_server& s;
       };

       std::map<std::string, boost::function<std::string(const std::string)> > methods;

In under 200 lines of code we were able to create a generic, reusable, effecient RPC framework that provides a natural calling syntax via the use of the MACE.Stub library. The full listing of this code can be found at examples/rpc.hpp