C++で書かれた関数をPythonから呼べるようにする。
動機としては、C++で書かれた既存のシミュレーションコードがあって、そのシミュレーションをしながら対話的に可視化したり、状態を出力したりしたい。つまりデバッガのように対話的に動かせるようにしたい。
swigなどを使ってC++の関数を呼べるようにしても良いが、MPIで書かれたコードのように特殊な実行方法が必要なこともあるので、C++アプリ自体は単独で起動できるようにRPC経由で呼ぶように実装する。
C++(simulation code) -- C++(grpc server) -- Python (grpc client)
というように連携させる。
grpcとは
Remote Procedure Callを独自のDSLで定義し、サーバーとクライアントを簡単に実装するためのツール。
protocol buffersを通信のプロトコルに利用している。
公式のドキュメントがとてもわかりやすい。
https://grpc.io/docs/
非同期I/O、streaming通信などもあるが、ここではもっともシンプルなRPCのみ定義する。
インストール
公式ではmake && make install
する方法が書かれているが、macの場合はbrewで簡単にインストールできる。
brew install protobuf grpc
でインストールできる。これでC++についてはOK。
さらにpythonのモジュールもインストールする。
ここではpipenvを使ってインストールしたが、どのような方法でインストールしてもよい。
pipenv install grpcio grpcio-tools googleapis-common-protos
以後、pipenv shell
したシェルで実行する。
RPCの実装手順
ここではC++のserverとPythonのclientを実装する。手順は以下の通り
- rpcを定義する.protoファイルを作る。
- (C++ server) protocコマンドでrpc定義の.protoファイルから、C++のファイルを作る(Serviceクラスが作られる)。
- (C++ server) 先ほど作られたServiceクラスを継承して、実際にserver側で行われる処理を実装する。
- (Python client) grpc-tools で.protoファイルからPythonのファイルを作る。Stubクラスが作られる。
- (Python client) 先ほど作られたstubを使って、クライアントの処理を実装する。
以下、次のようなディレクトリ構造を仮定する
(root) /
proto / (.protoファイルを置くためのディレクトリ)
server / (C++で書かれたserverのソースを置くためのディレクトリ)
client / (Pythonで書かれたclientのソースを置くディレクトリ)
RPCを定義する.protoファイルを作る
ここではランダムウォークをシミュレーションするserverを作ることにする。確率1/2で位置が+1または-1に移動する。
protoディレクトリにて、以下のようなprotoファイルを作る。
syntax = "proto3";
package random_walker;
service MyRandomWalker {
rpc Update (NumSteps) returns (Empty) {}
rpc GetPosition (Empty) returns (Position) {}
}
message Empty {
}
message NumSteps {
int32 n = 1;
}
message Position {
int32 x = 1;
}
-
rpc
というキーワードでrpc定義を行う。各RPCの引数と返り値は何かというのを定義する。 - 引数や返り値のデータ構造を
message
キーワードで定義する。(これはgrpcではなくprotocol buffers自体の仕様)- 引数や返り値がなくても何らかのデータを設定する必要がある。ここでは
Empty
という空のデータ構造を作っている。
- 引数や返り値がなくても何らかのデータを設定する必要がある。ここでは
protoからC++のファイルを作成する
serverディレクトリで以下のコマンドを実行する。
$ protoc -I ../proto --cpp_out=. ../proto/random_walker.proto
$ protoc -I ../proto --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` ../proto/random_walker.proto
最初のコマンドでmessage
キーワードが解釈され、データ構造に対応するC++のクラスが生成される。random_walker.pb.(h/cc) という名前のファイルが作られる。
中を見てみると次のようなクラスが作られていることがわかる。
namespace random_walker {
class Empty { ... };
class NumSteps { ... };
class Position { ... };
}
2つ目のコマンドでrpc
キーワードが解釈され、RPCのserver/clientのテンプレートとなるC++のクラスが生成される。random_walker.grpc.pb.(h/cc)というファイルが作られる。
その中では以下のようなService
というクラスが定義されている。その中ではUpdate
, GetPosition
という先ほどRPCとして定義したものが、純粋仮想関数として宣言されている。
他にも色々なクラスが宣言されているが、今回の用途ではService
のみ使えばよい。
(もしclientも作りたい場合はstatic std::unique_ptr<Stub> NewStub(...)
も使うことになる)
namespace random_walker {
class Service : public ::grpc::Service {
public:
Service();
virtual ~Service();
virtual ::grpc::Status Update(::grpc::ServerContext* context, const ::random_walker::NumSteps* request, ::random_walker::Empty* response);
virtual ::grpc::Status GetPosition(::grpc::ServerContext* context, const ::random_walker::Empty* request, ::random_walker::Position* response);
};
Serviceクラスを継承して、サーバーの処理を実装する
最小限のコードは以下の通りになる。
#include <iostream>
#include <cstdlib>
#include <string>
#include <grpc/grpc.h>
#include <grpcpp/server.h>
#include <grpcpp/server_builder.h>
#include <grpcpp/server_context.h>
#include <grpcpp/security/server_credentials.h>
#include "random_walker.grpc.pb.h"
class RandomWalkerImpl : public random_walker::MyRandomWalker::Service {
public:
RandomWalkerImpl() : random_walker::MyRandomWalker::Service() { pos = 0; }
::grpc::Status Update(::grpc::ServerContext* context, const ::random_walker::NumSteps* request, ::random_walker::Empty* response) {
int32_t n = request->n();
for(int32_t i=0; i<n; i++) {
if( rand() % 2 == 0 ) { pos += 1; }
else { pos -= 1; }
}
return grpc::Status::OK;
}
::grpc::Status GetPosition(::grpc::ServerContext* context, const ::random_walker::Empty* request, ::random_walker::Position* response) {
response->set_x(pos);
return grpc::Status::OK;
}
private:
int pos;
};
int main(int argc, char** argv) {
std::string server_address("0.0.0.0:50051");
RandomWalkerImpl service;
grpc::ServerBuilder builder;
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
std::cout << "Server listening on " << server_address << std::endl;
server->Wait();
return 0;
}
- Serverの処理の実装を
RandomWalkerImpl
クラスで定義。このクラスは先ほど生成されたService
クラスを継承している。 - RandomWalkerImplクラスの仮想関数を定義。ここでは
Update
,GetPosition
の2つを返す。-
request
にクライアントからのリクエストが入っている。response
にクライアントに返すレスポンスをセットする。 - 返り値は
grpc::Status::OK
-
- main関数でserverを起動する。
- この実装はほぼ定型句。そのままコピーすれば良い。
コンパイルコマンドは以下の通り。
$ g++ -std=c++11 `pkg-config --cflags protobuf grpc` -c -o random_walker.pb.o random_walker.pb.cc
$ g++ -std=c++11 `pkg-config --cflags protobuf grpc` -c -o random_walker.grpc.pb.o random_walker.grpc.pb.cc
$ g++ -std=c++11 `pkg-config --cflags protobuf grpc` -c -o server.o server.cc
$ g++ random_walker.grpc.pb.o random_walker.pb.o server.o -L/usr/local/lib `pkg-config --libs protobuf grpc++` -lgrpc++_reflection -ldl -o server
これでビルドすればserver側の実装は完了。
.protoファイルからPythonのファイルのクライアントのコードを生成する
$ python -m grpc_tools.protoc -I../proto --python_out=. --grpc_python_out=. ../proto/random_walker.proto
これを実行すると"random_walker_pb2.py"と"random_walker_pb2_grpc.py"の2つのファイルが作られる。
前者はprotoファイル内のmessage
で宣言されたクラス、後者はrpc
で宣言されたクラスが作られる。
この中でclientを実装するために使うクラスはMyRandomWalkerStub
クライアントの実装
最小限のクライアント実装は以下のようになる。
import grpc
import random_walker_pb2
import random_walker_pb2_grpc
def run():
with grpc.insecure_channel('localhost:50051') as channel:
stub = random_walker_pb2_grpc.MyRandomWalkerStub(channel)
stub.Update( random_walker_pb2.NumSteps(n=5) )
res = stub.GetPosition(random_walker_pb2.Empty())
print(res.x)
if __name__ == '__main__':
run()
MyRandomWalkerStub
クラスのインスタンスを初期化する。stubインスタンスはGetPosition
, Update
などのrpcで定義したメソッドを持っている。
あとは通常のメソッドコールのように使える。
これを使えばPythonのREPLから対話的にC++アプリを制御できる。