generic な golang gRPC サーバをつくる

# まえふり
gRPC サーバを実装するには protobuf ファイルから各言語のコードを自動生成し、各 RPC ごとの処理を実装することが一般的と思われる。一方で、どんな RPC に対しても同様な処理を行いたい generic な gRPC サーバを実現したい場合、もう少し簡単に RPC の追加を行いたい。

C++ の場合、このような generic な gRPC サーバを構築する方法はよく知られているように思う。[ここ](https://groups.google.com/g/grpc-io/c/NVtCvwiwIcE)のフォーラムで言及されているし、 一例を挙げると envoy の [json_grpc_transcoder](https://github.com/envoyproxy/envoy/blob/main/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.cc) で似たようなことは実装されている。
また、 Java でも[同様なことができる](https://groups.google.com/g/grpc-io/c/wtUnan5DdR4/m/MsBjl0Y7BwAJ)とサンプルコードとともに提示されている。

このような低レイヤのIFの提供は全ての言語で行われているわけではないが、golang でも[2020年に新しくなったライブラリ](https://go.dev/blog/protobuf-apiv2)を使えば generic な golang gRPC サーバを構築できそうだったのでつくってみたい。

## descriptor と dynamic message
具体的な実装に触れる前に、 generic な gRPC サーバを作るための要素となる descriptor と dynamic message について軽く触れておく。

### descriptor
descriptor は protobuf ファイルに対応する情報を持った protobuf message のことである。

protobuf ファイルを作成すると何らかの名称・型・オプション等を持つ field を含んだ message を作ることになる。descriptor はこれらのような protobuf ファイルで定義される情報を保持できるような構造を持った protobuf message となっている。descriptor の [protobuf ファイル](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/descriptor.proto) にあるように、protobuf の file・service・rpc・message・field・enum のそれぞれに対応する file descriptor・service descriptor・method descriptor・field descriptor・enum descriptor が存在する。
なので、file descriptor がわかっていれば protobuf ファイルをほぼ復元することができる。

### dynamic message
dynamic message は message descriptor から生成された protobuf message のことである。

protobuf message は各言語のコード自動生成によって各言語に応じた型やクラスにマッピングされるが、これをこれを使うとそれらの型やクラスを使わずに、 runtime で渡された descriptor をもとに protobuf message を生成できる。
C++ は [dynamic_message](https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.dynamic_message), Java は [DynamicMessage](
https://developers.google.com/protocol-buffers/docs/reference/java/com/google/protobuf/DynamicMessage), Golang は [dynamicpb](
https://pkg.go.dev/google.golang.org/protobuf/types/dynamicpb) で実装されている。

# golang での実装
golang protobuf の[標準ライブラリ](https://pkg.go.dev/google.golang.org/protobuf)を使って実装する。

## descriptor_set ファイルの生成
protobuf ファイルは[公式の helloworld.proto](https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld.proto)サンプルを利用する。
unary RPC を一つ持つシンプルな protobuf を例として取り上げるが、 複数 unary や stream RPC を含む場合でも似たようなことができると思う。

protobuf ファイルから下記コマンドで descriptor_set ファイルが生成される。これはhelloworld.proto の内容を [google.protobuf.descriptor.FileDescriptorSet](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/descriptor.proto#L57)  message の形に serialize されたファイルとなっている。

```bash
protoc --descriptor_set_out=helloworld_descriptor.pb helloworld.proto
```

## descriptor set file を取り込む
gRPC サーバ起動時に `RegisterService` メソッドを呼び出して service を登録する際に、 descriptor_set ファイル から `descriptorpb.FileDescriptorSet` に deserialize して全ての service を取得して下記のようにそれらを登録する。

```golang
func newGrpcServer() (*grpc.Server, error) {
	s := grpc.NewServer()
	bytes, err := ioutil.ReadFile("<descriptor file path>")
	if err != nil {
		return nil, err
	}
	var fileSet descriptorpb.FileDescriptorSet
	if err := proto.Unmarshal(bytes, &fileSet); err != nil {
		return nil, err
	}

	files := protoregistry.GlobalFiles
	for _, fd := range fileSet.File {
		d, err := protodesc.NewFile(fd, files)
		if err != nil {
			return nil, err
		}
		for i := 0; i < d.Services().Len(); i++ {
			// protobuf service descriptor から gRPC service descriptor に変換して service を登録する
			s.RegisterService(...)
		}
	}
	return s, nil
}
```

上記サンプルでは省略したが、もし gRPC reflection の機能をサーバに取り込みたい場合` protoregistry.GlobalFiles.RegisterFile(...)` を使って proto file を registry に登録する必要がある。 


## RPC 呼び出し時の処理をおこなう
全ての RPC で同様な処理を行いたい場合、 gRPC service descriptor を作成するときに `grpc.MethodDesc.Handler` に同一の handler を仕込むことになる。
その handler は例えば下記のような実装になる。request, response message ともに service descriptor から取得できる input message descriptor, output message descriptor から dynamic message を生成している。

```golang
func unaryHandler(svcd protoreflect.ServiceDescriptor, md protoreflect.MethodDescriptor) func(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
	return func(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
		in := dynamicpb.NewMessage(md.Input())
		if err := dec(in); err != nil {
			return nil, err
		}
		if interceptor == nil {
			return callRPC(ctx, in, md.FullName(), md.Output())
		}
		info := &grpc.UnaryServerInfo{
			Server:     srv,
			FullMethod: fmt.Sprintf("/%s/%s", svcd.FullName(), md.Name()),
		}
		handler := func(ctx context.Context, req interface{}) (interface{}, error) {
			return callRPC(ctx, req.(proto.Message), md.FullName(), md.Output())
		}
		return interceptor(ctx, in, info, handler)
	}
}

// RPC が呼ばれた際の処理例
func callRPC(base context.Context, request proto.Message, fullName protoreflect.FullName, mdesc protoreflect.MessageDescriptor) (interface{}, error) {
	reqbyte, err := protojson.Marshal(request)
	if err != nil {
		return nil, err
	}
	var m map[string]interface{}
	err = json.Unmarshal(reqbyte, &m)
	if err != nil {
		return nil, err
	}
	resp := dynamicpb.NewMessage(mdesc)
	respbyte := []byte(fmt.Sprintf("{\"message\":\"%s\"}", fmt.Sprintf("hello %s", m["name"])))
	err = protojson.Unmarshal(respbyte, resp)
	return resp, err
}
```

# さいごに
実際に実行可能なコードは[こちらのリポジトリ](https://github.com/hiroyoshii/golang-generic-grpcserver)にまとめた。
今回は標準ライブラリを使って実装したが、[github.com/jhump/protoreflect](https://github.com/jhump/protoreflect) も使いやすい API になっておりオススメ。