* 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>
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. AScanover a large keyspace materializes every value in memory on both sides. - No compression. A frame with the compressed flag set is rejected as
unimplemented.