Cap'n Proto is a fast, compact binary serialization protocol that is similar to Protocol Buffers. It allows for platform-independent serialization of structured data and supports cross-language RPC using an event-driven model like Node.js. Data is defined using a .capnp schema file that generates code for serialization/deserialization. It supports primitive types, lists, structs, and other features. RPC uses an interface definition and promises to allow for asynchronous calls between clients and servers with "time travel" capabilities.
1. Cap’n Proto or: How I
Learned to Stop Worrying
and Love RPC
Razvan Rotari
2. What it is?
● Serialization protocol
● Like JSON but binary
● Similar to Protocol Buffers (same author)
3. Features
● Fast
● Platform independent format - always in little-endian
● Backwards compatible
● Cross language support
● Strong Typed
● Reference counted
● Time Travel RPC
4. Serialization
Uses a schema file to define the message structure.
The schema file is compiled to the target language using capnp.
capnp compile -oc++ schema.capnp
Will generate a schema.capnp.h and schema.capnp.c++ that need to be
included in your application
5. Serialization
Supported types:
● Void: Void
● Boolean: Bool
● Integers: Int8, Int16, Int32, Int64
● Unsigned Integers: UInt8, UInt16, UInt32, UInt64
● Floating-point: Float32, Float64
● Blobs: Text, Data
● Lists: List(T)
● Structs
● Generic types - Similar to Java Generics
● Unions
● Interfaces
● Methods
No support for dictionaries!
6. .capnp file example
#id generated by capnp
@0xed1e03e015818faa;
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 {
work @0;
mobil @1;
home @2;
}
}
}
struct AddressBook {
contacts @0 :List(Person);
}
7. How to use it? Write
#include <AddressBook.capnp.h>
void writeAddressBook(int fd) {
::capnp::MallocMessageBuilder message;
AddressBook::Builder addressBook = message.initRoot<AddressBook>(); //Create the
root node
::capnp::List<Person>::Builder people = addressBook.initContacts(1);
Person::Builder ion = people[0];
ion.setName("Ion");
...
// Lists are fixed sized
::capnp::List<Person::PhoneNumber>::Builder ionPhones = ion.initPhones(1);
ionPhones[0].setNumber("0755-555-321");
ionPhones[0].setType(Person::PhoneNumber::Type::MOBILE);
writePackedMessageToFd(fd, message);
}
8. How to use it? Read
void printAddressBook(int fd) {
::capnp::PackedFdMessageReader message(fd);
AddressBook::Reader addressBook = message.getRoot<AddressBook>();
for (Person::Reader person : addressBook.getContacts()) {
std::cout << person.getName().cStr() << ": " << person.getEmail().cStr()
<< std::endl;
for (Person::PhoneNumber::Reader phone : person.getPhones()) {
std::cout << " " << " phone: " << phone.getNumber().cStr()
<< std::endl;
}
}
}
9. RPC
● Uses KJ concurrency framework
● Event driven
● Based on event loop, promises and callbacks
● Similar to node.js
● Can be used over TCP or UNIX sockets
10. RPC .capnp example
@0x952fc0868f401293;
interface User {
//Methods need to include a index number
login @0 (username :Text, password :Text) -> (token :AuthToken);
getAge @1 (token :AuthToken) -> (age :UInt32);
struct AuthToken {
owner @0 :Text;
token @1 :UInt64;
}
}
11. Server example
class UserImpl final: public User::Server {
public:
kj::Promise<void> login(LoginContext context) override {
if (context.getParams().hasUsername()) // All fields are optional by default
auto userName = context.getParams().getUsername();
auto token = context.getResults().getToken();
token.setToken(40);
return kj::READY_NOW;
}
};
…
capnp::EzRpcServer server(kj::heap<UserImpl>(), “127.0.0.1”, 5923);
auto& waitScope = server.getWaitScope(); //Register an event loop for this thread
kj::NEVER_DONE.wait(waitScope);
12. Client example
capnp::EzRpcClient client(“127.0.0.1”, 5923);
auto& waitScope = client.getWaitScope();
// Request the bootstrap capability from the server.
User::Client cap = client.getMain<User>();
// Create a request
auto request = cap.loginRequest();
request.setUsername("admin");
request.setPassword("123456");
auto promise = request.send(); // Make a call to the server.
// Wait for the result. This is the only line that blocks.
auto response = promise.wait(waitScope);
13. Time Travel!
The result of a RPC call can be used
immediately, even before the server receives it.
The only catch is that it can only be used in
another RPC request.
For example:
foo(bar(f())
Will do a single network round trip.