學習 Cap'n proto
Cap'n Proto
Introduction
最近在研究一些編碼的事情,然後就發現了 Cap'n Proto,作者號稱性能是直接秒殺 Google Protobuf,直接上官方對比:
雖然我知道很多編碼方案都比 Google Protobuf 要快很多,但性能快到這個地步,還是第一次聽說,加之後續我們決定自己的系統也是用這套編碼方案,於是就需要好好研究一下了。
爲啥這麼快,Cap'n Proto 的文檔裏面就立刻說明了,因爲這個測試 Cap'n Proto 沒有任何 encoding/decoding 步驟,Cap'n Proto 編碼的數據格式跟在內存裏面的佈局是一致的,所以可以直接將編碼好的 structure 直接字節存放到硬盤上面。
Cap'n Proto 的編碼是方案是獨立於任何平臺的,但在現在的 CPU 上面(小端序)會有更高的性能。數據的組織類似 compiler 組織 struct:固定寬度,固定偏移,以及合適的內存對齊,對於可變的數組使用 pointer 嵌入,而 pointer 也是使用的偏移存放而不是絕對地址。整數使用的是小端序,因爲多數現代 CPU 都是小端序的。
其實如果熟悉 C 或者 C++ 的結構體,就可以知道 Cap'n Proto 的編碼方式就跟 struct 的內存佈局差不多。
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;
}
幾個需要關注的地方:
- 類型是定義在名字後面的,通常來說,對於一個變量來說,我們可能最關注的是它的名字,一個好的命名,就很容易讓大家知道是幹啥的。譬如上面的 name 一看就知道是表示的用戶的名字。這點跟 c 語言是反的,它是先類型,在變量名,不過很多後續的語言,譬如 go,rust 等都是先名字,再類型了。
@N
用來給 struct 裏面的 field 進行編號,編號從 0 開始,而且必須是連續的(這點跟 Protobuf 不一樣)。上面 birthdate 雖然看起來在 email 和 phones 的前面,但是它的編號較大,實際編碼的時候會放到後面。
參考
註釋
使用 #
進行註釋,註釋應該跟在定義的後面,或者新啓一行:
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.
}
內置類型
原生支持的數據類型如下:
- 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 進行編碼,通常很少使用,但是可以作爲 union 的 member。Text
通常是 UTF-8 編碼的,使用 NULL 結尾的字符串。Data
是任意二進制數據。List
是一個泛型類型,我們可以用特定類型去特化實現,譬如List(Int32)
就是一個 Int32 的 List。
結構體
結構體其實類似於 c 的 struct,field 的有名字,有類型定義,同時需要編號:
struct Person {
name @0 :Text;
email @1 :Text;
}
Field 也可以有默認值:
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";
聯合
Union 是定義在 struct 裏面同一個位置的一組 fields,一次只能允許一個 field 被設置,我們使用不一樣的 tag 來獲知當前哪個 field 被設置了,不同於 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
類型,這個類型沒有任何額外的信息,僅僅是爲了跟其他狀態區分。 - 通常,當一個 struct 初始化的時候,在 union 裏面具有最小 number field 會被默認的設置,如果不想默認設置任何 field,我們可以用在 union 裏面的最小 number 定義一個 unset 的 field。
- 我們可以將當前存在的 field 加入一個新的 union,並且不會破壞當前數據的兼容性。
羣組
我們通過 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 裏面獨立的一個對象,它裏面的 fields 仍然是 struct 的 fields,需要跟其他 struct 的 fields 一起編號。
通常在一個 struct 裏面使用 group 其實沒啥大的意思,但是在 union 裏面就比較有趣了:
struct Shape {
area @0 :Float64;
union {
circle :group {
radius @1 :Float64;
}
rectangle :group {
width @2 :Float64;
height @3 :Float64;
}
}
}
在 union 裏面使用 group,我們很好的將 field 進行了自說明,現在看到 radius,我們就知道它是 circle 的變量,而不需要額外的註釋了。
當然,使用 group,對於後續協議升級也是很有幫助的,在最開始的時候,我們的 shape 是 square,但是現在想支持 rectangle,如果需要額外的加入一個 field。如果有 group,我們僅僅需要添加一個新的 group 就可以了。
動態類型域
Struct 可以定義 field 的類型爲AnyPointer
,類似於 c 裏面的void*
.
枚舉
Enum 就是一組符號值的集合:
enum Rfc3092Variable {
foo @0;
bar @1;
baz @2;
qux @3;
# ...
}
Enum 的成員必須從 0 開始編號,在 c 語言裏面,enum 通常都是數字類型的,但是在 Cap'n Proto 裏面,它還可以是其他值。
接口
Interface 是一組 methods 的集合,各個 method 可以有參數,有返回值,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 作爲參數來特化這個 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,然後在對應的 factory 裏面,創建這個 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 裏面,我們通過.
來進行引用獲取。
嵌套,作用域以及別名
我們可以在 struct 或者 interface 裏面嵌套常量,別名或者新的類型定義。
struct Foo {
struct Bar {
#...
}
bar @0 :Bar;
}
struct Baz {
bar @0 :Foo.Bar;
}
上面 Baz 裏面我們通過Foo.Bar
來進行類型的獲取。
我們可以使用 using 對一個類型設置別名。
struct Qux {
using Foo.Bar;
bar @0 :Bar;
}
struct Corge {
using T = Foo.Bar;
bar @0 :T;
}
導入
我們通過 import 導入其他文件的類型定義
struct Foo {
# Use type "Baz" defined in bar.capnp.
baz @0 :import "bar.capnp".Baz;
}
也可以直接使用 using 來設置別名
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 的自有協議。這就是 Annotation,不過話說真有必要嗎?這裏還是先忽略吧。
唯一 ID
每個 Cap'n Proto 文件都必須有唯一的一個 64bit ID,使用capnp id
生成。譬如最開始例子裏面的 file ID
# file ID
@0xdbb9ad1f14bf0b36;
其實 struct,enum 這些的也需要定義 ID,但默認情況下面,我們都是自動生成的。
64 位的 ID 還是很可能衝突的,但是實際不用考慮這樣的情況,反而是錯誤的使用(譬如 copy 了一個 example 但沒有更改 file ID)更可能導致衝突。
升級協議
如果我們要升級定義的協議,需要注意:
- 新的類型,常量或者別名可以添加到任何地方,他們不會影響現有的類型。
- 新的 fields,enumerants 以及 methods 需要使用比之前都要大的編號。
- 新加入到 method 裏面的參數必須添加到參數列表的最後,並且有默認值。
- 成員可以隨意在文件裏面變換位置,只要 number 不變。
- 符號名字可以任意更改,只要 ID 和 number 別換就行了。但要注意默認生成的 ID 是根據父 ID 以及 name 來生成的,所以我們需要通過
capnp compile -ocapnp myschema.capnp
找到這個名字關聯的 ID 並且在改名後顯示的定義。 - 類型定義可以移動到任意的作用域,只要 ID 顯示聲明。
- 一個 field 可以被移入 union 或者 group 裏面,就像在 struct 裏面替換了以前的 field,新加入了一個 group 或者 union。
- 一個非泛型的類型可以變成泛型。(話說對於泛型的研究後續在考慮吧,總覺得沒必要弄得這麼複雜)
有一些操作是不安全的:
- 別更改 field,method 或者 enum 的 number 號。
- 別更改 field,method 參數的類型或者默認值。
- 別更改 type 的 ID。
- 別隨便更改沒有顯示 ID 的類型名字。
- 不能將沒有顯示 ID 的類型隨便移到其他的作用域裏面。
- 不能將一個已經存在的 field 移入 / 移除到一個已經存在的 union 裏面。
還有麼?
本文大多數只是對 Cap'n Proto 的文檔進行了自己的中文解釋,僅僅列出了 Cap'n Proto 的語言參考,沒有涉及到 RPC 的問題。另外,因爲我們的系統不是 C++ 開發,所以還需要熟悉特定語言下面 Cap'n Proto 的使用。這些都會後續會好好研究。
最後,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.
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://www.jianshu.com/p/f1110b22cb5c