Project

General

Profile

Java Generator

NOTE: THIS IS A WORK IN PROGRESS. It currently has the C++ Wiki page contents, since it most resembles C++. DO NOT USE THIS PAGE AS A REFERENCE YET!

One of the targeted languages for the protocol compiler is Java. After processing the source file, the compiler will create a file with the same base name as the .proto file and a .java extension.

All protocol data types are nested in a top-level class which has the name of the protocol.

namespace protocol {
    namespace Demo {
        namespace request {
            class Sample;
        };

        namespace reply {
        };
    };
};

Type-mapping

The protocol compiler types are mapped to C++ types in the following way:

Protocol Compiler Type C++ Type
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
T [] std::vector<T>
optional T std::auto_ptr<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<>.

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. The nickname field will get filled with the user's nickname. The first reply for the request will be a registered reply where the id 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. the id field is the client's unique ID and the message field holds the text. The server will return a single delivered reply. Each text message is a new, single-reply request.
  • When the server receives a deliver request, it translates it into a message reply (filling in when the message arrived, who sent it, and the message.) This reply is sent to all open multiple-reply requests. Viewed another way, a client makes a multiple-reply request. The first reply is a registered reply. All remaining replies are message 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";
    }