Cap'n Proto
Introduction
最近在研究一些編碼的事情,然后就發(fā)現(xiàn)了Cap'n Proto,作者號稱性能是直接秒殺Google Protobuf,直接上官方對比:

雖然我知道很多編碼方案都比Google Protobuf要快很多,但性能快到這個地步,還是第一次聽說,加之后續(xù)我們決定自己的系統(tǒng)也是用這套編碼方案,于是就需要好好研究一下了。
為啥這么快,Cap'n Proto的文檔里面就立刻說明了,因?yàn)檫@個測試Cap'n Proto沒有任何encoding/decoding步驟,Cap'n Proto編碼的數(shù)據(jù)格式跟在內(nèi)存里面的布局是一致的,所以可以直接將編碼好的structure直接字節(jié)存放到硬盤上面。
Cap'n Proto的編碼是方案是獨(dú)立于任何平臺的,但在現(xiàn)在的CPU上面(小端序)會有更高的性能。數(shù)據(jù)的組織類似compiler組織struct:固定寬度,固定偏移,以及合適的內(nèi)存對齊,對于可變的數(shù)組使用pointer嵌入,而pointer也是使用的偏移存放而不是絕對地址。整數(shù)使用的是小端序,因?yàn)槎鄶?shù)現(xiàn)代CPU都是小端序的。
其實(shí)如果熟悉C或者C++的結(jié)構(gòu)體,就可以知道Cap'n Proto的編碼方式就跟struct的內(nèi)存布局差不多。
Example
跟Protobuf一樣,Cap'n Proto也需要定義描述文件,然后通過capnp的編譯器編譯成特定語言的對象使用。一個描述文件的簡單例子:
@0xdbb9ad1f14bf0b36; # unique file ID, generated by `capnp id`
struct Person {
name @0 :Text;
birthdate @3 :Date;
email @1 :Text;
phones @2 :List(PhoneNumber);
struct PhoneNumber {
number @0 :Text;
type @1 :Type;
enum Type {
mobile @0;
home @1;
work @2;
}
}
}
struct Date {
year @0 :Int16;
month @1 :UInt8;
day @2 :UInt8;
}
幾個需要關(guān)注的地方:
- 類型是定義在名字后面的,通常來說,對于一個變量來說,我們可能最關(guān)注的是它的名字,一個好的命名,就很容易讓大家知道是干啥的。譬如上面的name一看就知道是表示的用戶的名字。這點(diǎn)跟c語言是反的,它是先類型,在變量名,不過很多后續(xù)的語言,譬如go,rust等都是先名字,再類型了。
-
@N用來給struct里面的field進(jìn)行編號,編號從0開始,而且必須是連續(xù)的(這點(diǎn)跟Protobuf不一樣)。上面birthdate雖然看起來在email和phones的前面,但是它的編號較大,實(shí)際編碼的時候會放到后面。
參考
注釋
使用 #進(jìn)行注釋,注釋應(yīng)該跟在定義的后面,或者新啟一行:
struct Date {
# A standard Gregorian calendar date.
year @0 :Int16;
# The year. Must include the century.
# Negative value indicates BC.
month @1 :UInt8; # Month number, 1-12.
day @2 :UInt8; # Day number, 1-30.
}
內(nèi)置類型
原生支持的數(shù)據(jù)類型如下:
-
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)
需要注意:
-
Void只有一個可能的值,使用0 bits進(jìn)行編碼,通常很少使用,但是可以作為union的member。 -
Text通常是UTF-8編碼的,使用NULL結(jié)尾的字符串。 -
Data是任意二進(jìn)制數(shù)據(jù)。 -
List是一個泛型類型,我們可以用特定類型去特化實(shí)現(xiàn),譬如List(Int32)就是一個Int32的List。
結(jié)構(gòu)體
結(jié)構(gòu)體其實(shí)類似于c的struct,field的有名字,有類型定義,同時需要編號:
struct Person {
name @0 :Text;
email @1 :Text;
}
Field也可以有默認(rèn)值:
foo @0 :Int32 = 123;
bar @1 :Text = "blah";
baz @2 :List(Bool) = [ true, false, false, true ];
qux @3 :Person = (name = "Bob", email = "bob@example.com");
corge @4 :Void = void;
grault @5 :Data = 0x"a1 40 33";
聯(lián)合
Union是定義在struct里面同一個位置的一組fields,一次只能允許一個field被設(shè)置,我們使用不一樣的tag來獲知當(dāng)前哪個field被設(shè)置了,不同于c里面的union,它不是類型,只是簡單的fields聚合。
struct Person {
# ...
employment :union {
unemployed @4 :Void;
employer @5 :Company;
school @6 :School;
selfEmployed @7 :Void;
# We assume that a person is only one of these.
}
}
union可以沒有名字,但是一個struct里面最多只能包含一個沒名字的union:
struct Shape {
area @0 :Float64;
union {
circle @1 :Float64; # radius
square @2 :Float64; # width
}
}
對于union,我們需要注意:
- Union里面的field需要跟struct的field一起編號。
- 我們在上面的union中使用了
Void類型,這個類型沒有任何額外的信息,僅僅是為了跟其他狀態(tài)區(qū)分。 - 通常,當(dāng)一個struct初始化的時候,在union里面具有最小number field會被默認(rèn)的設(shè)置,如果不想默認(rèn)設(shè)置任何field,我們可以用在union里面的最小number定義一個unset的field。
- 我們可以將當(dāng)前存在的field加入一個新的union,并且不會破壞當(dāng)前數(shù)據(jù)的兼容性。
群組
我們通過group將一組fields封裝到特定的作用域里面:
struct Person {
# ...
# Note: This is a terrible way to use groups, and meant
# only to demonstrate the syntax.
address :group {
houseNumber @8 :UInt32;
street @9 :Text;
city @10 :Text;
country @11 :Text;
}
}
Group并不是struct里面獨(dú)立的一個對象,它里面的fields仍然是struct的fields,需要跟其他struct的fields一起編號。
通常在一個struct里面使用group其實(shí)沒啥大的意思,但是在union里面就比較有趣了:
struct Shape {
area @0 :Float64;
union {
circle :group {
radius @1 :Float64;
}
rectangle :group {
width @2 :Float64;
height @3 :Float64;
}
}
}
在union里面使用group,我們很好的將field進(jìn)行了自說明,現(xiàn)在看到radius,我們就知道它是circle的變量,而不需要額外的注釋了。
當(dāng)然,使用group,對于后續(xù)協(xié)議升級也是很有幫助的,在最開始的時候,我們的shape是square,但是現(xiàn)在想支持rectangle,如果需要額外的加入一個field。如果有g(shù)roup,我們僅僅需要添加一個新的group就可以了。
動態(tài)類型域
Struct可以定義field的類型為AnyPointer,類似于c里面的void*.
枚舉
Enum就是一組符號值的集合:
enum Rfc3092Variable {
foo @0;
bar @1;
baz @2;
qux @3;
# ...
}
Enum的成員必須從0開始編號,在c語言里面,enum通常都是數(shù)字類型的,但是在Cap'n Proto里面,它還可以是其他值。
接口
Interface是一組methods的集合,各個method可以有參數(shù),有返回值,methods也必須從0開始編號。Interface支持繼承,同樣也支持多繼承。
interface Node {
isDirectory @0 () -> (result :Bool);
}
interface Directory extends(Node) {
list @0 () -> (list: List(Entry));
struct Entry {
name @0 :Text;
node @1 :Node;
}
create @1 (name :Text) -> (file :File);
mkdir @2 (name :Text) -> (directory :Directory);
open @3 (name :Text) -> (node :Node);
delete @4 (name :Text);
link @5 (name :Text, node :Node);
}
interface File extends(Node) {
size @0 () -> (size: UInt64);
read @1 (startAt :UInt64 = 0, amount :UInt64 = 0xffffffffffffffff)
-> (data: Data);
# Default params = read entire file.
write @2 (startAt :UInt64, data :Data);
truncate @3 (size :UInt64);
}
泛型
我們可以定義泛型的struct或者interface
struct Map(Key, Value) {
entries @0 :List(Entry);
struct Entry {
key @0 :Key;
value @1 :Value;
}
}
struct People {
byName @0 :Map(Text, Person);
# Maps names to Person instances.
}
在上面的例子中,我們定義了一個泛型的Map,然后在People里面用Text,Person作為參數(shù)來特化這個Map,如果我們了解c++的模板,就可以知道他們差不多。
泛型方法
interface也可以提供泛型method:
interface Assignable(T) {
# A generic interface, with non-generic methods.
get @0 () -> (value :T);
set @1 (value :T) -> ();
}
interface AssignableFactory {
newAssignable @0 [T] (initialValue :T)
-> (assignable :Assignable(T));
# A generic method.
}
我們首先定義了一個泛型的interface,然后在對應(yīng)的factory里面,創(chuàng)建這個interface的method就是泛型的method。
常量
我們可以用const來定義常量
const pi :Float32 = 3.14159;
const bob :Person = (name = "Bob", email = "bob@example.com");
const secret :Data = 0x"9f98739c2b53835e 6720a00907abd42f";
我們可以直接引用這些常量
const foo :Int32 = 123;
const bar :Text = "Hello";
const baz :SomeStruct = (id = .foo, message = .bar);
通常常量都都定義在全局scope里面,我們通過.來進(jìn)行引用獲取。
嵌套,作用域以及別名
我們可以在struct或者interface里面嵌套常量,別名或者新的類型定義。
struct Foo {
struct Bar {
#...
}
bar @0 :Bar;
}
struct Baz {
bar @0 :Foo.Bar;
}
上面Baz里面我們通過Foo.Bar來進(jìn)行類型的獲取。
我們可以使用using對一個類型設(shè)置別名。
struct Qux {
using Foo.Bar;
bar @0 :Bar;
}
struct Corge {
using T = Foo.Bar;
bar @0 :T;
}
導(dǎo)入
我們通過import導(dǎo)入其他文件的類型定義
struct Foo {
# Use type "Baz" defined in bar.capnp.
baz @0 :import "bar.capnp".Baz;
}
也可以直接使用using來設(shè)置別名
using Bar = import "bar.capnp";
struct Foo {
# Use type "Baz" defined in bar.capnp.
baz @0 :Bar.Baz;
}
或者這樣
using import "bar.capnp".Baz;
struct Foo {
baz @0 :Baz;
}
注解
有時候我們需要在Cap'n Proto上面附加一些不屬于Cap'n Proto的自有協(xié)議。這就是Annotation,不過話說真有必要嗎?這里還是先忽略吧。
唯一ID
每個Cap'n Proto文件都必須有唯一的一個64bit ID,使用capnp id生成。譬如最開始例子里面的file ID
# file ID
@0xdbb9ad1f14bf0b36;
其實(shí)struct,enum這些的也需要定義ID,但默認(rèn)情況下面,我們都是自動生成的。
64位的ID還是很可能沖突的,但是實(shí)際不用考慮這樣的情況,反而是錯誤的使用(譬如copy了一個example但沒有更改file ID)更可能導(dǎo)致沖突。
升級協(xié)議
如果我們要升級定義的協(xié)議,需要注意:
- 新的類型,常量或者別名可以添加到任何地方,他們不會影響現(xiàn)有的類型。
- 新的fields,enumerants以及methods需要使用比之前都要大的編號。
- 新加入到method里面的參數(shù)必須添加到參數(shù)列表的最后,并且有默認(rèn)值。
- 成員可以隨意在文件里面變換位置,只要number不變。
- 符號名字可以任意更改,只要ID和number別換就行了。但要注意默認(rèn)生成的ID是根據(jù)父ID以及name來生成的,所以我們需要通過
capnp compile -ocapnp myschema.capnp找到這個名字關(guān)聯(lián)的ID并且在改名后顯示的定義。 - 類型定義可以移動到任意的作用域,只要ID顯示聲明。
- 一個field可以被移入union或者group里面,就像在struct里面替換了以前的field,新加入了一個group或者union。
- 一個非泛型的類型可以變成泛型。(話說對于泛型的研究后續(xù)在考慮吧,總覺得沒必要弄得這么復(fù)雜)
有一些操作是不安全的:
- 別更改field,method或者enum的number號。
- 別更改field,method參數(shù)的類型或者默認(rèn)值。
- 別更改type的ID。
- 別隨便更改沒有顯示ID的類型名字。
- 不能將沒有顯示ID的類型隨便移到其他的作用域里面。
- 不能將一個已經(jīng)存在的field移入/移除到一個已經(jīng)存在的union里面。
還有么?
本文大多數(shù)只是對Cap'n Proto的文檔進(jìn)行了自己的中文解釋,僅僅列出了Cap'n Proto的語言參考,沒有涉及到RPC的問題。另外,因?yàn)槲覀兊南到y(tǒng)不是C++開發(fā),所以還需要熟悉特定語言下面Cap'n Proto的使用。這些都會后續(xù)會好好研究。
最后,Cap'n Proto的LICNESE:
Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors
Licensed under the MIT License:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.