├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── e2e ├── buf.gen.yaml ├── e2e.pb.binary.go ├── e2e.pb.go ├── e2e.proto └── e2e_test.go ├── gen └── template.go ├── go.mod ├── go.sum └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | /protoc-gen-go-binary 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 HashiCorp, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: proto 2 | 3 | proto: build 4 | cd e2e && buf generate 5 | 6 | build: 7 | go build . 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # protoc-gen-go-binary 2 | 3 | This is a plugin for the Google Protocol Buffers compiler 4 | [`protoc`](https://github.com/protocolbuffers/protobuf) that generates 5 | code to implement [`encoding.BinaryMarshaler`](https://golang.org/pkg/encoding/#BinaryMarshaler) 6 | and [`encoding.BinaryUnmarshaler`](https://golang.org/pkg/encoding/#BinaryUnmarshaler) 7 | by just calling the `Marshal` and `Unmarshal` functions already generated for the types. 8 | 9 | This enables Go-generated protobuf messages to be used in situations where the code 10 | already supports using the binary marshaling interfaces. 11 | 12 | The code heavily relies on google.golang.org/protobuf/compiler/protogen and is mostly boilerplate. 13 | 14 | ## Install 15 | 16 | ``` 17 | go get github.com/hashicorp/protoc-gen-go-binary 18 | ``` 19 | 20 | Also required: 21 | 22 | - [buf](https://github.com/bufbuild/buf) 23 | - [protoc-gen-go](https://github.com/protocolbuffers/protobuf-go) 24 | 25 | ## Usage 26 | 27 | Define your messages like normal: 28 | 29 | ```proto 30 | syntax = "proto3"; 31 | 32 | message Request { 33 | oneof kind { 34 | string name = 1; 35 | int32 code = 2; 36 | } 37 | } 38 | ``` 39 | 40 | The example message purposely uses a `oneof` since this won't work by 41 | default with `encoding/json`. Next, generate the code: 42 | 43 | ``` 44 | protoc --go_out=. --go-binary_out=. request.proto 45 | ``` 46 | 47 | Your output should contain a file `request.pb.binary.go` which contains 48 | the implementation of `encoding.BinaryMarshal/BinaryUnmarshal` for all your message types. 49 | You can then encode binary encode your message as protobufs. 50 | 51 | ```go 52 | import ( 53 | "bytes" 54 | "encoding/gob" 55 | ) 56 | 57 | var buf bytes.Buffer 58 | encoder := gob.NewEncoder(&buf) 59 | 60 | // Marshal 61 | err := encoder.Encode(&Request{ 62 | Kind: &Kind_Name{ 63 | Name: "alice", 64 | }, 65 | } 66 | 67 | // Unmarshal 68 | var result Request 69 | decoder := gob.NewDecoder(&buf) 70 | err := decoder.Decode(&result) 71 | ``` 72 | -------------------------------------------------------------------------------- /e2e/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | version: v1 2 | managed: 3 | enabled: true 4 | go_package_prefix: 5 | default: github.com/hashicorp/protoc-gen-go-binary/e2e 6 | plugins: 7 | - name: go 8 | out: . 9 | opt: paths=source_relative 10 | - name: go-binary 11 | out: . 12 | path: ../protoc-gen-go-binary 13 | opt: paths=source_relative 14 | -------------------------------------------------------------------------------- /e2e/e2e.pb.binary.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-binary. DO NOT EDIT. 2 | // source: e2e.proto 3 | 4 | package e2e 5 | 6 | import ( 7 | "google.golang.org/protobuf/proto" 8 | ) 9 | 10 | // MarshalBinary implements encoding.BinaryMarshaler 11 | func (msg *Basic) MarshalBinary() ([]byte, error) { 12 | return proto.Marshal(msg) 13 | } 14 | 15 | // UnmarshalBinary implements encoding.BinaryUnmarshaler 16 | func (msg *Basic) UnmarshalBinary(b []byte) error { 17 | return proto.Unmarshal(b, msg) 18 | } 19 | -------------------------------------------------------------------------------- /e2e/e2e.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.1 4 | // protoc (unknown) 5 | // source: e2e.proto 6 | 7 | package e2e 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | // Basic just tests basic fields, including oneofs and so on that don't 24 | // generally work automatically with encoding/json. 25 | type Basic struct { 26 | state protoimpl.MessageState 27 | sizeCache protoimpl.SizeCache 28 | unknownFields protoimpl.UnknownFields 29 | 30 | A string `protobuf:"bytes,1,opt,name=a,proto3" json:"a,omitempty"` 31 | } 32 | 33 | func (x *Basic) Reset() { 34 | *x = Basic{} 35 | if protoimpl.UnsafeEnabled { 36 | mi := &file_e2e_proto_msgTypes[0] 37 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 38 | ms.StoreMessageInfo(mi) 39 | } 40 | } 41 | 42 | func (x *Basic) String() string { 43 | return protoimpl.X.MessageStringOf(x) 44 | } 45 | 46 | func (*Basic) ProtoMessage() {} 47 | 48 | func (x *Basic) ProtoReflect() protoreflect.Message { 49 | mi := &file_e2e_proto_msgTypes[0] 50 | if protoimpl.UnsafeEnabled && x != nil { 51 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 52 | if ms.LoadMessageInfo() == nil { 53 | ms.StoreMessageInfo(mi) 54 | } 55 | return ms 56 | } 57 | return mi.MessageOf(x) 58 | } 59 | 60 | // Deprecated: Use Basic.ProtoReflect.Descriptor instead. 61 | func (*Basic) Descriptor() ([]byte, []int) { 62 | return file_e2e_proto_rawDescGZIP(), []int{0} 63 | } 64 | 65 | func (x *Basic) GetA() string { 66 | if x != nil { 67 | return x.A 68 | } 69 | return "" 70 | } 71 | 72 | var File_e2e_proto protoreflect.FileDescriptor 73 | 74 | var file_e2e_proto_rawDesc = []byte{ 75 | 0x0a, 0x09, 0x65, 0x32, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x65, 0x32, 0x65, 76 | 0x22, 0x15, 0x0a, 0x05, 0x42, 0x61, 0x73, 0x69, 0x63, 0x12, 0x0c, 0x0a, 0x01, 0x61, 0x18, 0x01, 77 | 0x20, 0x01, 0x28, 0x09, 0x52, 0x01, 0x61, 0x42, 0x6e, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 78 | 0x32, 0x65, 0x42, 0x08, 0x45, 0x32, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2d, 79 | 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 80 | 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 81 | 0x67, 0x6f, 0x2d, 0x62, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x2f, 0x65, 0x32, 0x65, 0xa2, 0x02, 0x03, 82 | 0x45, 0x58, 0x58, 0xaa, 0x02, 0x03, 0x45, 0x32, 0x65, 0xca, 0x02, 0x03, 0x45, 0x32, 0x65, 0xe2, 83 | 0x02, 0x0f, 0x45, 0x32, 0x65, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 84 | 0x61, 0xea, 0x02, 0x03, 0x45, 0x32, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 85 | } 86 | 87 | var ( 88 | file_e2e_proto_rawDescOnce sync.Once 89 | file_e2e_proto_rawDescData = file_e2e_proto_rawDesc 90 | ) 91 | 92 | func file_e2e_proto_rawDescGZIP() []byte { 93 | file_e2e_proto_rawDescOnce.Do(func() { 94 | file_e2e_proto_rawDescData = protoimpl.X.CompressGZIP(file_e2e_proto_rawDescData) 95 | }) 96 | return file_e2e_proto_rawDescData 97 | } 98 | 99 | var file_e2e_proto_msgTypes = make([]protoimpl.MessageInfo, 1) 100 | var file_e2e_proto_goTypes = []interface{}{ 101 | (*Basic)(nil), // 0: e2e.Basic 102 | } 103 | var file_e2e_proto_depIdxs = []int32{ 104 | 0, // [0:0] is the sub-list for method output_type 105 | 0, // [0:0] is the sub-list for method input_type 106 | 0, // [0:0] is the sub-list for extension type_name 107 | 0, // [0:0] is the sub-list for extension extendee 108 | 0, // [0:0] is the sub-list for field type_name 109 | } 110 | 111 | func init() { file_e2e_proto_init() } 112 | func file_e2e_proto_init() { 113 | if File_e2e_proto != nil { 114 | return 115 | } 116 | if !protoimpl.UnsafeEnabled { 117 | file_e2e_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 118 | switch v := v.(*Basic); i { 119 | case 0: 120 | return &v.state 121 | case 1: 122 | return &v.sizeCache 123 | case 2: 124 | return &v.unknownFields 125 | default: 126 | return nil 127 | } 128 | } 129 | } 130 | type x struct{} 131 | out := protoimpl.TypeBuilder{ 132 | File: protoimpl.DescBuilder{ 133 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 134 | RawDescriptor: file_e2e_proto_rawDesc, 135 | NumEnums: 0, 136 | NumMessages: 1, 137 | NumExtensions: 0, 138 | NumServices: 0, 139 | }, 140 | GoTypes: file_e2e_proto_goTypes, 141 | DependencyIndexes: file_e2e_proto_depIdxs, 142 | MessageInfos: file_e2e_proto_msgTypes, 143 | }.Build() 144 | File_e2e_proto = out.File 145 | file_e2e_proto_rawDesc = nil 146 | file_e2e_proto_goTypes = nil 147 | file_e2e_proto_depIdxs = nil 148 | } 149 | -------------------------------------------------------------------------------- /e2e/e2e.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package e2e; 4 | 5 | // Basic just tests basic fields, including oneofs and so on that don't 6 | // generally work automatically with encoding/json. 7 | message Basic { 8 | string a = 1; 9 | } 10 | -------------------------------------------------------------------------------- /e2e/e2e_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestInterfaceImpl(t *testing.T) { 8 | b := &Basic{A: "foo"} 9 | 10 | var b2 Basic 11 | buf, err := b.MarshalBinary() 12 | if err != nil { 13 | t.Fatalf("MarshalBinary errored: %v", err) 14 | } 15 | 16 | if err := b2.UnmarshalBinary(buf); err != nil { 17 | t.Fatalf("UnmarshalBinary errored: %v", err) 18 | } 19 | 20 | if b.A != b2.A { 21 | t.Fatalf("Binary Marshal + Unmarshal isnt lossless") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gen/template.go: -------------------------------------------------------------------------------- 1 | package gen 2 | 3 | import ( 4 | "io" 5 | "text/template" 6 | 7 | "github.com/golang/glog" 8 | "google.golang.org/protobuf/compiler/protogen" 9 | ) 10 | 11 | func ApplyTemplate(w io.Writer, f *protogen.File) error { 12 | 13 | if err := headerTemplate.Execute(w, tplHeader{ 14 | File: f, 15 | }); err != nil { 16 | return err 17 | } 18 | 19 | return applyMessages(w, f.Messages) 20 | } 21 | 22 | func applyMessages(w io.Writer, msgs []*protogen.Message) error { 23 | for _, m := range msgs { 24 | 25 | if m.Desc.IsMapEntry() { 26 | glog.V(2).Infof("Skipping %s, mapentry message", m.GoIdent.GoName) 27 | continue 28 | } 29 | 30 | glog.V(2).Infof("Processing %s", m.GoIdent.GoName) 31 | if err := messageTemplate.Execute(w, tplMessage{ 32 | Message: m, 33 | }); err != nil { 34 | return err 35 | } 36 | 37 | // recursively apply any nested messages 38 | if err := applyMessages(w, m.Messages); err != nil { 39 | return err 40 | } 41 | } 42 | 43 | return nil 44 | } 45 | 46 | type tplHeader struct { 47 | *protogen.File 48 | } 49 | 50 | type tplMessage struct { 51 | *protogen.Message 52 | } 53 | 54 | var ( 55 | headerTemplate = template.Must(template.New("header").Parse(` 56 | // Code generated by protoc-gen-go-binary. DO NOT EDIT. 57 | // source: {{.Proto.Name}} 58 | 59 | package {{.GoPackageName}} 60 | 61 | import ( 62 | "google.golang.org/protobuf/proto" 63 | ) 64 | `)) 65 | 66 | messageTemplate = template.Must(template.New("message").Parse(` 67 | // MarshalBinary implements encoding.BinaryMarshaler 68 | func (msg *{{.GoIdent.GoName}}) MarshalBinary() ([]byte,error) { 69 | return proto.Marshal(msg) 70 | } 71 | 72 | // UnmarshalBinary implements encoding.BinaryUnmarshaler 73 | func (msg *{{.GoIdent.GoName}}) UnmarshalBinary(b []byte) error { 74 | return proto.Unmarshal(b, msg) 75 | } 76 | `)) 77 | ) 78 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/protoc-gen-go-binary 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b 7 | google.golang.org/protobuf v1.28.1 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 2 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 3 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 4 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 5 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 6 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 7 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 8 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 9 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 10 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 11 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "github.com/golang/glog" 8 | "github.com/hashicorp/protoc-gen-go-binary/gen" 9 | "google.golang.org/protobuf/compiler/protogen" 10 | plugin "google.golang.org/protobuf/types/pluginpb" 11 | ) 12 | 13 | var ( 14 | importPrefix = flag.String("import_prefix", "", "prefix to be added to go package paths for imported proto files") 15 | file = flag.String("file", "-", "where to load data from") 16 | ) 17 | 18 | func main() { 19 | flag.Parse() 20 | defer glog.Flush() 21 | 22 | protogen.Options{ 23 | ParamFunc: flag.CommandLine.Set, 24 | }.Run(func(gp *protogen.Plugin) error { 25 | 26 | gp.SupportedFeatures = uint64(plugin.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) 27 | 28 | for _, name := range gp.Request.FileToGenerate { 29 | f := gp.FilesByPath[name] 30 | 31 | if len(f.Messages) == 0 { 32 | glog.V(1).Infof("Skipping %s, no messages", name) 33 | continue 34 | } 35 | 36 | glog.V(1).Infof("Processing %s", name) 37 | glog.V(2).Infof("Generating %s\n", fmt.Sprintf("%s.pb.binary.go", f.GeneratedFilenamePrefix)) 38 | 39 | gf := gp.NewGeneratedFile(fmt.Sprintf("%s.pb.binary.go", f.GeneratedFilenamePrefix), f.GoImportPath) 40 | 41 | err := gen.ApplyTemplate(gf, f) 42 | if err != nil { 43 | gf.Skip() 44 | gp.Error(err) 45 | continue 46 | } 47 | } 48 | 49 | return nil 50 | }) 51 | } 52 | --------------------------------------------------------------------------------