Project

General

Profile

Rust Generator

NOTE: The protocol compiler has support for the Rust programming language, but its API will probably change as we get feedback from Rust programmers.

When a protocol is compiled to Rust, it generates an .rs source file with the name indicating the protocol. For instance, a protocol defined in Chat.proto will generate the file chat_protocol.rs. This file is intended to be a part of a binary or library crate rather than being standalone.

Type-mapping

The base protocol compiler types are mapped to Rust types in the following way:

Protocol Compiler Type Rust Type
bool bool
int16 i16
int32 i32
int64 i64
double f64
string String
binary Vec<u8>
enum T enum T
T [] Vec<T>
optional T Option<T>

User-defined Types

Protocol struct mapping

The struct keyword, in the protocol source file, defines a new data type and is implemented in Rust as a struct data type. For example:

Protocol Struct Rust Mapping
struct Data {
    int64 stamp;
    double data;
};
#[derive(Debug,Default)]
pub struct Data {
    pub stamp: i64,
    pub data: f64
}

Protocol enum mapping

The enum keyword defines a new data type and is implemented as a field-less enumeration. For example:

Protocol Enum Rust Mapping
enum State {
    Idle, On, Off
};
pub enum StateEnum {
    Idle,
    On,
    Off
}

Request and Reply Messages

Requests and replies are defined using enumerated types. Rust enumerations are as powerful as variant types found in functional languages, like OCaml. So, for instance, if the protocol source has two request types like this:

Protocol Requests Rust Mapping
request DeviceInfo {
   string name;
}

request DeviceReading {
    string name;
}
pub enum Request {
    DeviceInfo { name: str },
    DeviceReading { name: str }
};

impl Request {
    pub fn marshal(&self) -> Vec<u8>;
    pub fn unmarshal(iter: &mut Iterator<Item=u8>) -> Option<Request>;
}

The module will use the Codec traits from the tokio-util crate. So the generated Rust module will have the following hierarchy:

// enums defined in protocol

enum *;

// structures defined in protocol

struct *;

// enum which includes all requests

enum Request { ... };

// enum which includes all replies

enum Reply { ... };

// defines Codec used by clients

struct ClientCodec(());

impl Encoder for ClientCodec {
    type Item = Request;
    type Error = ProtocolError;

    fn encode(&mut self, event: Self::Item, buf: &mut BytesMut) -> Result<(), Self::Error>;
}

impl Decoder for ClientCodec {
    type Item = Reply;
    type Error = ProtocolError;

    fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error>;
}

// defines Codec used by services

struct ServiceCodec(());

impl Encoder for ServiceCodec {
    type Item = Reply;
    type Error = ProtocolError;

    fn encode(&mut self, event: Self::Item, buf: &mut BytesMut) -> Result<(), Self::Error>;
}

impl Decoder for ServiceCodec {
    type Item = Request;
    type Error = ProtocolError;

    fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error>;
}

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 rust Chat.proto") yields chat_protocol.rs.