LoginSignup
4
5

More than 5 years have passed since last update.

grpcでPythonからC++の関数を呼ぶ

Posted at

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を実装する。手順は以下の通り

  1. rpcを定義する.protoファイルを作る。
  2. (C++ server) protocコマンドでrpc定義の.protoファイルから、C++のファイルを作る(Serviceクラスが作られる)。
  3. (C++ server) 先ほど作られたServiceクラスを継承して、実際にserver側で行われる処理を実装する。
  4. (Python client) grpc-tools で.protoファイルからPythonのファイルを作る。Stubクラスが作られる。
  5. (Python client) 先ほど作られたstubを使って、クライアントの処理を実装する。

以下、次のようなディレクトリ構造を仮定する

(root) /
    proto /  (.protoファイルを置くためのディレクトリ)
    server /   (C++で書かれたserverのソースを置くためのディレクトリ)
    client /    (Pythonで書かれたclientのソースを置くディレクトリ)

RPCを定義する.protoファイルを作る

ここではランダムウォークをシミュレーションするserverを作ることにする。確率1/2で位置が+1または-1に移動する。
protoディレクトリにて、以下のようなprotoファイルを作る。

random_walker.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(...)も使うことになる)

random_walker.grpc.pb.h
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クラスを継承して、サーバーの処理を実装する

最小限のコードは以下の通りになる。

server.cc
#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

クライアントの実装

最小限のクライアント実装は以下のようになる。

client.py
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++アプリを制御できる。

参考

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5