Project

General

Profile

Erlang Generator

The new ACSys front-end framework, which participates heavily with the Control System, is written in Erlang so support for this language is required in the protocol compiler. 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 appended with "_protocol" followed with .erl and .hrl extensions.

Erlang has only one level of name-spacing, which is at the module level. Because of this limitation, more information is packed into the names of Erlang records to guarantee uniqueness.

Type-mapping

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

Protocol Compiler Type Erlang Type
bool boolean()
int16 integer()
int32 integer()
int64 integer()
double float() or 'NaN' or '+Inf' or '-Inf'
string string() (or unicode:unicode_binary(), if --erl-bin-str is used)
binary binary()
enum T defines atom() types for each enum value
T [] list(T)
optional T T or 'nil'
_msg_ name{T1 f1; T2 f1} #PROTOCOL_name_msg{f1 :: T1, f2 :: T2}

The primitive types (bool, int16, int32, int64, double) are mapped to the equivalent Erlang primitives. Erlang has only one integer size, so all protocol integer types are mapped to it. The marshal/unmarshal functions ensure integer fields are in the proper range.

String fields are mapped to Erlang strings unless the --erl-bin-str command line option is specified.

For enumerations, the set of names of the enumerated values are converted to atoms. The marshal/unmarshal functions only accept atoms in the set.

Protocol fields representing arrays are mapped to Erlang lists.

Optional fields either contain a value or contain the atom 'nil'.

The struct, request and reply keywords, in the protocol source file, define new data types and are implemented in Erlang as a record type (i.e. tagged tuples.) Because records are defined in header files, they aren't associated with any module so the name of the record gets quite large. The name of the record will consist of three portions separated by underscores. The first part is the protocol name in all lower case. The second part is the name of the structure. The last part is "struct", "request" or "reply".

The module will have four exported functions: marshal_request/1, unmarshal_request/1, marshal_reply/1, and unmarshal_reply/1 (unless either --client or --server command line option is used.)

If the record has any optional fields, they will either contain an item, or the atom 'nil'. It is, therefore, recommended that your don't use the value 'nil' as an enumeration value of your protocol.

Erlang-specific options

The protocol compiler has two command line options that affect Erlang's code generation.

--erl-bin-str

By default, string fields in the protocol are mapped to Erlang strings (i.e. list of integers.) If the Erlang application needs to do processing on these strings, this may be a useful mapping. In some applications, however, the strings aren't manipulated but are, instead, just passed around (DPM is one such application.) For this usage pattern, it's more efficient to leave the strings as binaries. This option generates Erlang source which encodes/decodes protocol strings as unicode:unicode_binary() values.

--erl-no-native

When marshaling values, the generated code makes heavy use of binary manipulation. By default, the code generator adds a -compile(native). directive to the source file to force native code compilation (because byte-code isn't as efficient at handling binaries.) If there is a good reason to not generate native code, then this option prevents it and generates only byte-code (although compiling with the +native command line option cancels the effect of this option.)

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 erlang Chat.proto") yields two files, chat_protocol.erl and chat_protocol.hrl. The .hrl file has the interesting definitions for the protocol:

% Generated by the protocol compiler version 2.0
% DO NOT EDIT THIS FILE DIRECTLY!

-record(chat_register_request, {nickname=[] :: string()}).

-record(chat_deliver_request, {id=0 :: integer(),
                               message=[] :: string()}).

-record(chat_registered_reply, {id=0 :: integer()}).

-record(chat_message_reply, {time=0 :: integer(),
                             who=[] :: string(),
                             what=[] :: string()}).

-record(chat_delivered_reply, {}).

A simple, Erlang client (no GUI, minimal error checking) that uses this protocol is:

-module(chat).

-include_lib("acnet/include/acnet.hrl").
-include("chat_protocol.hrl").

-export([connect/1, send/2]).

connect(Nick) ->
    spawn(fun task_start/1, [Nick]).

send(Pid, Msg) ->
    Pid ! {chat, Msg},
    ok.

task_start(Nick) ->
    acnet:start(chat),
    Req = #chat_register_request{nickname = Nick},
    ReqId = acnet:request_replies(chat, "CHAT@NODE", Req, chat_protocol),
    receive
        #acnet_reply{ref = ReqId, data = #chat_registered_reply{id = Id}} ->
            task_main(ReqId, Id)
    end.

task_main(ReqId, Id) ->
    receive
        {chat, Msg} ->
            Req = #chat_deliver_request{id = Id, message = Msg},
            acnet:rpc(chat, "CHAT@NODE", 1000, Req, chat_protocol);

        #acnet_reply{data = #chat_message_reply{who = Who, what = What},
                     ref = ReqId} ->
            io:format("~s said \"~s\"~n", [Who, What])
    end,
    task_main(ReqId, Id).

An example of using it:

1> C = chat:connect("Rich").
2> chat:send(C, "Hello!").
Rich said "Hello!" 
3>