Files
Mahdi ramezaan zaadeandAlexander Medvednikov 3bfe757356 pbgen: emit files that are already v fmt clean (#29570)
* pbgen: emit files that are already v fmt clean

A generated file that the formatter rewrites is a bug in the generator, not a
quirk of the generated code. Two things in the output were not what `v fmt`
produces, so every file this tool wrote reformatted itself the first time anyone
ran the formatter -- a `v git-fmt-hook`, a `v fmt -w`, an editor -- and that
reformat then landed as a diff nobody had written, in whatever repository held
the generated file.

`emit_message_struct` indented `pub mut:` by one level and its fields by two.
`v fmt` puts `pub mut:` at column 0 and its fields one level in, so the whole
struct body shifted on the first format. `write_generated` also ended on the
blank line an emitter naturally arrives at, where `v fmt` leaves exactly one
trailing newline.

Both are fixed at the source rather than by formatting after the fact. The
second one is trimmed in `write_generated` because every output path goes
through it, which also covers the `-grpc` output and the stdout case is
unaffected since it does not write a file.

`examples/grpc/README.md` loses its `v fmt -w` line as a result: there is no
format step to remember any more. Verified with a compiler built from this
tree, running the documented command and confirming the output survives `v fmt`
byte for byte.

The negative control: with these two changes reverted, the same command's output
does not survive `v fmt`.

test_write_generated_replaces_its_own_output expected the written bytes to equal
its input exactly, which the trailing-newline contract changes; it now asserts
the normalised form, and test_write_generated_normalises_trailing_newlines covers
the three trailing-newline shapes an emitter produces.

v -silent test cmd/tools/vpbgen/    2 passed, 2 total

* pbgen: keep enum and service output formatter clean

---------

Co-authored-by: Alexander Medvednikov <alexander@medvednikov.com>
2026-10-05 23:27:24 +03:00
..

net.grpc example: an etcd-lite key-value service

A worked example of net.grpc: a small key-value store served over native gRPC, exercising every RPC shape.

examples/grpc/
  server.v          the service, served over gRPC on HTTP/2 + TLS
  client.v          calls every RPC against a running server
  kv.proto          the schema, kept next to the code that implements it
  kv/codec.v        generated by `v pbgen` from kv.proto, do not edit
  kv/service.v      generated method-path constants and the transport-free `KV` interface
  kv/service_impl.v the hand-written net.grpc `Service`
  cert/             a self-signed certificate so gRPC has something to speak

Run it

v run examples/grpc/server.v     # terminal 1
v run examples/grpc/client.v     # terminal 2

The client prints one section per RPC shape: unary, server streaming, client streaming, a call with a deadline and request metadata, and two error cases.

The four RPCs

RPC Shape Path
Get unary /kv.KV/Get
Put unary /kv.KV/Put
Scan server streaming (one request, many responses) /kv.KV/Scan
PutMany client streaming (many requests, one response) /kv.KV/PutMany

Two things worth copying

A handler reports failure by returning a StatusError, not by encoding one. GrpcServer turns a returned StatusError into the grpc-status trailer, which is what makes it arrive at the client as a typed code:

fn (mut s Service) put(req PutRequest, mut ctx grpc.ServerContext) ![]u8 {
	if req.key.len == 0 {
		return grpc.StatusError{
			status: grpc.Status{
				code:    .invalid_argument
				message: 'key must not be empty'
			}
		}
	}
	// ...
}

Wrapping the StatusError in a reply payload instead — return [grpc.StatusError{...}] — looks equivalent but is not: it becomes a framed body the client tries to parse as a message, and the call fails with a framing error instead of a status.

net.grpc never looks inside a message. Payloads are []u8 end to end, so the codec is a separate concern. Here kv/codec.v and kv/service.v are generated from kv.proto by the bundled v pbgen, on top of the encoding.protobuf runtime. Run from examples/grpc, this reproduces the committed files exactly:

v pbgen -m kv -o kv/codec.v -grpc kv/service.v kv.proto

pbgen writes what v fmt would write, so there is no format step to remember and no second commit to explain a reformat.

kv/service_impl.v is hand-written and is not regenerated. Only the method paths (kv_method_*) and the codec come from the generated files; Service deliberately does not implement the generated KV interface, because its handlers need a grpc.ServerContext to set headers and trailers (such as x-kv-scanned), and the transport-free interface has no parameter for one.

After regenerating, run v test vlib/encoding/protobuf/: the codec has no tests of its own because the wire format it is written against is tested once, in the module, rather than per example.

Nothing above the codec changes, because the generated code plugs into the same [][]u8 boundary.

The certificate

gRPC is HTTP/2-only, and V's net.http only negotiates HTTP/2 for https URLs — it advertises h2 over ALPN on the TLS listener and nothing else. So the example has to speak TLS even on loopback; there is no cleartext client path to test against.

cert/server.crt and cert/server.key are a self-signed pair for localhost, valid until 2050, copied from net.http's own TLS tests. To make your own with step-ca:

step ca certificate "localhost" cert/server.crt cert/server.key \
	--profile localhost --no-password --insecure

The client trusts that certificate by naming it as its own root:

mut client := grpc.Client{
	base_url: 'https://localhost:50051'
	verify:   os.join_path(os.dir(@FILE), 'cert', 'server.crt')
}

That is the self-signed equivalent of pinning a CA. In real use, point verify at your CA bundle. Leaving cert/cert_key empty on the server instead gives a cleartext h2c listener, which an insecure gRPC client can talk to — but V's client cannot, because it has no h2c path.

Limitations

net.grpc's limitations apply here too, and Scan/PutMany show both:

  • Streaming is buffered, not incremental. net.http's handler is one request in and one response out, so each call is a single POST whose body carries the whole stream. A Scan over a large keyspace materializes every value in memory on both sides.
  • No compression. A frame with the compressed flag set is rejected as unimplemented.