protobuf や gRPC を Kotlin DSL で実装してみる

# protobuf の Kotlin support

Kotlin で protobuf や gRPC を使うときに、基本的には Java でコンパイルされた protoc の出力を使って開発している方も多いかと思います。

しかし、protobuf には 2021 年ごろ公式に [Kotlin support](https://protobuf.dev/getting-started/kotlintutorial/) が追加されており、Kotlin DSL 形式で protobuf を使う事が可能になっています。

https://developers.googleblog.com/2021/11/announcing-kotlin-support-for-protocol.html

Kotlin の上で protobuf を利用しているなら、かなり使い心地が違うので、これからは DSL 記法を使っていきたい所です。Kotlin と Java を同時に出力することも可能なので、共存や段階的な移行も可能です。

簡単に、Kotlin support を使うための設定や、便利な点を紹介したいと思います。

## Java で出力したクラスを使って実装した場合

従来通り Java で出力した proto は、以下のように builder を使う必要があります。

```proto:hello.proto
message HelloRequest {
  string name = 1;
}
```

```Kotlin
val request = HelloRequest.newBuilder()
    .setName("hktechno")
    .build()
```

## Kotlin support を使って DSL 形式で実装した場合

Kotlin support を使うと、以下のようにシンプルな DSL で Kotlin らしく書く事ができます。

```Kotlin
val request = helloRequest {
    name = "hktechno"
}
```

### その他便利な機能

Kotlin support を使うと Kotlin の nullable との親和性が向上しています。

message フィールドに対して、orNull 付きのメソッドが追加され、nullable として取得する事ができます。optional field と一緒に使うと便利でしょう。

```proto:optional.proto
message NullableExample {
  optional string message = 1;
  optional HelloRequest request = 2;
}
```

```Kotlin
val example = nullableExample { .... }

// message の場合は optional に関わらず getValueOrNull メソッドが提供される
example.requestOrNull?.also {
    println("from: ${it.name}")
}

// string など non-message field は従来通り has メソッドを利用 (optional field のみ)
// example.messageOrNull とはできない
if (example.hasMessage()) {
    println("message: ${example.message}")
}
```

# gRPC の Kotlin support

gRPC でも Kotlin support が追加されています。

重要な点は coroutines への対応です。`CoroutineImplBase` や `CoroutineStub` が生成され、それぞれメソッドが `suspend fun` となり、gRPC を簡単に Kotlin coroutines へ対応させる事ができます。


```proto:hello.proto
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

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

```Kotlin
// Server
class HelloWorldService : GreeterCoroutineImplBase() {
  override suspend fun sayHello(request: HelloRequest) = helloReply {
    message = "hello, ${request.name}"
  }
}

// Client
val stub = GreeterCoroutineStub(channel)
suspend fun hello() {
    val request = helloRequest { name = "world" }
    val response = stub.sayHello(request)
    println("Received: ${response.message}")
}
```


# Gradle ビルド時の設定方法

ここでは、Gradle を利用して、Kotlin support を使って proto をコンパイルするためのビルドスクリプトの設定例を紹介します。

以下の例では、gRPC も使う前提で plugin の設定をしています。

## protoc を使う場合


```Kotlin:build.gradle.kts
protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:<version>"
    }
    plugins {
        id("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java:<version>"
        }
        id("grpckt") {
            artifact = "io.grpc:protoc-gen-grpc-kotlin:<version>"
        }
    }
    generateProtoTasks {
        all().forEach {
            it.builtins {
                id("kotlin")
            }
        }
        ofSourceSet("main").forEach {
            it.plugins {
                id("grpc")
                id("grpckt")
            }
        }
    }
}
```

## buf を使う場合の設定

`buf.gen.yaml` に以下の様に Kotlin plugin の設定を追加して buf generate すれば問題ありません。

```yml:buf.gen.yaml
version: v1
plugins:
  - plugin: buf.build/grpc/java
    out: java
  - plugin: buf.build/grpc/kotlin
    out: kotlin
  - plugin: buf.build/protocolbuffers/java
    out: java
  - plugin: buf.build/protocolbuffers/kotlin
    out: kotlin
```

Gradle を利用している場合には source set の設定で、以下の様に java と kotlin 両方を含める様にすれば良いでしょう。

```Kotlin:build.gradle.kts
sourceSets {
    listOf("java", "kotlin").forEach {
        val dir = "${layout.buildDirectory.get()}/$BUF_BUILD_DIR/$GENERATED_DIR/$it"
        getByName("main").java.srcDir(dir)
    }
}
```

# 参考文献

* [Protocol Buffer Basics: Kotlin](https://protobuf.dev/getting-started/kotlintutorial/)
* [Kotlin Generated Code Guide](https://protobuf.dev/reference/kotlin/kotlin-generated/)
* [Announcing Kotlin support for protocol buffers - Google for Developers](https://developers.googleblog.com/2021/11/announcing-kotlin-support-for-protocol.html)
* [Quick start | Kotlin | gRPC](https://grpc.io/docs/languages/kotlin/quickstart/)
* [Basics tutorial | Kotlin | gRPC](https://grpc.io/docs/languages/kotlin/basics/)