Description
tl;dr provide a way to override the mapping (i.e. the Converter
) between Go reflect
and Protobuf protoreflect
types so protoimpl
can be reused by other implementations
Problem and background
The protoreflect
interfaces behaviourally describe protobuf messages and are intended, amongst other things, to allow for custom code generation (for which there is strong demand, but an uncertain future: gogo/protobuf#691). However implementing the entire interface while supporting concurrency and fast path methods is a large undertaking.
Many of the low-hanging fruit (e.g. supporting fixed-width named type byte arrays) would need relatively minor changes to existing code to work but custom implementations find they need to fork large amounts of protobuf-go
in order to reuse the code in protoimpl
because of the structure of internal
packages (see below).
An approach for extending the nice functionality provided by protoimpl
that is conceptually appealing is to embed a MessageState
(*MessageInfo
) object in a struct and intercept protoreflect.Message
calls where needed before passing along to the underlying MessageInfo
object (continuing the earlier example, for example, taking a slice over a fixed-width byte array). However this approach does not work because MessageInfo.initOnce()
uses the go type to establish a Converter
the implementation of which is hard-coded and will throw errors when it does not see the Go type it expects for the protobuf type it is converting (for instance if a proto message field is not a pointer or a proto bytes
field is not a slice).
Possible solution
I understand why the protobuf-go
team has wanted to limit their public API, but I would like to propose that some small incision be made into protoimpl
in order to allow dependees to extend it and reuse the protoimpl
implementation.
My hunch is that if the protoimpl.TypeBuilder
as used by generated code were to take a user-providable Converter
implementation and pass it through the protoimpl
implementation then that might be enough to reuse most of the protoimpl
code when serialising to custom types. Another option would be to introduce a Converter() Converter
method to the protoreflect.Message
or another protoreflect
interface and have that optionally return a custom converter (otherwise returning nil, like with ProtoMethods()
), another option still would be to add it to the ProtoMethods()
return type. Athough this would not be about 'fast path', it might be convenient since the return type is already weakly typed.
Alternatives
I have attempted to only minimally fork protobuf-go
as suggested by @dsnet here: xen0n/protobuf-gogogo#1 with some success, the results of which are here: https://github.com/monax/peptide/
However, as described above, in order to extend the protoimpl
machinery this is not enough.
Another alternative would be to push most of the work of protoimpl
into the code generator itself and not use go reflection (strong static types and generated methods everywhere). This would probably also provide some performance benefits, but I am happy with protobuf-go performance and I would rather minimise the surface area that needs to be maintained for some simple idiomatic type mappings.
Context
Please see my initial ramble on this topic here: xen0n/protobuf-gogogo#1 (comment) including some code examples around how a wrapper/decorator implementation of protoreflect.Message
on top of MessageInfo
might work.
A further remark having read #526. It may be possible that protobuf-go
improves its generated code API to 'close the gap' that gogo proto has previously filled, which I am all in favour of, but there are clearly significant trade-offs, legacy, and consistency issues that the present project has to deal with being at the apex of the protobuf ecosystem.
I think it could be much more efficient for protobuf-go
to let a thousand flowers bloom here, provide a slightly larger public API that includes some limited access into protoimpl
, while hopefully still allowing you to evolve the internal implementation without making breaking changes. Projects like gogogo and peptide have more freedom to iterate on the code generation side and in doing so we can proof out any interface boundaries around protoimpl
, which protobuf-go
itself can make use of to provide a better generated Go API down the line.
Work
I had hoped to continue with my 'minimal fork' approach but it looks like the dependency tree will force me to fork many internal packages of protobuf-go
. I am open to guidance on what would be a good use of time, I could try and implement a proof-of-concept for the Converter
idea against a plain fork for protobuf-go
if it has any chance of getting accepted. Alternatively I could continue to hack and slash through my minimal fork version, but I'm not convinced I'll end up with something I want to maintain.