はじめに、やりたかったこと
Python <-> C言語 間でのプロセス間通信
諸般の事情により、C言語でプログラムを書くことになりましたが、インタフェース部分も同じCで書くのは非常に辛いなぁっと思ったので、インタフェースはPythonで作り、IPC等々でプログラム同士を結ぶことを考えました。
はじめは独自に頑張ってC言語の構造体をpack/unpackしてもいいかなと思ってましたがせっかく先人たちが作ったものがあるのでProtocol Buffer使えるんじゃね?っということで色々調べて、試してみることにしました。
Protocol Buffer(protobuf)
公式のProtocol bufferのサポートは以下の通りです。(2018/11/11現在)
proto2
- Java
- Python
- Objective-C
- C++
proto3(上記+)
proto2とproto3には互換性はありません。proto2とproto3、protobufの概要等を詳しく説明している記事がQiitaにもありましたのでぜひ読んでみてください。
「Proto2 vs Proto3」:https://qiita.com/ksato9700/items/0eb025b1e2521c1cab79
Protobufのサンプルコード
protoファイルを生成
message DataStructs{
optional uint32 id = 1;
optional uint32 ip_address = 2;
optional uint32 port_num = 3;
}
message IpcMessage {
enum Errors {
SUCCESS = 200;
ERROR_BAD_REQUEST = 400;
ERROR_NOT_FOUND = 404;
ERROR_SERVER_ERROR = 500;
ERROR_SERVICE_UNAVAILABLE = 503;
}
// error_codeは上記のErrorsを参照しています。
optional Errors error_code = 1;
// messageをネストできます。
optional DataStructs data = 2;
}
上記はproto2の記述です。初めて見る人でもなんとなく理解ができると思います。
.proto
ファイルのなかではコメントなどもかけてので便利です。
proto3はSyntaxが異なるため互換性がありません。
protobuf-c
で、今回はC言語でprotobufを動かしたいというのがことの始まりです。
公式ではサポートがないのですが、調べてみるとサードパーティのプロジェクトがあるようですのでそれを利用します。
protobuf-c : https://github.com/protobuf-c/protobuf-c
protobuf-cはproto3には未対応ということのようですので、以後はproto2で試してみます。
protobuf-cのインストール
コンパイルするしかないかと思ったら、最近のディストリビューションにはパッケージがあるようです。
CentOSでもUbuntuでも標準のリポジトリからインストールできます。
$ yum search protobuf-c | grep x86_64
protobuf-c.x86_64 : C bindings for Google's Protocol Buffers
protobuf-c-compiler.x86_64 : Protocol Buffers C compiler
protobuf-c-devel.x86_64 : Protocol Buffers C headers and libraries
...
$ apt-cache search protobuf-c | grep '(protobuf-c)'
libprotobuf-c1 - Protocol Buffers C shared library (protobuf-c)
libprotobuf-c-dev - Protocol Buffers C static library and headers (protobuf-c)
libprotobuf-c1-dbg - Protocol Buffers C shared library debug symbols (protobuf-c)
protobuf-c-compiler - Protocol Buffers C compiler (protobuf-c)
今回はCentOS環境にて実施します。
$ cat /etc/redhat-release && uname -r && rpm -aq | egrep '(protobuf)|(gcc)' && python -V
CentOS Linux release 7.5.1804 (Core)
3.10.0-862.14.4.el7.x86_64
gcc-4.8.5-28.el7_5.1.x86_64
protobuf-2.5.0-8.el7.x86_64
protobuf-c-devel-1.0.2-3.el7.x86_64
protobuf-c-1.0.2-3.el7.x86_64
protobuf-compiler-2.5.0-8.el7.x86_64
libgcc-4.8.5-28.el7_5.1.x86_64
protobuf-python-2.5.0-8.el7.x86_64
protobuf-c-compiler-1.0.2-3.el7.x86_64
Python 2.7.5
protoファイルのprotoc-cによるコンパイル
.proto
ファイルをprotobuf-c用のprotobufコンパイラであるprotoc-c
にてコンパイルし、.pb-c.h
と.pb-c.c
を生成し、以後利用します。
$ protoc-c sample.proto --c_out=./ && ls sample.*
sample.pb-c.c sample.pb-c.h sample.proto
Python -> C言語
C言語でprotobufを読み込む例です
PythonのSerializeスクリプト
C言語でデシリアライズするプログラムを書く前に、Pythonで標準出力にシリアライズするスクリプトを用意します。
まず、ProtoファイルをPython用に作成します。
$ protoc sample.proto --python_out=. ; ls *.py
sample_pb2.py
標準出力にpythonでシリアライズされたprotobufを表示するスクリプトです。今回はprotobuf-cの話なので詳しくは割愛。
#!/usr/bin/python
# -*- encoding:utf-8 -*-
import sample_pb2
import sys
message = sample_pb2.IpcMessage()
message.error_code = sample_pb2.IpcMessage.ERROR_NOT_FOUND
message.data.id=123
message.data.ip_address=(192<<24)+(168<<16)+(0<<8)+5
message.data.port_num=5060
data=message.SerializeToString()
# 改行コードを含まないように出力
sys.stdout.write(data)
C言語でのデシリアライズ
c言語では標準入力からデシリアライズさせてみます。
#include <stdio.h>
#include "sample.pb-c.h"
int main(){
char buffer[1024];
int len=0;
FILE *fp;
// 標準入力からBinaryモードで入力
fp=freopen(NULL, "rb", stdin);
len=fread(buffer, sizeof(char), sizeof(buffer), fp);
// Protoファイルで定義した定義に従う
IpcMessage *message;
// unpackする際にはシリアライズされたデータの正確な長さが必要です。短すぎても長すぎてもNG
message=ipc_message__unpack(NULL, len, buffer);
// has_*でoptionalな項目に値があるかを確認します。
if(message->has_error_code)
printf("error_code : %d\n", message->error_code);
// ネストされたmessageについてもpackした際に動的に生成されます。
if(message->data->has_id)
printf("data.id : %d\n", message->data->id);
if(message->data->has_ip_address)
printf("data.ip_address : %d.%d.%d.%d\n", (message->data->ip_address)>>24 & 0xff,
(message->data->ip_address)>>16 & 0xff,
(message->data->ip_address)>>8 & 0xff,
(message->data->ip_address) & 0xff);
if(message->data->has_port_num)
printf("data.port_num : %d\n", message->data->port_num);
// unpackしたオブジェクトの解放
// ネストされたmessageについても解放されるようです
ipc_message__free_unpacked(message, NULL);
close(fp);
return 0;
}
コンパイルします
$ gcc -l protobuf-c deserialize_sample.c sample.pb-c.c -o deserialize_sample
実行
$ ./serialize_sample.py | ./deserialize_sample
error_code : 404
data.id : 123
data.ip_address : 192.168.0.5
data.port_num : 5060
Pythonでシリアライズしたprotobufのメッセージを無事C言語で取り出すことができました。
C言語 -> Python
次にC言語で生成したProtobufをPythonで読み込む例
C言語でのシリアライズ
#include <stdio.h>
#include <stdlib.h>
#include "sample.pb-c.h"
int main(){
void *buffer;
int len=0;
FILE *fp;
// 標準入力からBinaryモードで入力。
fp=freopen(NULL, "wb", stdout);
// INITマクロを使う例もありますが、ここではmallocによる動的確保
// malloc後に__initを使用して初期化する必要があり
IpcMessage *message;
message=(IpcMessage *)malloc(sizeof(IpcMessage));
ipc_message__init(message);
// pack時とは異なり、initをしてもネストされたmessageの領域までは確保されない
// 別途ネストされたmessageについて領域の確保とinitでの。初期化が必要
message->data=(DataStructs *)malloc(sizeof(DataStructs));
data_structs__init(message->data);
// .protoファイルでoptionalに指定している要素はhas_*のフラグをtrueに。
// trueにしないとシリアラズする際に無視される
message->has_error_code=1;
message->error_code=IPC_MESSAGE__ERRORS__ERROR_SERVICE_UNAVAILABLE;//503
message->data->has_id=1;
message->data->id=1192;
message->data->has_ip_address=1;
message->data->ip_address=(192<<24)+(168<<16)+(0<<8)+234;
message->data->has_port_num=1;
message->data->port_num=8080;
// シリアライズ処理、サイズを取得してmalloc&シリアライズ処理
len=ipc_message__get_packed_size(message);
buffer=malloc(len);
ipc_message__pack(message, buffer);
// 標準出力にバイナリで出力
fwrite(buffer, sizeof(void), len, fp);
// mallocした領域の解放
free(buffer);
free(message->data);
free(message);
close(fp);
return 0;
}
コンパイルします
$ gcc -l protobuf-c serialize_sample.c sample.pb-c.c -o serialize_sample
PythonのDeserializeスクリプト
標準入力のprotobufでシリアライズされた入力をデシリアライズして表示するスクリプトです。
#!/usr/bin/python
# -*- encoding:utf-8 -*-
import sample_pb2
import sys
data = sys.stdin.read()
message = sample_pb2.IpcMessage()
message.ParseFromString(data)
if message.HasField("error_code"):
print("error_code : {}".format(message.error_code))
if message.data.HasField("id"):
print("data.id : {}".format(message.data.id))
if message.data.HasField("ip_address"):
print("data.ip_address : {}.{}.{}.{}".format((message.data.ip_address>>24)&0xff,
(message.data.ip_address>>16)&0xff,
(message.data.ip_address>> 8)&0xff,
(message.data.ip_address>> 0)&0xff))
if message.data.HasField("port_num"):
print("data.port_num : {}".format(message.data.port_num))
実行
$ ./serialize_sample | ./deserialize_sample.py
error_code : 503
data.id : 1192
data.ip_address : 192.168.0.234
data.port_num : 8080
C言語でシリアライズしたprotobufのメッセージを無事Pythonで取り出すことができました。
あとがきと所感
protobuf-c
の日本語での情報があまりなかったので簡単ではありますが、シリアライズの方法、デシリアライズの方法をまとめました。
エラー処理などはあまり意識していないサンプルコードですし、実際にはIPCで実装する場合は単純な標準入出力ではなくなるのでsocket等での組み直しが必要ですがエッセンスについては理解できるのではないかなと思います・・・。
今まではprotocol buffer
のありがたみもよくわかったおらず、「jsonでdumpすればいいやん」としか思ってなかったのが正直なところでした。
たしかにREST/APIなど、ある程度人が理解しやすい形で定義する必要がある場合はjsonやyamlで定義するというのも良いのでしょうけど、異なるプロセス間、異なるプログラミング言語間などのデータフォーマットをどうするのかという問題に対する解としては非常に有用に感じました。
さらにC言語のような低水準(最近の言語と比較して)な言語であれば、jsonやyaml何かを標準では対応しておらず、またその計算速度も極力無駄にしない有り難い仕組みだなぁっと理解いたしました。