Rustでマルチインターフェースパケットキャプチャを実装する
この記事は Rust Advent Calendar 2018 の3日目の記事です。
本記事では、タイトルの通り、Rustでマルチインターフェースパケットキャプチャを実装します。 今回の記事で達成したい目標は以下2点です。
- Rustでネットワークプログラミングをしたい
- マルチインターフェースにすることでマルチスレッド対応したい
どうやって低レイヤを扱うか
Rustでネットワークプログラミングを行うには、libpnetが便利なので、今回はこちらを利用します。libpnetを使えるようCargo.toml
には以下を記載しておきます。
[dependencies] serde = "1.0" serde_derive = "1.0" [dependencies.pnet] version = "0.21.0"
アーキテクチャ
今回実装するパケットキャプチャのアーキテクチャは以下です。
I/F1
からI/Fn
の各インターフェース用のスレッドがrx
を監視します。
rx
で受信したパケットをqueue
に渡します。
packet handlerは、queue
にあるパケットを一つずつ処理(標準出力に表示)する役割を果たします。
tcpdump
などでは対象や表示形式など引数を設定できますが、今回はL2-4までを対象として、表示対象を以下の通りとします。ARPは遊び心で加えました。
Layer | 表示対象 |
---|---|
L2 | 送信元MACアドレス |
L2 | 宛先MACアドレス |
L2-3 | ARP request |
L2-3 | ARP reply |
L3 | 送信元IPアドレス |
L3 | 宛先IPアドレス |
L4 | 送信元ポート番号 |
L4 | 宛先ポート番号 |
実装
パケットの実装
各パケットが どのインターフェースから着信したものか を保持するために、queueに渡すパケットを以下の構造体にまとめます。
#[derive(Clone, Debug)] struct PacketWithInterface { interface: NetworkInterface, packet: Vec<u8>, }
Queueの実装
各インターフェースのrx
を監視する部分をマルチスレッドで実装します。
各スレッドからqueue
を共有できるようArc
とMutex
で包みます。
#[derive(Clone, Debug)] struct Queue<T: Send> { inner: Arc<Mutex<VecDeque<T>>>, }
上記Queue
に対してnew
、get
、add
を実装します。
注意が必要なのはget
、add
を行う際にlock
を取ってあげることくらいでしょうか。
なお、lock
を取った際に_queue
で束縛しており、_queue
がスコープから外れたタイミングで自動的にunlock
されます。
impl<T: Send> Queue<T> { fn new() -> Self { Self { inner: Arc::new(Mutex::new(VecDeque::new())) } } fn get(&self) -> Option<T> { let _queue = self.inner.lock(); if let Ok(mut queue) = _queue { queue.pop_front() } else { None } } fn add(&self, obj: T) -> usize { let _queue = self.inner.lock(); if let Ok(mut queue) = _queue { queue.push_back(obj); queue.len() } else { 0 } } }
packet handlerの実装
queue
からパケットを取り出す部分はmain内で実装するので、ここでは取り出したパケットの処理することに注力します。
基本的にはバイト列の先頭からL2
-> L3
-> L4
と、各レイヤーのヘッダを確認し、該当するフィールドを表示する処理になります。
なお、ARPパケットはEthernetヘッダに記載されているEtherTypeで判断できます。
fn handle_ethernet_frame(interface: &NetworkInterface, ethernet: &EthernetPacket) { println!( "{}: {} > {}", interface.name, ethernet.get_source(), ethernet.get_destination(), ); print!(" {}: ", ethernet.get_ethertype()); match ethernet.get_ethertype() { EtherTypes::Arp => { let arp = arp::ArpPacket::new(ethernet.payload()).unwrap(); match arp.get_operation() { arp::ArpOperations::Reply => { println!( "ARP reply({}): {} -> {}", arp.get_sender_proto_addr(), arp.get_sender_hw_addr(), arp.get_target_hw_addr() ); } arp::ArpOperations::Request => { println!( "ARP request({}): {} -> {}", arp.get_target_proto_addr(), arp.get_sender_hw_addr(), arp.get_target_hw_addr() ); } _ => (), } } EtherTypes::Ipv4 => { let ip = Ipv4Packet::new(ethernet.payload()).unwrap(); println!("{} -> {}", ip.get_source(), ip.get_destination()); handle_ip_packet(&interface, &ip) } _ => (), } } fn handle_ip_packet(interface: &NetworkInterface, ip: &Ipv4Packet) { print!(" {}: ", ip.get_next_level_protocol()); match ip.get_next_level_protocol() { IpNextHeaderProtocols::Tcp => { let tcp = tcp::TcpPacket::new(ip.payload()).unwrap(); handle_tcp_packet(&interface, &tcp); } IpNextHeaderProtocols::Udp => { let udp = udp::UdpPacket::new(ip.payload()).unwrap(); handle_udp_packet(&interface, &udp); } _ => (), } } fn handle_tcp_packet(_interface: &NetworkInterface, tcp: &tcp::TcpPacket) { println!("{} -> {}", tcp.get_source(), tcp.get_destination()); } fn handle_udp_packet(_interface: &NetworkInterface, udp: &udp::UdpPacket) { println!("{} -> {}", udp.get_source(), udp.get_destination()); }
mainの実装
インターフェースを初期化
キャプチャ対象とするインターフェースをここで指定します。 configファイルから読み取る実装もしてみましたが、今回は簡単のためmainにべた書きとしました。
let interface_names: HashSet<&str> = vec!["RT2_veth0", "RT2_veth1"] .into_iter() .collect(); let interfaces: Vec<NetworkInterface> = datalink::interfaces() .into_iter() .filter(|interface: &NetworkInterface| interface_names.contains(interface.name.as_str())) .collect();
パケットを受信する
各インターフェースのrx
を取得して、ループでrx
を監視します。そしてrx
で受信したパケットをqueue
に投げ込みます。
今回の実装でqueue
に投げ込む部分が一番ハマりました。
rx.next()
で受信できるパケットは&[u8]
なので参照を渡そうとすると、spawn
で作ったクロージャより長生きできず全然コンパイルが通りませんでした。
let mut handles: Vec<_> = interfaces.into_iter() .map(|interface| { let queue = queue.clone(); thread::spawn(move || { let mut rx = datalink::channel(&interface, Default::default()) .map(|chan| match chan { Ethernet(_, rx) => rx, _ => panic!("could not initialize datalink channel {:?}", interface.name), }).unwrap(); loop { match rx.next() { Ok(src) => { queue.add(PacketWithInterface { interface: interface.clone(), packet: src.to_owned(), }); } Err(_) => { continue; } } } }) } ) .collect();
パケットを処理する
queue
からパケットを取り出し、EthernetPacket
にmatch
した後、handle_ethernet_frame
に渡します。渡した後は上述のpacket handlerの責務です。
異常パケットを受信した場合はパケットをドロップし、無視します。異常パケット1つ受信しただけでpanic
してプロセスごと落ちない設計としました。
handles.push(thread::spawn(move || { loop { let queue = queue.clone(); match queue.get() { Some(packet_with_interface) => { let _packet = packet_with_interface.packet.deref(); match EthernetPacket::new(_packet) { Some(packet) => { handle_ethernet_frame(&packet_with_interface.interface, &packet); }, _ => { continue; } } }, _ => { continue; } } } }));
実際に動かしてみる
環境
検証用の環境は以下の通りです。
$ uname -a Linux vagrant-ubuntu-trusty-64 3.13.0-161-generic #211-Ubuntu SMP Wed Oct 3 14:52:35 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
ネットワーク構築
netns
で以下のネットワークを用意します。
構築スクリプトはこちら。
RT2
の★マークの箇所RT2_veth0
とRT2_veth1
がパケットキャプチャの対象インターフェースです。
pingを打ったときのパケットキャプチャ
host
の192.168.0.1
からRT1
の192.168.1.254
へpingを打ってみます。
# host $ ping 192.168.1.254 -c 1 PING 192.168.1.254 (192.168.1.254) 56(84) bytes of data. 64 bytes from 192.168.1.254: icmp_seq=1 ttl=63 time=0.706 ms --- 192.168.1.254 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 0.706/0.706/0.706/0.000 ms
このときのパケットキャプチャは以下のようになりました。
# RT2 $ sudo ./target/debug/pcap RT2_veth1: d6:c7:d0:99:8e:bc > ff:ff:ff:ff:ff:ff Arp: ARP request(192.168.1.254): d6:c7:d0:99:8e:bc -> 00:00:00:00:00:00 RT2_veth1: f6:63:73:99:cc:cd > d6:c7:d0:99:8e:bc Arp: ARP reply(192.168.1.254): f6:63:73:99:cc:cd -> d6:c7:d0:99:8e:bc RT2_veth1: d6:c7:d0:99:8e:bc > f6:63:73:99:cc:cd Ipv4: 192.168.0.1 -> 192.168.1.254 Icmp: RT2_veth1: f6:63:73:99:cc:cd > d6:c7:d0:99:8e:bc Ipv4: 192.168.1.254 -> 192.168.0.1 Icmp: RT2_veth1: f6:63:73:99:cc:cd > d6:c7:d0:99:8e:bc
最初はまったく通信がないので、ARPによってIPアドレスからMACアドレスの解決を行っています。
その後、192.168.0.1
から192.168.1.254
へのICMP通信が発生し、その応答が返ってきていることがわかります。
TCP通信を発生させたときのパケットキャプチャ
RT1
で以下のように1234
ポートでリッスンしておきます。
# RT1 $ nc -l -p 1234
host
の192.168.0.1
からRT1
の192.168.1.254:1234
へ接続します。
# host $ nc 192.168.1.254 1234
このときのパケットキャプチャは以下のようになりました。
# RT2 RT2_veth1: d6:c7:d0:99:8e:bc > f6:63:73:99:cc:cd Ipv4: 192.168.0.1 -> 192.168.1.254 Tcp: 45693 -> 1234 RT2_veth1: f6:63:73:99:cc:cd > d6:c7:d0:99:8e:bc Ipv4: 192.168.1.254 -> 192.168.0.1 Tcp: 1234 -> 45693 RT2_veth1: d6:c7:d0:99:8e:bc > f6:63:73:99:cc:cd Ipv4: 192.168.0.1 -> 192.168.1.254 Tcp: 45693 -> 1234
正しくキャプチャできていますね。
45693
は送信元なのでhighポートがOSによって割り当てられています。
また、3つ目のパケットから、nc
コマンドでリッスンしていると応答がないのでTCPの再送が行われていることがわかります。
最後に
以上、Rustによるマルチインターフェースパケットキャプチャでした。 本文中でも述べましたがlifetimeですごいハマりました。 少しずつRustを勉強していますが、まだまだですね。今回みたいになにか作っているといろいろハマって理解が深まるので前向きにがんばります。
ご指摘等あればtwitterまでご連絡ください。
References
【GAS】「SpreadsheetApp.create を呼び出す権限がありません」を解決する
表題の通りです。
タイトルのエラーメッセージでググったところ、トリガーを設定し、解決されている方が多いようです。
しかし、当方の要件としては、トリガーで定期的に実行させる必要はなく、エラーメッセージは権限の問題なのでスコープを設定したかったので、appsscript.json
でスコープを設定する方法を残します。
余談ですが、GUIから設定できるのかと思ったので、探してみたのですが、
ファイル
-> プロジェクトのプロパティ
-> スコープ
タブでは、権限が確認できるのみで変更はできなかったので、appsscript.json
に以下を追加しました1。
{ "oauthScopes": ["https://www.googleapis.com/auth/spreadsheets.readonly"], }
なお、参照権限のみで十分であれば、https://www.googleapis.com/auth/spreadsheets.readonly
のようにreadonly
を付けましょう。
References
- [GAS]実行に失敗: その操作を実行する権限がありません。に悩んだこと
- [GAS] 実行する権限がありません。についての対策まとめ
- Manifests - Google Apps Script
- Authorization Scopes - Google Apps Script
-
必要な箇所のみ記載して、前後は省略しています。↩
clasp runがローカルで実行されない
背景
Google Apps Scriptをローカルで開発するために、
Google製のCLIツールclaspを導入しました。
Google Drive上のプロジェクトとclasp push
やclasp pull
で同期できてとても便利なのですが、
以下事象でハマったので記事に残します。
事象
- ローカルでコードを編集
clasp push
するclasp run <function name>
すると、2.でpush
したコードが実行されない
切り分けをしたところ
- ブラウザ上のプロジェクトで見ると
公開 - 実行可能APIとして導入...
が2.で上げたバージョンになっていない
が原因のようでした。
解決方法
--dev
オプションをつければローカルで実行されます。
❯ clasp run --dev <function name>
References
protobufのNon-varintの互換性について
以前、Varint
を64bitから32bitに変更したときの挙動を検証しました。
上記記事の通りVarint
は、下位32-bitを残して切り詰める結果でした。
今回は、Non-varint
の互換性を検証します。
例によって、Language Guideを見てみると
fixed32 is compatible with sfixed32, and fixed64 with sfixed64.
と述べられています。
fixed32
とsfixed32
、fixed64
とsfixed64
のように同じ長さであれば、互換性があるようです。
実際、Protocol Buffers - Encodingのwire typesで以下のように定義されており、32bitと64bitでそもそもType
が異なります。
Type | Meaning | Used For |
---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimited | string, bytes, embedded messages, packed repeated fields |
3 | Start group | groups (deprecated) |
4 | End group | groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
.proto
今回は以下の.proto
を使います。
syntax = "proto3"; package fixed; message User { fixed32 id = 1; }
id
をfixed32
、fixed64
、sfixed32
、sfixed64
の4パターンで変化させて、検証します。
書き出し
書き出し用のコードは以下の通りです。
Id
のvalueはbitがすべて1になる値で検証します(user.Id
フィールドのコメントアウト参照)。
package main import ( "io/ioutil" "log" pb "github.com/cipepser/protobuf-sample/fixed" "github.com/golang/protobuf/proto" "fmt" // "math" // fixed32とfixed64のときのみ利用 ) func main() { user := &pb.User{ //Id: math.MaxUint32, // fixed32のとき //Id: math.MaxUint64, // fixed64のとき Id: -1, // sfixed32とsfixed64のとき } fmt.Printf("%x\n", user.Id) if err := write("./fixed/fixed32.bin", user); err != nil { log.Fatal(err) } } func write(file string, user *pb.User) error { out, err := proto.Marshal(user) if err != nil { return err } fmt.Printf("% x\n", out) if err := ioutil.WriteFile(file, out, 0644); err != nil { return err } return nil }
出力ファイルfixed32.bin
の部分は、fixed32
、fixed64
、sfixed32
、sfixed64
ごとに別ファイルとして出力します。
読み込み
読み込み用のコードです。
package main import ( "fmt" "io/ioutil" "log" pb "github.com/cipepser/protobuf-sample/fixed" "github.com/golang/protobuf/proto" ) func main() { if err := read("./fixed/fixed32.bin"); err != nil { log.Fatal(err) } if err := read("./fixed/fixed64.bin"); err != nil { log.Fatal(err) } if err := read("./fixed/sfixed32.bin"); err != nil { log.Fatal(err) } if err := read("./fixed/sfixed64.bin"); err != nil { log.Fatal(err) } } func read(file string) error { in, err := ioutil.ReadFile(file) if err != nil { return err } user := &pb.User{} if err := proto.Unmarshal(in, user); err != nil { return err } fmt.Printf("%d\n", user.Id) return nil }
読み込みの前に、.proto
のid
をfixed32
、fixed64
、sfixed32
、sfixed64
で変化させて、protoc -I=fixed/ --go_out=fixed/ fixed.proto
を実行しています。
結果
書き出し、読み込みを全パターンで行った結果は以下の通りです。
※表のレイアウトが崩れるため、4294967295(math.MaxUint32)
と18446744073709551615(math.MaxUint64)
をMaxUint32
とMaxUint64
で略記します。
正しく読み込めている(○
)のは、書き出し/読み込みで同じ.proto
を使ったパターンのみでした。
.proto(write時) | 値(write) | .proto(read時) | 値(read) | 結果 |
---|---|---|---|---|
fixed32 | MaxUint32 | fixed32 | MaxUint32 | ○ |
fixed32 | MaxUint32 | fixed64 | 0 | × |
fixed32 | MaxUint32 | sfixed32 | -1 | △ |
fixed32 | MaxUint32 | sfixed64 | 0 | × |
fixed64 | MaxUint64 | fixed32 | 0 | × |
fixed64 | MaxUint64 | fixed64 | MaxUint64 | ○ |
fixed64 | MaxUint64 | sfixed32 | 0 | × |
fixed64 | MaxUint64 | sfixed64 | -1 | △ |
sfixed32 | -1 | fixed32 | MaxUint32 | △ |
sfixed32 | -1 | fixed64 | 0 | × |
sfixed32 | -1 | sfixed32 | -1 | ○ |
sfixed32 | -1 | sfixed64 | 0 | × |
sfixed64 | -1 | fixed32 | 0 | × |
sfixed64 | -1 | fixed64 | MaxUint64 | △ |
sfixed64 | -1 | sfixed32 | 0 | × |
sfixed64 | -1 | sfixed64 | -1 | ○ |
上記結果を見ると、
パースそのものに失敗して、値が0
となった(×
)ものと、
値は読み込めているものの元の値と異なる(△
)もの
があることがわかります。
よくよく見てみると×
は32bitと64bitで長さが異なるもののみです。
fixed32 is compatible with sfixed32, and fixed64 with sfixed64.
の通りですね。
しかし、同じ長さであればcompatibleなはずなのに△
で元の値と変わってしまったものもあります。
もう少し深掘りするために、書き出したバイナリを表に加えてみます。(binary
列)
proto(write時) | 値(write) | binary | proto(read時) | 値(read) | 結果 |
---|---|---|---|---|---|
fixed32 | MaxUint32 | 0d ff ff ff ff | fixed32 | MaxUint32 | ○ |
fixed32 | MaxUint32 | 0d ff ff ff ff | fixed64 | 0 | ×(32bit vs 64bit) |
fixed32 | MaxUint32 | 0d ff ff ff ff | sfixed32 | -1 | △ |
fixed32 | MaxUint32 | 0d ff ff ff ff | sfixed64 | 0 | ×(32bit vs 64bit) |
fixed64 | math.MaxUint64 | 09 ff ff ff ff ff ff ff ff | fixed32 | 0 | ×(32bit vs 64bit) |
fixed64 | math.MaxUint64 | 09 ff ff ff ff ff ff ff ff | fixed64 | math.MaxUint64 | ○ |
fixed64 | math.MaxUint64 | 09 ff ff ff ff ff ff ff ff | sfixed32 | 0 | ×(32bit vs 64bit) |
fixed64 | math.MaxUint64 | 09 ff ff ff ff ff ff ff ff | sfixed64 | -1 | △ |
sfixed32 | -1 | 0d ff ff ff ff | fixed32 | MaxUint32 | △ |
sfixed32 | -1 | 0d ff ff ff ff | fixed64 | 0 | ×(32bit vs 64bit) |
sfixed32 | -1 | 0d ff ff ff ff | sfixed32 | -1 | ○ |
sfixed32 | -1 | 0d ff ff ff ff | sfixed64 | 0 | ×(32bit vs 64bit) |
sfixed64 | -1 | 09 ff ff ff ff ff ff ff ff | fixed32 | 0 | ×(32bit vs 64bit) |
sfixed64 | -1 | 09 ff ff ff ff ff ff ff ff | fixed64 | math.MaxUint64 | △ |
sfixed64 | -1 | 09 ff ff ff ff ff ff ff ff | sfixed32 | 0 | ×(32bit vs 64bit) |
sfixed64 | -1 | 09 ff ff ff ff ff ff ff ff | sfixed64 | -1 | ○ |
まず、追加したbinary
列の先頭の0d
や09
でkeyのfield_number
とwire_type
がわかります。
Protocol Buffers - Encodingに書いてあるとおり、keyは、(field_number << 3) | wire_type
でエンコードされます。
つまり、
0d
は(1 << 3) | 5
なので、type5
(32-bit)のフィールド1
(Id
)であり、
09
は(1 << 3) | 1
なので、type5
(64-bit)のフィールド1
(Id
)です。
まとめ
以上から、×
となった32bitと64bitは0d
と09
でそもそものwire_type
が異なり、パースに失敗し、defalut valueである0
となったようです。
△
となった箇所については、wire_type
が同じ(fixed32
とsfixed32
は0d
、fixed64
とsfixed64
は09
)ため、後続のバイナリをvalueとしてパースします。
パースする際に.proto
が異なるため、書き込んだ値と違う値で解釈されてしまったわけです。
例えば、fixed32
→sfixed32
では、
fixed32
としてMaxUint32(4294967295)
を0xff ff ff ff
としてエンコードします。
しかし、sfixed32
で0xff ff ff ff
をパースすると-1
として解釈されてしまいます。
パースに際してエラーが起きるわけではない2ので、正しく解釈できる範囲でcompatibleです。
上記の例(32bit)では、0x00 00 00 00
〜0x07 ff ff ff
です。
これを超えると、正数と2の補数で表現された負数が区別できないため、signed/unsignedで結果が異なります。
References
protobufで0 byteをデコードする
Language Guideに以下のようにフィールドに値がセットされていない場合はdefault valueが使われることが書かれています。
When a message is parsed, if the encoded message does not contain a particular singular element, the corresponding field in the parsed object is set to the default value for that field.
ということは何も書かれていない(0 byteの)バリナリをパースしようとすると、すべてのフィールドがdefault valueとなるはずなので試してみました。
パースする.proto
以下のような.proto
を用意します。
syntax = "proto3"; package blank; message User { int32 id = 1; string name = 2; fixed64 age = 3; Contact contact = 4; } message Contact { string phone = 1; string email = 2; }
protocします。
❯ protoc -I=blank/ --go_out=blank/ blank.proto
パースする
0 byteの入力としてin := []byte{}
を使います。
in
をUser
にパースし、各フィールドを標準出力して、default valueになっているか確認します。
package main import ( "fmt" "log" pb "github.com/cipepser/protobuf-sample/blank" "github.com/golang/protobuf/proto" ) func main() { user := &pb.User{} in := []byte{} if err := proto.Unmarshal(in, user); err != nil { log.Fatal(err) } fmt.Println("ID: ", user.Id) fmt.Println("Name: ", user.Name) fmt.Println("Age: ", user.Age) fmt.Println("Contact: ", user.Contact) }
結果
実行結果は以下の通りです。
❯ go run BlankRead.go ID: 0 Name: Age: 0 Contact: <nil>
ちゃんとdefault valueでパースできました。
ちなみに、上記の通りuser.Contact
はnil
がdefault valueのため、Phone
やEmail
にアクセスしようとするとpanicになります。
fmt.Println("Phone: ", user.Contact.Phone) // panic: runtime error: invalid memory address or nil pointer dereference
References
Golangで出力したprotobufバイナリをRustで読み込む
ここ三回くらいprotobufの記事を書いてきましたが、Goばかりだったので、Rustで読み込んでみました。
事前準備:Goでバイナリを出力する
user.proto
を以下のように定義します。
syntax = "proto3"; package user; message User { string name = 1; int32 age = 2; }
Go用にprotocする
❯ protoc -I=./ --go_out=./ user.proto
user.pb.go
が生成されます。
バイナリを書き出す
以下のユーザをprotobufのバイナリとしてgo_user.bin
を書き出します。
フィールド | 値 |
---|---|
Name | Alice |
Age | 20 |
出力用のコードは以下の通りです。
package main import ( "fmt" "io/ioutil" "log" pb "github.com/cipepser/protobuf-sample/rust-protobuf-example/user" "github.com/golang/protobuf/proto" ) func main() { p := &pb.User{ Name: "Alice", Age: 20, } out, err := proto.Marshal(p) if err != nil { log.Fatal(err) } fmt.Println(out) if err := ioutil.WriteFile("./go_user.bin", out, 0644); err != nil { log.Fatalln("Failed to write:", err) } }
Rustで読み込む
いよいよ本題のRustです。
Cargo.toml
の設定
Rustでprotobufを扱うためにrust-protobufを使います。
Cargo.toml
のdependencies
にprotobuf
を記載します。
READMEにも書いてありますが、bytes
クレートを使っているのでbytes
も記載します。
[dependencies] protobuf = { version = "~2.0", features = ["with-bytes"] }
2019/08/03追記
stepancheg/rust-protobuf: Rust implementation of Google protocol buffersに、version2未満がサポート対象外になったと書いてあるので、追記現在では以下のように記載します。
[dependencies] protobuf = { version = "2", features = ["with-bytes"] }
Rust用にprotocする
user.proto
は、Goでprotoc
したときと同じものです。
❯ protoc --rust_out src/ user.proto
以下のようなメッセージが出る場合は、protoc-gen-rust
がないのでインストールしましょう。
protoc-gen-rust: program not found or is not executable --rust_out: protoc-gen-rust: Plugin failed with status code 1
以下でインストール出来ます(Cargo.toml
に書いてもいいのかも)。
❯ cargo install protobuf-codegen
protoc
がうまくいけば、src/user.rs
がgenerateされます。
読み込む
ファイル読み込み、User
型にmerge
、標準出力するまでのコードは以下のとおりです。
extern crate protobuf; mod user; use user::User; use std::fs::File; use std::io::{BufReader}; use protobuf::{CodedInputStream, Message}; fn main() { let file = File::open("./go_user.bin").expect("fail to open file"); let mut buffered_reader = BufReader::new(file); let mut cis = CodedInputStream::from_buffered_reader(&mut buffered_reader); let mut u = User::new(); u.merge_from(&mut cis).expect("fail to merge"); println!("Name: {}", u.get_name()); println!("Age: {}", u.get_age()); }
実行すると、以下のようにName
とAge
が読み取れていることがわかります。
/usr/local/bin/cargo run --color=always --package rust-protobuf-example --bin rust-protobuf-example Finished dev [unoptimized + debuginfo] target(s) in 0.04s Running `target/debug/rust-protobuf-example` Name: Alice Age: 20 Process finished with exit code 0
References
protobufのVarintを64bitから32bitに変更したときの挙動
protobufのVarint
はProtocol Buffers - Encodingで定義されるように、32-bit(int32
, uint32
, sint32
)や64-bit(int64
, uint64
, sint64
)のどちらも含まれています。
Varint
のエンコーディングは、その値によってbyte長が変わりますが(負数は-1
のような小さい数でも最上位bitが1
になりエンコードした結果長くなってしまうので非推奨となっている)、64-bitでエンコードされた値を32-bitで読み込んだ場合、どのような動作になるのか気になったので試してみました。
なお、Varint
には、bool
やenum
も含まれていますが、
その変換については以下の記事で検証したのため、興味があればそちらをご覧ください。
結論から言うと仕様に、以下のように書いてあるので、処理系の実装依存と思われますが、Goで実際に試してみたメモとして残します。
If a number is parsed from the wire which doesn't fit in the corresponding type, you will get the same effect as if you had cast the number to that type in C++ (e.g. if a 64-bit number is read as an int32, it will be truncated to 32 bits).
環境
❯ go version
go version go1.10.3 darwin/amd64
❯ protoc --version
libprotoc 3.6.0
検証(uint64
-> uint32
)
uint64
でmath.MaxUint32
よりも大きくなる値をuint32
で読み込んでみて動作を検証します。
(最後にint
も検証結果も記載しますが、コードも含めてすべて記載すると長いのでuint
のみとします)
今回の例では、読み込む値をUser
がもつId
とし、以下のように.proto
を定義します。
syntax = "proto3"; package max; message User { uint64 id = 1; }
書き出し
次に書き出し用のコードです。
math.MaxUint32
よりも大きくなる値を0b1111111111111111111111111111111100000000000000000000000000000000
とします。
これは前半32-bitが1
で後半32-bitが0
となる値で、math.MaxUint64 - math.MaxUint32
で生成します。この数字を使うことでuint32
で読み込んだときに後半32-bitだけに切り詰められる(0
になる)のか、math.MaxUint32
より大きくオーバーフローしてしまうのか確認します。
// MaxWrite.go package main import ( "io/ioutil" "log" "math" pb "github.com/cipepser/protobuf-sample/max" "github.com/golang/protobuf/proto" ) func main() { user := &pb.User{ Id: math.MaxUint64 - math.MaxUint32, } if err := write("./max/uint.bin", user); err != nil { log.Fatal(err) } } func write(file string, user *pb.User) error { out, err := proto.Marshal(user) if err != nil { return err } if err := ioutil.WriteFile(file, out, 0644); err != nil { return err } return nil }
読み込み
読み込み用のコードです。
// MaxRead.go package main import ( "fmt" "io/ioutil" "log" pb "github.com/cipepser/protobuf-sample/max" "github.com/golang/protobuf/proto" ) func main() { if err := read("./max/uint.bin"); err != nil { log.Fatal(err) } } func read(file string) error { in, err := ioutil.ReadFile(file) if err != nil { return err } user := &pb.User{} if err := proto.Unmarshal(in, user); err != nil { return err } fmt.Printf("0d%d\n0b%b\n", user.Id, user.Id) return nil }
10進数(0d
)と2進数(0b
)でuser.Id
を標準出力します。
試しに実行してみると、以下のように正常にデコードできていることがわかります。
❯ go run MaxRead.go 0d18446744069414584320 0b1111111111111111111111111111111100000000000000000000000000000000
.proto
をuint32
に変更する
では、uint32
に変更して、上記で出力したuint64
のバイナリを読み込みましょう。
syntax = "proto3"; package max; message User { uint32 id = 1; }
protoc
してから読み込みます。
❯ go run MaxRead.go 0d0 0b0
0
になったので、後半32-bitだけに切り詰められたようです。
結果
同様の検証をint64
-> int32
、sint64
-> sint32
でもやってみた結果も含めて、まとめると以下のようになりました。
検証パターン(.proto) | 読み込む値 | 64-bitのまま読み込み | 32-bitにして読み込み |
---|---|---|---|
uint64 -> uint32 |
math.MaxUint64 - math.MaxUint32 | 0d18446744069414584320 0b1111111111111111111111111111111100000000000000000000000000000000 |
0d0 0b0 |
int64 -> int32 |
math.MaxInt64 - math.MaxInt32 - 1 <<31 | 0d9223372032559808512 0b111111111111111111111111111111100000000000000000000000000000000 |
0d0 0b0 |
結果、uint
でもint
でも同様に後半32-bitに切り詰められる結果となりました。
余談
int
のほうで1 << 31
を引いているのですが、最初、math.MaxInt64 - math.MaxInt32
としていました。
符号付き整数なので、これだと下位31-bitしか0
にできず、この値をint32
として読み込むと以下のようになってしまいます。
0d-2147483648 0b-10000000000000000000000000000000
検証としては、下位32-bitを残して切り詰める結果から相違ないので、参考としてこの結果も載せておきます。
ちなみにmath.MinInt64 - math.MinInt32
を読み書きしても上記と同じ結果になります(2進数で見たときに同じ値になるので)。