C++ Generator¶
One of the targeted languages for the protocol compiler is C++. After processing the source file, the compiler will create two files, a header file and the source file. The two generated files will have the same base name as the .proto
file with .cpp
and .h
extensions, respectively.
All protocol data types are placed in a global protocol
namespace and then in a nested namespace named after the protocol and, finally, in a nested request
or reply
namespace. For instance, if we have a .proto
file called Demo.proto
with a request type Sample
, it would live in the following scope:
namespace protocol { namespace Demo { namespace request { class Sample; }; namespace reply { }; }; };
Type-mapping¶
As C++ evolved, richer data types have been introduced and we want to take advantage of them. The various C++-specific options (--c++-legacy
, --c++-11
, --c++-14
, --c++-17
, and --c++-17exp
) enable the use of these newer types. The protocol compiler types are mapped to C++ types in the following way:
Protocol Compiler Type |
C++ Type | ||||
---|---|---|---|---|---|
pre- C++11 | C++11 | C++14 | C++17 | C++17exp | |
bool |
bool |
||||
int16 |
int16_t |
||||
int32 |
int32_t |
||||
int64 |
int64_t |
||||
double |
double |
||||
string |
std::string |
||||
binary |
std::vector<uint8_t> |
||||
enum T |
enum T |
enum class T |
|||
T [] |
std::vector< T > |
||||
optional T |
std::auto_ptr< T > |
std::unique_ptr< T > |
std::experimental::optional< T > |
From the table, we can see that each of the protocol compiler primitive types are represented in C++ by native types that are robust and exception-safe. One association which may seem strange is the mapping for optional fields. We debated about various designs for this type and settled on using a pointer to the type (where NULL represents no field.) To avoid problems with raw pointers, we decided to wrap the memory resource with an auto_ptr<>
/ unique_ptr<>
. Fortunately, C++17 introduced an optional class type, so newer compilers will have a better representation for optional fields of a protocol.
User-defined Types¶
The struct
keyword, in the protocol source file, defines a new data type and is implemented in C++ as a native structure. The structure resides in the protocol::PROTO-NAME
namespace. A constructor is defined which makes sure an instance of this type has default values (zeroes, NULLs, etc.) All fields are public, with no attempt at data hiding. Because the types are messages to be sent to other processes (which have access to all the fields), there's no reason to restrict access to them.
The structure has a method called marshal(ostream&) const
which marshals the structure to the output stream. The compiler also generates the ==
operator, so two structures can be compared for equality. In addition, a swap()
method is defined which is guaranteed to not throw an exception (which is very useful when writing exception-safe code.)
If the structure has any optional fields, the copy constructors and assignment operators are generated to make sure the underlying auto-pointers are handled correctly.
Request and Reply Messages¶
Request and Reply messages have all the implementation of structs, with some extras thrown in.
All request messages are derived from a common base class: protocol::PROTO-NAME::request::Base
. Its methods include:
static request::Base::Ptr unmarshal(std::istream&) |
Static method which unmarshals a message from the input stream. The message is dynamically allocated so a pointer to it is returned. If there's a problem unmarshalling the message, an std::runtime_error exception is thrown. |
void marshal(std::ostream&) const |
Derived classes implement this so they can be marshaled to an output stream either directly or through this base class interface. |
void deliverTo(request::Base::Receiver&) |
This function is used in conjunction with request::Base::unmarshal() . Rather than applying dynamic_cast<>() to the returned request::Base::Ptr to determine which derived class it is, this method can be called. The argument is an object that was derived from the request::Base::Receiver class (described below.) |
request::Base
defines a nested class, request::Base::Receive
. This is an abstract class which defines an interface of pure, virtual methods. A protocol containing n
request messages will generate a request::Base::Receive
class with n
overloaded void handle()
methods, each version taking one type of request as a parameter.
Reply messages have the same framework as requests messages except that all reply-related functions are in the protocol::PROTO-NAME::reply
namespace.
Example¶
For this example, we'll define a protocol for a simple ACNET chat system (it won't have chat rooms.) The source for the protocol is
request register { string nickname; } reply registered { int32 id; } reply message { int64 time; string who; string what; } request deliver { int32 id; string message; } reply delivered { }
The protocol compiler only creates encoders/decoders for the messages. It doesn't care how you transfer them; it could be via ACNET, TCP, or simply written to a file. In this example, we're going to use ACNET and we've added fields to the messages which lend themselves to ACNET communication. Communication between chat clients and the server works like this:
- The chat server creates a
CHAT
ACNET handle. This is the handle where clients will send their messages. - A client sends a
register
request to the server as a multiple-reply request. Thenickname
field will get filled with the user's nickname. The first reply for the request will be aregistered
reply where theid
field holds a unique value for the client. - When a client wants to send a chat message, it send a single-reply request using the
deliver
request. theid
field is the client's unique ID and themessage
field holds the text. The server will return a singledelivered
reply. Each text message is a new, single-reply request. - When the server receives a
deliver
request, it translates it into amessage
reply (filling inwhen
the message arrived,who
sent it, and themessage
.) This reply is sent to all open multiple-reply requests. Viewed another way, a client makes a multiple-reply request. The first reply is aregistered
reply. All remaining replies aremessage
replies. - When a client cancels the initial, multiple-reply request, the server removes it from its tables/resources.
Running this source through the protocol compiler ("pc -l c++ Chat.proto
") yields two files, Chat.h
and Chat.cpp
.
We define a class that can handle requests of this protocol:
#include <Chat.h>
using namespace protocol::Chat;
class ChatReceiver : public request::Base::Receiver {
public:
virtual void handle(request::register& msg)
{
std::cout << "User is registering with nickname " <<
msg.nickname << '\n';
}
virtual void handle(request::deliver& msg)
{
std::cout << "Chat message from ID " << msg.id << ": \"" <<
msg.message << "\"\n";
}
};
Somewhere in the code, we create an instance of it:
ChatReceiver cr;
Elsewhere, as we receive protocol messages, we unmarshal them and send them to the receiver class:
std::stream is = /* obtain an input stream */
try {
request::Base::unmarshal(is)->deliverTo(cr);
}
catch (...) {
std::cerr << "problem handling the message";
}