Overview of ecapnp: the Cap'n Proto library for Erlang

Copyright 2013, Andreas Stenius <kaos@astekk.se>

Version: May 30 2014 12:56:29

Authors: Andreas Stenius (kaos@astekk.se) [web site: http://blog.astekk.se].

Description

The ecapnp library supports both Cap'n Proto serialization and RPC. The RPC support is currently a level 1 implementation.

Cap'n Proto schemas are compiled with the capnpc-erl plugin, generating Erlang modules.

To invoke the Cap'n Proto Erlang compiler plugin, it is convenient to use the capnpc-erl script like this:
  capnpc -oerl my_schema.capnp

This requires that ecapnp/bin/ecapnpc-erl is in your $PATH, and that ecapnp is in your Erlang libs path (hint: use the ERL_LIBS environment variable).

//TODO: See Installation for more details.

Schema modules

Once a .capnp schema file has been compiled to an Erlang module, all types defined in that schema is available to you from Erlang.

There are several ways to get the schema for a given type, depending on what you use to find it. Each type is exported as a 0 arity function, both by its name and by its id, as well as a 1 arity function by its name taking a list nested type names.

There's also a common schema/1 function where you can look up the schema for any given type by name or id. Nested types are also supported by passing the type names in a list.

See Sample compiled schema.

Typically, this is schema(root, ...) for getting a root object, schema(get, Field, Object) for reading and schema(set, Field, Value, Object) for writing; where schema is the name of the schema file.

There are also functions for type casting references to lists (or text/data) or other structs (useful when reading fields of type object).

Example

A practical example is best to show what it looks like.

The addressbook example from Cap'n Proto has been ported and serves as an example for ecapnp as well; and is included here as a reference example.

Addressbook schema
The addressbook.capnp schema is defined thus:
@0x9eb32e19f86ee174;

using Cxx = import "/capnp/c++.capnp";
$Cxx.namespace("addressbook");

struct Person {
  id @0 :UInt32;
  name @1 :Text;
  email @2 :Text;
  phones @3 :List(PhoneNumber);

  struct PhoneNumber {
    number @0 :Text;
    type @1 :Type;

    enum Type {
      mobile @0;
      home @1;
      work @2;
    }
  }

  employment :union {
    unemployed @4 :Void;
    employer @5 :Text;
    school @6 :Text;
    selfEmployed @7 :Void;
    # We assume that a person is only one of these.
  }
}

struct AddressBook {
  people @0 :List(Person);
}
To give a feel for how the compiled schema works, here's an excerpt of what it compiles to (with added comments):
%% The schema file id
-vsn(11435534567900897652).

%% functions for getting the schema for any given type
-export([schema/1, 'Person'/0, 'Person'/1, '10988939875124296728'/0, '9317543775882349264'/0,
	 '10511609358742521391'/0, '13477914502553102653'/0, 'AddressBook'/0, 'AddressBook'/1,
	 '17957216978475721012'/0, root/0, root/1, '11435534567900897652'/0]).

%% a list mapping all type names with their corresponding id
-types([{10988939875124296728, 'Person'}, {9317543775882349264, ['Person', 'PhoneNumber']},
	{10511609358742521391, ['Person', 'PhoneNumber', 'Type']},
	{13477914502553102653, ['Person', employment]}, {17957216978475721012, 'AddressBook'},
	{11435534567900897652, root}]).

%% any imported types can be seen in the source, but are not directly listed in
%% the types above, but are accessible using the `schema/1' function
-import('c++_capnp', ['13386661402618388268'/0]).

The root type is the file level schema. It can be used to find all top-level types as well as any annotations that applies to the schema file.

Writing an addressbook

To write an addressbook message, we first need an AddressBook root object:

{ok, AddressBook} = ecapnp:set_root(addressbook_capnp:'AddressBook'().

Now, we can fill in the details. Let's add two people, and call them Alice and Bob:

[Alice, Bob] = ecapnp:set(people, 2, AddressBook).

Now, Alice has one phone number, while Bob has two:

[ecapnp:set(phones, N, P) || {P, N} <- [{Alice, 1}, {Bob, 2}]].

Ok, we're all set to fill in the blanks of the people and phone objects we have allocated. We're using a list comprehension to save on some typing:

[ecapnp:set(Field, Value, Obj)
 || {Obj, FieldValue} <-
          [{Alice,
                [{id, 123},
                 {name, <<"Alice">>},
                 {email, <<"alice@example.com">>},
                 {employment, {shool, <<"MIT">>}},
                 {phones, {0, {number, <<"555-1212">>}}},
                 {phones, {0, {type, mobile}}}]},
           {Bob,
                [{id, 456},
                 {name, <<"Bob">>},
                 {email, <<"bob@example.com">>},
                 {employment, unemployed},
                 {phones, {0, {number, <<"555-4567">>}}},
                 {phones, {0, {type, home}}},
                 {phones, {1, {number, <<"555-7654">>}}},
                 {phones, {1, {type, mobile}}}]}],
    {Field, Value} <- FieldValues].

Note that we could have saved a reference to the phone objects directly and used those instead of embedding them in the calls to Alice and Bob, when we allocated the phone objects, the same way we saved the result when allocating people.

All that is left now is to get the message out. Here's how to get a packed binary ready for dispatching:

Data = ecapnp_serialize:pack(ecapnp_message:write(AddressBook)).

If you intend to send it to io, make sure it uses unicode encoding:

io:setopts([{encoding, unicode}]). will take care of it.

Reading an addressbook message

Assuming you have the packed binary from the previous section in Data, here's how you can read some stuff out of it.

First, we need to unpack it and parse the message header (segment table) before getting at the root struct node:

Unpacked = ecapnp_serialize:unpack(Data),
{ok, Message, <<>>} = ecapnp_message:read(Unpacked),
{ok, AddressBook} = ecapnp:get_root(addressbook_capnp:'AddressBook'(), Message)

Then we can start reading data out of it using ecapnp:get:

%% this will of course fail if the people list is empty
[Person|People] = ecapnp:get(people, AddressBook),
Name = ecapnp:get(name, Person),

[Phone|Phones] = ecapnp:get(phones, Person),
Number = ecapnp:get(number, Phone),

{Employment, Value} = ecapnp:get(employment, Person),

%% `Value' can be the school name, employer or `void' depending on the value of
%% Employment.

RPC

The RPC support is at level 1 (almost). Still lacking is a bit of infrastructure for sockets, and the reference counting/releasing is still a bit of a moving target.

The calculator sample application is the best source of examples, and I won't repeat everything here at this time, as things are still subject to change..

In short, there is:

- ecapnp:request/2 to invoke a call on a capability. - ecapnp:send/1 to dispatch a call request. - ecapnp:wait/1 to wait for the results of a call. - ecapnp_vat:import_capability/3 to restore capabilities.

For now, refer to the client and server samples for the glue code needed.

Project info

Project page
http://ecapnp.astekk.se
Source code
http://github.com/kaos/ecapnp
Cap'n Proto
Home: http://capnproto.com
Code: http://github.com/kentonv/capnproto
License
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Generated by EDoc, May 30 2014, 12:56:29.