4 Kotlin Guide
trev edited this page 2026-07-16 03:02:35 -04:00

Kotlin Guide

The Kotlin runtime provides coroutine-based clients and servers for all four RPC shapes. It lives under trevrpc-kotlin/ and currently builds from source rather than from published Maven artifacts.

Modules

Module Purpose
core Android-compatible runtime, framing, status, metadata, and server APIs
transport-netty JVM native QUIC, ordinary HTTP/3 clients, and the unified server
transport-cronet Android-oriented ordinary HTTP/3 client using an injected Cronet engine
protoc-gen-trevrpc-kotlin Protobuf plugin that generates Kotlin clients and server bindings
examples Compile-tested generated Greeter service and client

The build uses a Java 25 toolchain. core and transport-cronet target JVM 8 bytecode; the Netty transport, generator, and examples target JVM 21.

Add the Runtime

The modules are not published yet. Include trevrpc-kotlin in your source build and depend on the required projects:

dependencies {
    implementation(project(":core"))
    implementation(project(":transport-netty"))
    implementation("com.google.protobuf:protobuf-java:4.35.1")
}

Android applications can use :core with :transport-cronet instead. Application protobuf Java or lite message generation remains separate from TrevRPC binding generation.

Generate Bindings

Build the generator from the repository root:

cd trevrpc-kotlin
./gradlew :protoc-gen-trevrpc-kotlin:installDist

The executable is under protoc-gen-trevrpc-kotlin/build/install/protoc-gen-trevrpc-kotlin/bin/. Generated files use .trevrpc.kt by default and reference zip.trev.trevrpc unless runtime_package is set.

For each service, generated code contains:

  • A {Service}Service interface with suspend functions and Flow streams.
  • A {Service}Client with typed methods for every RPC shape.
  • A register{Service} function for Server.
  • Message codecs for the corresponding protobuf Java classes.
  • Service, method, and RPC-kind constants.

See Protobuf and Code Generation for plugin options.

Implement a Service

class Greeter : GreeterService {
    override suspend fun sayHello(
        context: RequestContext,
        request: HelloRequest,
    ): HelloReply =
        HelloReply.newBuilder()
            .setMessage("hello, ${request.name}")
            .build()

    override suspend fun lotsOfReplies(
        context: RequestContext,
        request: HelloRequest,
    ): Flow<HelloReply> = flowOf(reply("first"), reply("second"))

    override suspend fun lotsOfGreetings(
        context: RequestContext,
        requests: Flow<HelloRequest>,
    ): HelloReply = reply(requests.toList().joinToString { it.name })

    override suspend fun bidiHello(
        context: RequestContext,
        requests: Flow<HelloRequest>,
    ): Flow<HelloReply> = requests.map { reply(it.name) }
}

Throw TrevRpcException(Status...) to return an explicit TrevRPC status. Decode failures, deadlines, cancellation, and resource limits are mapped by the runtime.

Start a Netty Server

val server = Server(
    options = ServerOptions(),
    authorizer = Authorizer { request -> authorize(request.metadata) },
    metrics = metrics,
)
registerGreeter(server, Greeter())

val listener = NettyRpcServer.bind(
    server,
    NettyRpcServerConfig(
        bindAddress = InetSocketAddress("127.0.0.1", 50051),
        tls = NettyServerTls.Pem(
            privateKey = File("server-key.pem"),
            certificateChain = File("server-cert.pem"),
        ),
        enableNative = true,
        enableHttp3 = true,
        enableWebTransport = false,
    ),
)

NettyRpcServer binds exactly one UDP listener. Native QUIC uses ALPN trevrpc/1; ordinary HTTP/3 and WebTransport use h3. When WebTransport is enabled, install an explicit WebTransportAdmission policy for path, authority, origin, and TLS checks.

Call listener.shutdown() to stop admission, drain the core server according to ServerOptions.gracefulShutdownTimeout, close connections, and release Netty event-loop resources. Shutdown is idempotent.

Create a Netty Channel

val config = NettyQuicClientConfig(
    remoteAddress = InetSocketAddress("127.0.0.1", 50051),
    tls = NettyClientTls(
        serverName = "localhost",
        trustCertificates = listOf(certificate),
    ),
)
val channel: RpcChannel = NettyRpcChannel.nativeQuic(config)
channel.awaitReady()
val client = GreeterClient(
    channel,
    CallOptions(
        timeout = 5.seconds,
        metadata = Metadata.of("authorization" to "Bearer token".encodeToByteArray()),
    ),
)

try {
    val reply = client.sayHello(
        HelloRequest.newBuilder().setName("TrevRPC").build(),
    )
} finally {
    channel.close()
}

RpcChannel is the routine application API. It reconnects for future calls, but every RPC remains pinned to the ready generation it snapshots. Calls on a lost generation fail without retry, replay, resumption, or movement. New calls fail immediately with Unavailable while reconnecting; call channel.awaitReady() before issuing work only when waiting is intentional.

Use NettyRpcChannel.http3(config) for ordinary HTTP/3. Both Netty factories use fixed reconnect defaults: 100 ms initial delay, multiplier 2, 20 percent jitter, and a 30 second cap. Their routine APIs do not expose reconnect-policy tuning. Kotlin does not currently provide a WebTransport client.

The advanced single-connection classes are zip.trev.trevrpc.netty.advanced.RawNettyQuicRpcTransport and zip.trev.trevrpc.netty.advanced.RawNettyHttp3RpcTransport. They are intended for integrations, benchmarks, diagnostics, and deterministic tests; they do not reconnect.

NettyClientTls.insecureDevTrust and disabling hostname verification are development-only escape hatches. Production clients should use system trust, an explicit TrustManagerFactory, or explicit trust certificates while keeping hostname verification enabled.

Streaming

Generated convenience methods accept or return Flow:

client.lotsOfReplies(request).collect { reply ->
    println(reply.message)
}

val summary = client.lotsOfGreetings(flowOf(first, second))
val replies = client.bidiHello(flowOf(first, second))

Interactive upload and duplex methods expose bounded call objects:

val call = client.bidiHello()
try {
    call.send(first)
    call.send(second)
    call.closeSend()
    while (true) {
        val reply = call.receive() ?: break
        println(reply.message)
    }
} finally {
    call.close()
}

Generated Flow wrappers close calls in finally. For interactive calls, application code must call close() when it stops early. See Streaming for termination and limit semantics.

Android Cronet

CronetRpcChannel carries ordinary HTTP/3 RPCs through provider-managed Cronet BidirectionalStream instances:

val engine = provider.create(applicationContext)
val executor = Executors.newSingleThreadExecutor()
val channel: RpcChannel = CronetRpcChannel.create(
    engine,
    "https://api.example.com",
    executor,
)
val client = GreeterClient(channel)

The application selects the Cronet provider and owns both CronetEngine and the callback executor. The runtime does not create or shut down either resource. Provider availability, APK-size policy, device trust, and engine updates belong in application dependency injection. Call channel.close() before shutting down those resources.

Cronet owns its connection pool, reconnection, migration, and TLS ticket behavior. CronetRpcChannel provides the same long-lived application surface, and TrevRPC never retries or replays an RPC.

Limits and Lifecycle

CallOptions controls deadlines, response-body limits, response message/body limits, stream idle timeout, metadata, and the bounded request channel. ServerOptions controls frame size, connection/stream/request concurrency, request and stream limits, idle timeout, and graceful shutdown.

Nullable Kotlin limits disable the corresponding optional bound. Non-null numeric limits such as maxFrameSize and maxResponseBodySize remain explicit non-negative values. See Operations, Limits, and Security for defaults and deployment guidance.