Protocol Buffer (proto3)を使ったC++とPython間通信
Protocol Buffer (proto3)と使ったC++プログラムとPythonスクリプト間のデータのやり取り
Protocol Buffer
Protocol BufferはGoogleが開発したプラットフォーム非依存、拡張性の高いシリアライズデータ構造です。データをバイナリで扱うため、XMLやjsonのようなテキストベースのデータ構造よりも効率が高く、Pythonのstruct.pack/unpack
よりも拡張性に優れているのが特徴です。
テスト環境
- ホスト … Windows 10 64bit, Msys2
- Python … Python 3.8.2
- C++ … clang version 9.0.1
- Protocol Buffer … Protobuf 3.11.4
コンパイラインストール
Protocol Bufferはメッセージを定義した.proto
ファイルを各種言語、プラットフォーム向けにコンパイルして使用します。Msys2環境ではpacman
でコンパイラのインストールが可能です
% pacman -S mingw-w64-x86_64-protobuf mingw-w64-x86_64-python-protobuf % protoc --version libprotoc 3.11.4
メッセージ定義
今回はRaspberry Pi Zeroとの間で温度計データを送受信することを想定して、以下のようなメッセージを定義しました。データ型としては、一般的な整数、浮動小数点のほか、ブールや文字列、列挙型やサブメッセージなどが使用できます。詳細な仕様は公式ページを参照してください
syntax = "proto3"; message SensorData { string name = 1; // Sensor Name uint64 uid = 2; // Sensor Unique ID enum SensorType { Unknown = 0; Thermometer = 1; Humidity = 2; } SensorType type = 3; // Sensor Type uint64 timestamp_us = 4; // POSIX Timestamp in us int32 index = 5; // Data index repeated double value = 6; }
コンパイル
定義したメッセージファイル (proto/sensor_data.proto
)をprotoc
でコンパイルして、C++とPython用のメッセージファイルを生成します
% protoc -I proto --cpp_out=generated --python_out=generated proto/sensor_data.proto % ls genereted sensor_data.pb.cc sensor_data.pb.h sensor_data_pb2.py
メッセージの作成
Python
PythonではSerializeToString()
でメッセージをバイト列に変換します (Stringとありますが、文字列ではなくバイトデータです)。今回は簡易テストのためファイルに書き出していますが、そのままUDPデータとして送信することも当然可能です
import os import sys import time sys.path.append(os.path.join(os.path.dirname(__file__), "..", "generated")) import sensor_data_pb2 def main(): # データ書き出しファイル fo = open('python_sensor.data', 'wb') # バイナリ形式でオープン # ダミーセンサデータ作成 dummy_data = sensor_data_pb2.SensorData() dummy_data.name = "Dummy PyThermo" dummy_data.uid = 2 dummy_data.type = sensor_data_pb2.SensorData.SensorType.Thermometer dummy_data.timestamp_us = int(time.time_ns() / 1000) dummy_data.index = 1 dummy_data.value.extend([23.2, 23.3, 23.5]) fo.write(dummy_data.SerializeToString()) fo.close() if __name__ == "__main__": main()
import sensor_data_pb2
で、protoc
により生成されたsensor_data_pb2.py
をインポートしています。メッセージのメンバにはdata.<member_name>
のようにアクセスでき、repeated
メンバについてはlist
型と同じようにappend()
やextend()
でデータの追加が行えます
C++
C++の場合は生成されたsensor_data_pb2.h
に定義されたset_<member_name>()
というメンバ関数でデータを設定します。repeated
メンバへのデータ追加はadd_<member_name>()
関数です。シリアライズはSerializeToOstream()
関数で行います
#include <cstdint> #include <ctime> #include <fstream> #include <google/protobuf/util/json_util.h> #include "../generated/sensor_data.pb.h" using namespace std; int main() { // データ書き出しファイル fstream output("cpp_sensor.data", ios::out | ios::binary | ios::trunc); // ダミーセンサデータ作成 SensorData dummy_data; dummy_data.set_name("Dummy CppThermo"); dummy_data.set_uid(3); dummy_data.set_type(SensorData_SensorType_Thermometer); dummy_data.set_timestamp_us(static_cast<uint64_t>(time(nullptr))); dummy_data.set_index(1); dummy_data.add_value(15.1); dummy_data.add_value(14.2); dummy_data.SerializeToOstream(&output); output.close(); return 0; }
コンパイルにはprotoc
で生成したsensor_data.pb.cc
とともに-lprotobuf
オプションを追加してコンパイルします
% clang++ cpp/protobuf_example_wr.cc generated/sensor_data.pb.cc -lprotobuf -o protobuf_example_wr
メッセージの読み込み
続いて先ほど書き出したファイルをPythonでC++で作成したcpp_sensor.data
を、C++でPythonで作成したpython_sensor.data
を読み込みます
Python
読み出しはParseFromString()
です
import os from pprint import pprint import sys sys.path.append(os.path.join(os.path.dirname(__file__), "..", "generated")) import sensor_data_pb2 def main(): # データ読み出し cpp_data = sensor_data_pb2.SensorData() fi = open('cpp_sensor.data', 'rb') # バイナリ形式でオープン cpp_data.ParseFromString(fi.read()) pprint(cpp_data) fi.close() if __name__ == "__main__": main()
% python protobuf_example_rd.py name: "Dummy CppThermo" uid: 3 type: Thermometer timestamp_us: 1582651238 index: 1 value: 15.1 value: 14.2
C++
#include <fstream> #include <iostream> #include <google/protobuf/util/json_util.h> #include "../generated/sensor_data.pb.h" using namespace std; int main() { // データ読み出し fstream input("python_sensor.data", ios::in | ios::binary); SensorData python_data; python_data.ParseFromIstream(&input); string s; google::protobuf::util::MessageToJsonString(python_data, &s); cout << s << endl; return 0; }
% clang++ cpp/protobuf_example_rd.cc generated/sensor_data.pb.cc -lprotobuf -o protobuf_example_rd % ./protobuf_example_rd {"name":"Dummy PyThermo","uid":"2","type":"Thermometer","timestampUs":"1586255419234748","index":1,"value":[23.2,23.3,23.5]}
1件の返信
[…] 前回紹介したProtocol Bufferを用いてRaspberry Pi Zeroと会話する方法 […]