Vtprotobuf
A Protocol Buffers compiler that generates optimized marshaling & unmarshaling Go code for ProtoBuf APIv2
Install / Use
/learn @planetscale/VtprotobufREADME
vtprotobuf, the Vitess Protocol Buffers compiler
This repository provides the protoc-gen-go-vtproto plug-in for protoc, which is used by Vitess to generate optimized marshall & unmarshal code.
The code generated by this compiler is based on the optimized code generated by gogo/protobuf, although this package is not a fork of the original gogo compiler, as it has been implemented to support the new ProtoBuf APIv2 packages.
Available features
vtprotobuf is implemented as a helper plug-in that must be run alongside the upstream protoc-gen-go generator, as it generates fully-compatible auxiliary code to speed up (de)serialization of Protocol Buffer messages.
The following features can be generated:
-
size: generates afunc (p *YourProto) SizeVT() inthelper that behaves identically to callingproto.Size(p)on the message, except the size calculation is fully unrolled and does not use reflection. This helper function can be used directly, and it'll also be used by themarshalcodegen to ensure the destination buffer is properly sized before ProtoBuf objects are marshalled to it. -
equal: generates the following helper methods-
func (this *YourProto) EqualVT(that *YourProto) bool: this function behaves almost identically to callingproto.Equal(this, that)on messages, except the equality calculation is fully unrolled and does not use reflection. This helper function can be used directly. -
func (this *YourProto) EqualMessageVT(thatMsg proto.Message) bool: this function behaves like the abovethis.EqualVT(that), but allows comparing against arbitrary proto messages. IfthatMsgis not of type*YourProto, false is returned. The uniform signature provided by this method allows accessing this method via type assertions even if the message type is not known at compile time. This allows implementing a genericfunc EqualVT(proto.Message, proto.Message) boolwithout reflection.
-
-
marshal: generates the following helper methods-
func (p *YourProto) MarshalVT() ([]byte, error): this function behaves identically to callingproto.Marshal(p), except the actual marshalling has been fully unrolled and does not use reflection or allocate memory. This function simply allocates a properly sized buffer by callingSizeVTon the message and then usesMarshalToSizedBufferVTto marshal to it. -
func (p *YourProto) MarshalToVT(data []byte) (int, error): this function can be used to marshal a message to an existing buffer. The buffer must be large enough to hold the marshalled message, otherwise this function will panic. It returns the number of bytes marshalled. This function is useful e.g. when using memory pooling to re-use serialization buffers. -
func (p *YourProto) MarshalToSizedBufferVT(data []byte) (int, error): this function behaves likeMarshalTobut expects that the input buffer has the exact size required to hold the message, otherwise it will panic.
-
-
marshal_strict: generates the following helper methods-
func (p *YourProto) MarshalVTStrict() ([]byte, error): this function behaves likeMarshalVT, except fields are marshalled in a strict order by field's numbers they were declared in .proto file. -
func (p *YourProto) MarshalToVTStrict(data []byte) (int, error): this function behaves likeMarshalToVT, except fields are marshalled in a strict order by field's numbers they were declared in .proto file. -
func (p *YourProto) MarshalToSizedBufferVTStrict(data []byte) (int, error): this function behaves likeMarshalToSizedBufferVT, except fields are marshalled in a strict order by field's numbers they were declared in .proto file.
-
-
unmarshal: generates afunc (p *YourProto) UnmarshalVT(data []byte)that behaves similarly to callingproto.Unmarshal(data, p)on the message, except the unmarshalling is performed by unrolled codegen without using reflection and allocating as little memory as possible. If the receiverpis not fully zeroed-out, the unmarshal call will actually behave likeproto.Merge(data, p). This is because theproto.Unmarshalin the ProtoBuf API is implemented by resetting the destination message and then callingproto.Mergeon it. To ensure properUnmarshalsemantics, ensure you've calledproto.Reseton your message before callingUnmarshalVT, or that your message has been newly allocated.- The
ignoreUnknownFieldsoption can be used to ignore unknown fields in protobuf messages and further reduce memory allocations.
- The
-
unmarshal_unsafegenerates afunc (p *YourProto) UnmarshalVTUnsafe(data []byte)that behaves likeUnmarshalVT, except it unsafely casts slices of data tobytesandstringfields instead of copying them to newly allocated arrays, so that it performs less allocations. Data received from the wire has to be left untouched for the lifetime of the message. Otherwise, the message'sbytesandstringfields can be corrupted. -
pool: generates the following helper methods-
func (p *YourProto) ResetVT(): this function behaves similarly toproto.Reset(p), except it keeps as much memory as possible available on the message, so that further calls toUnmarshalVTon the same message will need to allocate less memory. This an API meant to be used with memory pools and does not need to be used directly. -
func (p *YourProto) ReturnToVTPool(): this function returns messagepto a local memory pool so it can be reused later. It clears the object properly withResetVTbefore storing it on the pool. This method should only be used on messages that were obtained from a memory pool by callingYourProtoFromVTPool. Usingpafter calling this method will lead to undefined behavior. -
func YourProtoFromVTPool() *YourProto: this function returns aYourProtomessage from a local memory pool, or allocates a new one if the pool is currently empty. The returned message is always empty and ready to be used (e.g. by callingUnmarshalVTon it). Once the message has been processed, it must be returned to the memory pool by callingReturnToVTPool()on it. Returning the message to the pool is not mandatory (it does not leak memory), but if you don't return it, that defeats the whole point of memory pooling.
-
-
clone: generates the following helper methods-
func (p *YourProto) CloneVT() *YourProto: this function behaves similarly to callingproto.Clone(p)on the message, except the cloning is performed by unrolled codegen without using reflection. If the receiverpisnila typednilis returned. -
func (p *YourProto) CloneMessageVT() proto.Message: this function behaves like the abovep.CloneVT(), but provides a uniform signature in order to be accessible via type assertions even if the type is not known at compile time. This allows implementing a genericfunc CloneVT(proto.Message)without reflection. If the receiverpisnil, a typednilpointer of the message type will be returned inside aproto.Messageinterface.
-
Field Options
uniqueis a field option available on strings. If it is set totruethen all all strings are interned using unique.Make. Go 1.23+ is needed.unmarshal_unsafetakes precendence overunique. Example usage:
import "github.com/planetscale/vtprotobuf/vtproto/ext.proto";
message Label {
string name = 1 [(vtproto.options).unique = true];
string value = 2 [(vtproto.options).unique = true];
}
Usage
-
Install
protoc-gen-go-vtproto:go install github.com/planetscale/vtprotobuf/cmd/protoc-gen-go-vtproto@latest -
Ensure your project is already using the ProtoBuf v2 API (i.e.
google.golang.org/protobuf). Thevtprotobufcompiler is not compatible with APIv1 generated code. -
Update your
protocgenerator to use the new plug-in. Example from Vitess:for name in $(PROTO_SRC_NAMES); do \ $(VTROOT)/bin/protoc \ --go_out=. --plugin protoc-gen-go="${GOBIN}/protoc-gen-go" \ --go-grpc_out=. --plugin protoc-gen-go-grpc="${GOBIN}/protoc-gen-go-grpc" \ --go-vtproto_out=. --plugin protoc-gen-go-vtproto="${GOBIN}/protoc-gen-go-vtproto" \ --go-vtproto_opt=features=marshal+unmarshal+size \ proto/$${name}.proto; \ doneNote that the
vtprotocompiler runs like an auxiliary plug-in to theprotoc-gen-goin APIv2, just like the new GRPC compiler plug-in,protoc-gen-go-grpc. You need to run it alongside the upstream generator, not as a replacement. -
(Optional) Pass the features that you want to generate as
--go-vtproto_opt. If no features are given, all the codegen steps will be performed. -
(Optional) If you have enabled the
pooloption, you need to manually specify which ProtoBuf objects will be pooled.- You can tag messages explicitly in the
.protofiles withoption (vtproto.mempool):
syntax = "proto3"; package app; option go_package = "app"; import "github.com/planetscale/vtprotobuf/vtproto/ext.proto"; message SampleMessage { option (vtproto.mempool) = true; // Enable memory pooling string name = 1; optional string project_id = 2; // ... }- Alternatively, you can enumerate the pooled objects with
--go-vtproto_opt=pool=<import>.<message>flags passed via the CLI:
$(VTROOT)/bin/protoc ... \ --go-vtproto_opt=features=marshal+unmarshal+size+pool \ --go-vtproto_opt=pool=vitess.io/vitess/go/vt/proto/query.Row \ --go-vtproto_opt=pool=vitess.io/vitess/go/vt/proto/binlogdata.VStreamRowsResponse \ - You can tag messages explicitly in the
-
(Optional) If you are handling messages containing unknown fields and don't intend to forward these messages to a tool that might expect these fields, you can ignore them using the
ignoreUnknownFieldsopti
