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]}

おすすめ