[Rust] gRPC in Rustしてみる その1

gRPCしたくなってきたので、Rustで試してみることにしました

# 環境

- Rust: 1.36
- protoc: 3.7.1
- ライブラリ
  - 今回は [pingcap/grpc-rs](https://github.com/pingcap/grpc-rs) を使うことにしました。
    - ( [stepancheg/grpc-rust](https://github.com/stepancheg/grpc-rust)を使わなかったのは、最新バージョンが2018年だったからです。 )
  - protoファイルのコンパイルは [grpc-rs/compiler at master · pingcap/grpc-rs](https://github.com/pingcap/grpc-rs/tree/master/compiler)

[Rustを使ったふつうのコマンドラインツール開発 - Qiita](https://qiita.com/hhatto/items/19dfa1af6cabbee571d1#grpc-in-rust)
参考にさせて頂きました :bow:

# `.proto` ファイルを書いてみる

[Language Guide (proto3)  |  Protocol Buffers  |  Google Developers](https://developers.google.com/protocol-buffers/docs/proto3)
[Proto3 Language Guide（和訳） - Qiita](https://qiita.com/CyLomw/items/9aa4551bd6bb9c0818b6)

```protobuf:proto/hello.proto
syntax = "proto3";

package example.helloworld;

message Request {
  string name = 1;
  int32 age = 2;
}

message Response {
  string message = 1;
}
```

# コンパイルする

```toml:Cargo.toml
[package]
name = "grpc-practice-rs"
version = "0.1.0"
authors = ["yagince <xxx@gmail.com>"]
edition = "2018"

[dependencies]
protobuf = "2.8.0"

[build-dependencies]
grpcio-compiler = "0.5.0-alpha.2"
which = "2.0.1"
```

```rust:build.rs
use std::process::Command;
use which::which;

fn main() {
    println!("gen protoc");
    let grpc_rust_plugin_path = which("grpc_rust_plugin")
        .expect("fail get extension path `grpc_rust_plugin`")
        .to_str()
        .expect("fail path string")
        .to_string();

    let output = Command::new("protoc")
        .arg("--rust_out=./src/")
        .arg("--grpc_out=./src/")
        .arg(format!("--plugin=protoc-gen-grpc={}", grpc_rust_plugin_path).as_str())
        .arg("proto/helloworld.proto")
        .output()
        .expect("fail command protoc");
    println!("{}", output.status);
    println!("{}", String::from_utf8_lossy(&output.stdout));
    println!("{}", String::from_utf8_lossy(&output.stderr));
}
```

```shell
$ cargo build
   Compiling grpc_practice_rs v0.1.0 (/Users/xxx/grpc-practice-rs)
error: failed to run custom build command for `grpc_practice_rs v0.1.0 (/Users/xxx/grpc-practice-rs)`

Caused by:
  process didn't exit successfully: `/Users/xxx/grpc-practice-rs/target/debug/build/grpc_practice_rs-2230aa86ff6e333c/build-script-build` (exit code: 101)
--- stdout
gen protoc

--- stderr
thread 'main' panicked at 'fail command protoc: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/libcore/result.rs:999:5
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
```

怒られました。
`protoc` コマンドはやっぱり必要でしたね。

# protocコマンドを入れる

[Protocol Buffers 導入メモ Mac/Win - Qiita](https://qiita.com/tkmn0/items/7208c880693e63ca247a#1-protoc-%E3%82%92%E5%85%A5%E6%89%8B)

```shell
$ brew install protobuf
...
$ protoc --version
libprotoc 3.7.1
```

# もう一度コンパイルしてみる

```shell
$ cargo build
```

成功しました

```rust:src/helloworld.rs
// This file is generated by rust-protobuf 2.8.0. Do not edit
// @generated

// https://github.com/Manishearth/rust-clippy/issues/702
#![allow(unknown_lints)]
#![allow(clippy::all)]

#![cfg_attr(rustfmt, rustfmt_skip)]

#![allow(box_pointers)]
#![allow(dead_code)]
#![allow(missing_docs)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(trivial_casts)]
#![allow(unsafe_code)]
#![allow(unused_imports)]
#![allow(unused_results)]
//! Generated file from `helloworld.proto`

use protobuf::Message as Message_imported_for_functions;
use protobuf::ProtobufEnum as ProtobufEnum_imported_for_functions;

/// Generated files are compatible only with the same version
/// of protobuf runtime.
const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_8_0;

#[derive(PartialEq,Clone,Default)]
pub struct Request {
    // message fields
    pub name: ::std::string::String,
    pub age: i32,
    // special fields
    pub unknown_fields: ::protobuf::UnknownFields,
    pub cached_size: ::protobuf::CachedSize,
}
...
```

# 生成されたファイルを読み込んでみる

```rust:src/lib.rs
pub mod helloworld;
```

```rust:src/bin/sample.rs
fn main() {
    let mut req = grpc_practice_rs::helloworld::Request::new();
    req.set_name("test".into());
    req.set_age(100);
    dbg!(req);
}
```

```shell
$ cargo run --bin sample
   Compiling grpc_practice_rs v0.1.0 (/Users/xxx/grpc-practice-rs)
    Finished dev [unoptimized + debuginfo] target(s) in 1.23s
     Running `target/debug/sample`
[src/bin/sample.rs:5] req = name: "test"
age: 100
```

とりあえず、読み込めましたね。

# serviceを定義してみる

In/Outのデータだけ定義していたので、Requestを受け取ってResponseを返すserviceを定義してみます。

```protobuf:proto/helloworld.proto
syntax = "proto3";

package example.helloworld;

message Request {
  string name = 1;
  int32 age = 2;
}

message Response {
  string message = 1;
}

// 追加
service Helloworld {
  rpc Call (Request) returns (Response);
}
```

```shell
$ cargo build
```

`src/helloworld_grpc.rs` が生成されました

```rust:src/hellworld_grpc.rs
// This file is generated. Do not edit
// @generated

// https://github.com/Manishearth/rust-clippy/issues/702
#![allow(unknown_lints)]
#![allow(clippy::all)]

#![cfg_attr(rustfmt, rustfmt_skip)]

#![allow(box_pointers)]
#![allow(dead_code)]
#![allow(missing_docs)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(trivial_casts)]
#![allow(unsafe_code)]
#![allow(unused_imports)]
#![allow(unused_results)]

const METHOD_HELLOWORLD_CALL: ::grpcio::Method<super::helloworld::Request, super::helloworld::Response> = ::grpcio::Method {
    ty: ::grpcio::MethodType::Unary,
    name: "/example.helloworld.Helloworld/Call",
    req_mar: ::grpcio::Marshaller { ser: ::grpcio::pb_ser, de: ::grpcio::pb_de },
    resp_mar: ::grpcio::Marshaller { ser: ::grpcio::pb_ser, de: ::grpcio::pb_de },
};

#[derive(Clone)]
pub struct HelloworldClient {
    client: ::grpcio::Client,
}

impl HelloworldClient {
    pub fn new(channel: ::grpcio::Channel) -> Self {
        HelloworldClient {
            client: ::grpcio::Client::new(channel),
        }
    }

    pub fn call_opt(&self, req: &super::helloworld::Request, opt: ::grpcio::CallOption) -> ::grpcio::Result<super::helloworld::Response> {
        self.client.unary_call(&METHOD_HELLOWORLD_CALL, req, opt)
    }

    pub fn call(&self, req: &super::helloworld::Request) -> ::grpcio::Result<super::helloworld::Response> {
        self.call_opt(req, ::grpcio::CallOption::default())
    }

    pub fn call_async_opt(&self, req: &super::helloworld::Request, opt: ::grpcio::CallOption) -> ::grpcio::Result<::grpcio::ClientUnaryReceiver<super::helloworld::Response>> {
        self.client.unary_call_async(&METHOD_HELLOWORLD_CALL, req, opt)
    }

    pub fn call_async(&self, req: &super::helloworld::Request) -> ::grpcio::Result<::grpcio::ClientUnaryReceiver<super::helloworld::Response>> {
        self.call_async_opt(req, ::grpcio::CallOption::default())
    }
    pub fn spawn<F>(&self, f: F) where F: ::futures::Future<Item = (), Error = ()> + Send + 'static {
        self.client.spawn(f)
    }
}

pub trait Helloworld {
    fn call(&mut self, ctx: ::grpcio::RpcContext, req: super::helloworld::Request, sink: ::grpcio::UnarySink<super::helloworld::Response>);
}

pub fn create_helloworld<S: Helloworld + Send + Clone + 'static>(s: S) -> ::grpcio::Service {
    let mut builder = ::grpcio::ServiceBuilder::new();
    let mut instance = s.clone();
    builder = builder.add_unary_handler(&METHOD_HELLOWORLD_CALL, move |ctx, req, resp| {
        instance.call(ctx, req, resp)
    });
    builder.build()
}
```

こんな感じ。

# Serverを実装してみる

[grpc-rs/server.rs at f0a26b528fb9648253044a8e46aaa62fab72b072 · pingcap/grpc-rs](https://github.com/pingcap/grpc-rs/blob/f0a26b528f/tests-and-examples/examples/hello_world/server.rs)

↑これを参考に実装してみます

```toml:Cargo.toml
[dependencies]
grpcio = "0.5.0-alpha.3"
protobuf = "2.8.0"
futures = "0.1.28"
log = "*"
env_logger = "*"

[build-dependencies]
grpcio-compiler = "0.5.0-alpha.2"
which = "2.0.1"
```

```rust:src/server.rs
use super::helloworld;
use super::helloworld_grpc;
use futures::Future;
use grpcio::{RpcContext, UnarySink};
use log::*;

#[derive(Debug, Clone, PartialEq)]
pub struct Server;

impl helloworld_grpc::Helloworld for Server {
    fn call(
        &mut self,
        ctx: RpcContext,
        req: helloworld::Request,
        sink: UnarySink<helloworld::Response>,
    ) {
        let mut res = helloworld::Response::default();
        res.set_message(format!(
            "Hello name: {} age: {}",
            req.get_name(),
            req.get_age()
        ));

        ctx.spawn(
            sink.success(res)
                .map_err(move |e| error!("failed to reply {:#?}: {:?}", req, e)),
        );
    }
}
```

```rust:src/bin/server.rs
use std::io::Read;
use std::sync::Arc;
use std::{io, thread};

use futures::{sync::oneshot, Future};
use grpc_practice::{helloworld_grpc::create_helloworld, Server};
use grpcio::{Environment, ServerBuilder};
use log::*;

fn main() {
    std::env::set_var("RUST_LOG", "server=info");
    env_logger::init();

    let env = Arc::new(Environment::new(1));
    let service = create_helloworld(Server);

    let mut server = ServerBuilder::new(env)
        .register_service(service)
        .bind("127.0.0.1", 50_051)
        .build()
        .unwrap();

    server.start();
    for &(ref host, port) in server.bind_addrs() {
        info!("listening on {}:{}", host, port);
    }
    let (tx, rx) = oneshot::channel();

    thread::spawn(move || {
        info!("Press ENTER to exit...");
        let _ = io::stdin().read(&mut [0]).unwrap();
        tx.send(())
    });
    let _ = rx.wait();
    let _ = server.shutdown().wait();
}
```

```shell
$ cargo run --bin server
...
[2019-07-29T13:07:34Z INFO  server] listening on 127.0.0.1:50051
[2019-07-29T13:07:34Z INFO  server] Press ENTER to exit...
```

とりあえず、起動しました。

# Clientを実装してみる

呼ぶ側を作らないことには動作してる様子がわからないので、書いてみます。

[grpc-rs/client.rs at f0a26b528fb9648253044a8e46aaa62fab72b072 · pingcap/grpc-rs](https://github.com/pingcap/grpc-rs/blob/f0a26b528f/tests-and-examples/examples/hello_world/client.rs)

こちらを参考に実装してみます。

```rust:src/bin/client.rs
use std::sync::Arc;

use grpc_practice::helloworld::Request;
use grpc_practice::helloworld_grpc::HelloworldClient;
use grpcio::{ChannelBuilder, EnvBuilder};
use log::*;

fn main() {
    std::env::set_var("RUST_LOG", "client=info");
    env_logger::init();

    let env = Arc::new(EnvBuilder::new().build());
    let ch = ChannelBuilder::new(env).connect("localhost:50051");

    let client = HelloworldClient::new(ch);

    let mut req = Request::default();
    req.set_name("world".to_owned());
    req.set_age(100);
    let reply = client.call(&req).expect("rpc");
    info!("Helloworld received: {}", reply.get_message());
}
```

```shell
$ cargo run --bin client
[2019-07-30T12:48:55Z INFO  client] Helloworld received: Hello name: world age: 100
```

とりあえず、疎通確認はできました。

# おまけ: Rubyから使う

せっかくなので、別の言語のClientからも呼んでみようかと思います。

```ruby:Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "grpc"
gem "grpc-tools"
```

```shell
$ mkdir ruby && cd ruby
$ grpc_tools_ruby_protoc -I ../proto --ruby_out=. --grpc_out=. ../proto/helloworld.proto
```


```ruby:helloworld_pb.rb
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: helloworld.proto

require 'google/protobuf'

Google::Protobuf::DescriptorPool.generated_pool.build do
  add_file("helloworld.proto", :syntax => :proto3) do
    add_message "example.helloworld.Request" do
      optional :name, :string, 1
      optional :age, :int32, 2
    end
    add_message "example.helloworld.Response" do
      optional :message, :string, 1
    end
  end
end

module Example
  module Helloworld
    Request = Google::Protobuf::DescriptorPool.generated_pool.lookup("example.helloworld.Request").msgclass
    Response = Google::Protobuf::DescriptorPool.generated_pool.lookup("example.helloworld.Response").msgclass
  end
end
```

```ruby:helloworld_services_pb.rb
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# Source: helloworld.proto for package 'example.helloworld'

require 'grpc'
require 'helloworld_pb'

module Example
  module Helloworld
    module Helloworld
      class Service

        include GRPC::GenericService

        self.marshal_class_method = :encode
        self.unmarshal_class_method = :decode
        self.service_name = 'example.helloworld.Helloworld'

        rpc :Call, Request, Response
      end

      Stub = Service.rpc_stub_class
    end
  end
end
```

こんな感じで出来ました。
呼んでみます

```ruby:client.rb
$LOAD_PATH.unshift(File.expand_path("../", __FILE__))
require 'helloworld_services_pb'

stub = Example::Helloworld::Helloworld::Stub.new('localhost:50051', :this_channel_is_insecure)

req = Example::Helloworld::Request.new(name: "hoge", age: 20000)

pp stub.call(req)
```

```shell
$ bundle exec ruby client.rb
<Example::Helloworld::Response: message: "Hello name: hoge age: 20000">
```

# TODO

- 認証まわりどうやるんだろうか
- DBアクセスするような場合のベストプラクティス
- etc...

今回は以上で、次回へつづく、、、かも。