diff --git a/.gitignore b/.gitignore index 5c986d1c..9c6aa35e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .cache vendor cmd/protoc-gen-go/protoc-gen-go +cmd/protoc-gen-go-grpc/protoc-gen-go-grpc diff --git a/cmd/protoc-gen-go-grpc/golden_test.go b/cmd/protoc-gen-go-grpc/golden_test.go new file mode 100644 index 00000000..e7efb3d9 --- /dev/null +++ b/cmd/protoc-gen-go-grpc/golden_test.go @@ -0,0 +1,25 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !race + +package main + +import ( + "flag" + "testing" + + "github.com/golang/protobuf/v2/internal/protogen/goldentest" +) + +// Set --regenerate to regenerate the golden files. +var regenerate = flag.Bool("regenerate", false, "regenerate golden files") + +func init() { + goldentest.Plugin(main) +} + +func TestGolden(t *testing.T) { + goldentest.Run(t, *regenerate) +} diff --git a/cmd/protoc-gen-go-grpc/internal_gengogrpc/grpc.go b/cmd/protoc-gen-go-grpc/internal_gengogrpc/grpc.go new file mode 100644 index 00000000..f8cf4b3c --- /dev/null +++ b/cmd/protoc-gen-go-grpc/internal_gengogrpc/grpc.go @@ -0,0 +1,409 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package internal_gengogrpc is internal to the protobuf module. +package internal_gengogrpc + +import ( + "fmt" + "strconv" + "strings" + + descpb "github.com/golang/protobuf/protoc-gen-go/descriptor" + "github.com/golang/protobuf/v2/protogen" +) + +type fileInfo struct { + *protogen.File + locationMap map[string][]*descpb.SourceCodeInfo_Location +} + +// GenerateFile generates a _grpc.pb.go file containing gRPC service definitions. +func GenerateFile(gen *protogen.Plugin, f *protogen.File) { + if len(f.Services) == 0 { + return + } + filename := f.GeneratedFilenamePrefix + "_grpc.pb.go" + g := gen.NewGeneratedFile(filename, f.GoImportPath) + g.P("// Code generated by protoc-gen-go-grpc. DO NOT EDIT.") + g.P() + g.P("package ", f.GoPackageName) + g.P() + GenerateFileContent(gen, f, g) +} + +// GenerateFileContent generates the gRPC service definitions, excluding the package statement. +func GenerateFileContent(gen *protogen.Plugin, f *protogen.File, g *protogen.GeneratedFile) { + if len(f.Services) == 0 { + return + } + file := &fileInfo{ + File: f, + locationMap: make(map[string][]*descpb.SourceCodeInfo_Location), + } + for _, loc := range file.Proto.GetSourceCodeInfo().GetLocation() { + key := pathKey(loc.Path) + file.locationMap[key] = append(file.locationMap[key], loc) + } + + // TODO: Remove this. We don't need to include these references any more. + g.P("// Reference imports to suppress errors if they are not otherwise used.") + g.P("var _ ", ident("context.Context")) + g.P("var _ ", ident("grpc.ClientConn")) + g.P() + + g.P("// This is a compile-time assertion to ensure that this generated file") + g.P("// is compatible with the grpc package it is being compiled against.") + g.P("const _ = ", ident("grpc.SupportPackageIsVersion4")) + g.P() + for _, service := range file.Services { + genService(gen, file, g, service) + } +} + +func genService(gen *protogen.Plugin, file *fileInfo, g *protogen.GeneratedFile, service *protogen.Service) { + clientName := service.GoName + "Client" + + g.P("// ", clientName, " is the client API for ", service.GoName, " service.") + g.P("//") + g.P("// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.") + + // Client interface. + // TODO deprecation + g.P("type ", clientName, " interface {") + for _, method := range service.Methods { + genComment(g, file, method.Path) + g.P(clientSignature(g, method)) + } + g.P("}") + g.P() + + // Client structure. + g.P("type ", unexport(clientName), " struct {") + g.P("cc *", ident("grpc.ClientConn")) + g.P("}") + g.P() + + // NewClient factory. + // TODO deprecation + g.P("func New", clientName, " (cc *", ident("grpc.ClientConn"), ") ", clientName, " {") + g.P("return &", unexport(clientName), "{cc}") + g.P("}") + g.P() + + var methodIndex, streamIndex int + // Client method implementations. + for _, method := range service.Methods { + if !method.Desc.IsStreamingServer() && !method.Desc.IsStreamingClient() { + // Unary RPC method + genClientMethod(gen, file, g, method, methodIndex) + methodIndex++ + } else { + // Streaming RPC method + genClientMethod(gen, file, g, method, streamIndex) + streamIndex++ + } + } + + // Server interface. + serverType := service.GoName + "Server" + g.P("// ", serverType, " is the server API for ", service.GoName, " service.") + // TODO deprecation + g.P("type ", serverType, " interface {") + for _, method := range service.Methods { + genComment(g, file, method.Path) + g.P(serverSignature(g, method)) + } + g.P("}") + g.P() + + // Server registration. + // TODO deprecation + serviceDescVar := "_" + service.GoName + "_serviceDesc" + g.P("func Register", service.GoName, "Server(s *", ident("grpc.Server"), ", srv ", serverType, ") {") + g.P("s.RegisterService(&", serviceDescVar, `, srv)`) + g.P("}") + g.P() + + // Server handler implementations. + var handlerNames []string + for _, method := range service.Methods { + hname := genServerMethod(gen, file, g, method) + handlerNames = append(handlerNames, hname) + } + + // Service descriptor. + g.P("var ", serviceDescVar, " = ", ident("grpc.ServiceDesc"), " {") + g.P("ServiceName: ", strconv.Quote(string(service.Desc.FullName())), ",") + g.P("HandlerType: (*", serverType, ")(nil),") + g.P("Methods: []", ident("grpc.MethodDesc"), "{") + for i, method := range service.Methods { + if method.Desc.IsStreamingClient() || method.Desc.IsStreamingServer() { + continue + } + g.P("{") + g.P("MethodName: ", strconv.Quote(method.GoName), ",") + g.P("Handler: ", handlerNames[i], ",") + g.P("},") + } + g.P("},") + g.P("Streams: []", ident("grpc.StreamDesc"), "{") + for i, method := range service.Methods { + if !method.Desc.IsStreamingClient() && !method.Desc.IsStreamingServer() { + continue + } + g.P("{") + g.P("StreamName: ", strconv.Quote(method.GoName), ",") + g.P("Handler: ", handlerNames[i], ",") + if method.Desc.IsStreamingServer() { + g.P("ServerStreams: true,") + } + if method.Desc.IsStreamingClient() { + g.P("ClientStreams: true,") + } + g.P("},") + } + g.P("},") + g.P("Metadata: \"", file.Desc.Path(), "\",") + g.P("}") + g.P() +} + +func clientSignature(g *protogen.GeneratedFile, method *protogen.Method) string { + s := method.GoName + "(ctx " + g.QualifiedGoIdent(ident("context.Context")) + if !method.Desc.IsStreamingClient() { + s += ", in *" + g.QualifiedGoIdent(method.InputType.GoIdent) + } + s += ", opts ..." + g.QualifiedGoIdent(ident("grpc.CallOption")) + ") (" + if !method.Desc.IsStreamingClient() && !method.Desc.IsStreamingServer() { + s += "*" + g.QualifiedGoIdent(method.OutputType.GoIdent) + } else { + s += method.ParentService.GoName + "_" + method.GoName + "Client" + } + s += ", error)" + return s +} + +func genClientMethod(gen *protogen.Plugin, file *fileInfo, g *protogen.GeneratedFile, method *protogen.Method, index int) { + service := method.ParentService + sname := fmt.Sprintf("/%s/%s", service.Desc.FullName(), method.Desc.Name()) + + // TODO deprecation + g.P("func (c *", unexport(service.GoName), "Client) ", clientSignature(g, method), "{") + if !method.Desc.IsStreamingServer() && !method.Desc.IsStreamingClient() { + g.P("out := new(", method.OutputType.GoIdent, ")") + g.P(`err := c.cc.Invoke(ctx, "`, sname, `", in, out, opts...)`) + g.P("if err != nil { return nil, err }") + g.P("return out, nil") + g.P("}") + g.P() + return + } + streamType := unexport(service.GoName) + method.GoName + "Client" + serviceDescVar := "_" + service.GoName + "_serviceDesc" + g.P("stream, err := c.cc.NewStream(ctx, &", serviceDescVar, ".Streams[", index, `], "`, sname, `", opts...)`) + g.P("if err != nil { return nil, err }") + g.P("x := &", streamType, "{stream}") + if !method.Desc.IsStreamingClient() { + g.P("if err := x.ClientStream.SendMsg(in); err != nil { return nil, err }") + g.P("if err := x.ClientStream.CloseSend(); err != nil { return nil, err }") + } + g.P("return x, nil") + g.P("}") + g.P() + + genSend := method.Desc.IsStreamingClient() + genRecv := method.Desc.IsStreamingServer() + genCloseAndRecv := !method.Desc.IsStreamingServer() + + // Stream auxiliary types and methods. + g.P("type ", service.GoName, "_", method.GoName, "Client interface {") + if genSend { + g.P("Send(*", method.InputType.GoIdent, ") error") + } + if genRecv { + g.P("Recv() (*", method.OutputType.GoIdent, ", error)") + } + if genCloseAndRecv { + g.P("CloseAndRecv() (*", method.OutputType.GoIdent, ", error)") + } + g.P(ident("grpc.ClientStream")) + g.P("}") + g.P() + + g.P("type ", streamType, " struct {") + g.P(ident("grpc.ClientStream")) + g.P("}") + g.P() + + if genSend { + g.P("func (x *", streamType, ") Send(m *", method.InputType.GoIdent, ") error {") + g.P("return x.ClientStream.SendMsg(m)") + g.P("}") + g.P() + } + if genRecv { + g.P("func (x *", streamType, ") Recv() (*", method.OutputType.GoIdent, ", error) {") + g.P("m := new(", method.OutputType.GoIdent, ")") + g.P("if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err }") + g.P("return m, nil") + g.P("}") + g.P() + } + if genCloseAndRecv { + g.P("func (x *", streamType, ") CloseAndRecv() (*", method.OutputType.GoIdent, ", error) {") + g.P("if err := x.ClientStream.CloseSend(); err != nil { return nil, err }") + g.P("m := new(", method.OutputType.GoIdent, ")") + g.P("if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err }") + g.P("return m, nil") + g.P("}") + g.P() + } +} + +func serverSignature(g *protogen.GeneratedFile, method *protogen.Method) string { + var reqArgs []string + ret := "error" + if !method.Desc.IsStreamingClient() && !method.Desc.IsStreamingServer() { + reqArgs = append(reqArgs, g.QualifiedGoIdent(ident("context.Context"))) + ret = "(*" + g.QualifiedGoIdent(method.OutputType.GoIdent) + ", error)" + } + if !method.Desc.IsStreamingClient() { + reqArgs = append(reqArgs, "*"+g.QualifiedGoIdent(method.InputType.GoIdent)) + } + if method.Desc.IsStreamingClient() || method.Desc.IsStreamingServer() { + reqArgs = append(reqArgs, method.ParentService.GoName+"_"+method.GoName+"Server") + } + return method.GoName + "(" + strings.Join(reqArgs, ", ") + ") " + ret +} + +func genServerMethod(gen *protogen.Plugin, file *fileInfo, g *protogen.GeneratedFile, method *protogen.Method) string { + service := method.ParentService + hname := fmt.Sprintf("_%s_%s_Handler", service.GoName, method.GoName) + + if !method.Desc.IsStreamingClient() && !method.Desc.IsStreamingServer() { + g.P("func ", hname, "(srv interface{}, ctx ", ident("context.Context"), ", dec func(interface{}) error, interceptor ", ident("grpc.UnaryServerInterceptor"), ") (interface{}, error) {") + g.P("in := new(", method.InputType.GoIdent, ")") + g.P("if err := dec(in); err != nil { return nil, err }") + g.P("if interceptor == nil { return srv.(", service.GoName, "Server).", method.GoName, "(ctx, in) }") + g.P("info := &", ident("grpc.UnaryServerInfo"), "{") + g.P("Server: srv,") + g.P("FullMethod: ", strconv.Quote(fmt.Sprintf("/%s/%s", service.Desc.FullName(), method.Desc.Name())), ",") + g.P("}") + g.P("handler := func(ctx ", ident("context.Context"), ", req interface{}) (interface{}, error) {") + g.P("return srv.(", service.GoName, "Server).", method.GoName, "(ctx, req.(*", method.InputType.GoIdent, "))") + g.P("}") + g.P("return interceptor(ctx, in, info, handler)") + g.P("}") + g.P() + return hname + } + streamType := unexport(service.GoName) + method.GoName + "Server" + g.P("func ", hname, "(srv interface{}, stream ", ident("grpc.ServerStream"), ") error {") + if !method.Desc.IsStreamingClient() { + g.P("m := new(", method.InputType.GoIdent, ")") + g.P("if err := stream.RecvMsg(m); err != nil { return err }") + g.P("return srv.(", service.GoName, "Server).", method.GoName, "(m, &", streamType, "{stream})") + } else { + g.P("return srv.(", service.GoName, "Server).", method.GoName, "(&", streamType, "{stream})") + } + g.P("}") + g.P() + + genSend := method.Desc.IsStreamingServer() + genSendAndClose := !method.Desc.IsStreamingServer() + genRecv := method.Desc.IsStreamingClient() + + // Stream auxiliary types and methods. + g.P("type ", service.GoName, "_", method.GoName, "Server interface {") + if genSend { + g.P("Send(*", method.OutputType.GoIdent, ") error") + } + if genSendAndClose { + g.P("SendAndClose(*", method.OutputType.GoIdent, ") error") + } + if genRecv { + g.P("Recv() (*", method.InputType.GoIdent, ", error)") + } + g.P(ident("grpc.ServerStream")) + g.P("}") + g.P() + + g.P("type ", streamType, " struct {") + g.P(ident("grpc.ServerStream")) + g.P("}") + g.P() + + if genSend { + g.P("func (x *", streamType, ") Send(m *", method.OutputType.GoIdent, ") error {") + g.P("return x.ServerStream.SendMsg(m)") + g.P("}") + g.P() + } + if genSendAndClose { + g.P("func (x *", streamType, ") SendAndClose(m *", method.OutputType.GoIdent, ") error {") + g.P("return x.ServerStream.SendMsg(m)") + g.P("}") + g.P() + } + if genRecv { + g.P("func (x *", streamType, ") Recv() (*", method.InputType.GoIdent, ", error) {") + g.P("m := new(", method.InputType.GoIdent, ")") + g.P("if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err }") + g.P("return m, nil") + g.P("}") + g.P() + } + + return hname +} + +var packages = map[string]protogen.GoImportPath{ + "context": "golang.org/x/net/context", + "grpc": "google.golang.org/grpc", +} + +func ident(name string) protogen.GoIdent { + idx := strings.LastIndex(name, ".") + return protogen.GoIdent{ + GoImportPath: packages[name[:idx]], + GoName: name[idx+1:], + } +} + +func genComment(g *protogen.GeneratedFile, file *fileInfo, path []int32) (hasComment bool) { + for _, loc := range file.locationMap[pathKey(path)] { + if loc.LeadingComments == nil { + continue + } + for _, line := range strings.Split(strings.TrimSuffix(loc.GetLeadingComments(), "\n"), "\n") { + hasComment = true + g.P("//", line) + } + break + } + return hasComment +} + +// deprecationComment returns a standard deprecation comment if deprecated is true. +func deprecationComment(deprecated bool) string { + if !deprecated { + return "" + } + return "// Deprecated: Do not use." +} + +// pathKey converts a location path to a string suitable for use as a map key. +func pathKey(path []int32) string { + var buf []byte + for i, x := range path { + if i != 0 { + buf = append(buf, ',') + } + buf = strconv.AppendInt(buf, int64(x), 10) + } + return string(buf) +} + +func unexport(s string) string { return strings.ToLower(s[:1]) + s[1:] } diff --git a/cmd/protoc-gen-go-grpc/main.go b/cmd/protoc-gen-go-grpc/main.go new file mode 100644 index 00000000..238a63ad --- /dev/null +++ b/cmd/protoc-gen-go-grpc/main.go @@ -0,0 +1,24 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The protoc-gen-go-grpc binary is a protoc plugin to generate Go gRPC +// service definitions. +package main + +import ( + "github.com/golang/protobuf/v2/cmd/protoc-gen-go-grpc/internal_gengogrpc" + "github.com/golang/protobuf/v2/protogen" +) + +func main() { + protogen.Run(nil, func(gen *protogen.Plugin) error { + for _, file := range gen.Files { + if !file.Generate { + continue + } + internal_gengogrpc.GenerateFile(gen, file) + } + return nil + }) +} diff --git a/cmd/protoc-gen-go-grpc/testdata/grpc/grpc.pb.go b/cmd/protoc-gen-go-grpc/testdata/grpc/grpc.pb.go new file mode 100644 index 00000000..d18855e3 --- /dev/null +++ b/cmd/protoc-gen-go-grpc/testdata/grpc/grpc.pb.go @@ -0,0 +1,108 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: grpc/grpc.proto + +package grpc + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type Request struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Request) Reset() { *m = Request{} } +func (m *Request) String() string { return proto.CompactTextString(m) } +func (*Request) ProtoMessage() {} +func (*Request) Descriptor() ([]byte, []int) { + return fileDescriptor_81ea47a3f88c2082, []int{0} +} + +func (m *Request) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Request.Unmarshal(m, b) +} +func (m *Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Request.Marshal(b, m, deterministic) +} +func (m *Request) XXX_Merge(src proto.Message) { + xxx_messageInfo_Request.Merge(m, src) +} +func (m *Request) XXX_Size() int { + return xxx_messageInfo_Request.Size(m) +} +func (m *Request) XXX_DiscardUnknown() { + xxx_messageInfo_Request.DiscardUnknown(m) +} + +var xxx_messageInfo_Request proto.InternalMessageInfo + +type Response struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Response) Reset() { *m = Response{} } +func (m *Response) String() string { return proto.CompactTextString(m) } +func (*Response) ProtoMessage() {} +func (*Response) Descriptor() ([]byte, []int) { + return fileDescriptor_81ea47a3f88c2082, []int{1} +} + +func (m *Response) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_Response.Unmarshal(m, b) +} +func (m *Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_Response.Marshal(b, m, deterministic) +} +func (m *Response) XXX_Merge(src proto.Message) { + xxx_messageInfo_Response.Merge(m, src) +} +func (m *Response) XXX_Size() int { + return xxx_messageInfo_Response.Size(m) +} +func (m *Response) XXX_DiscardUnknown() { + xxx_messageInfo_Response.DiscardUnknown(m) +} + +var xxx_messageInfo_Response proto.InternalMessageInfo + +func init() { + proto.RegisterType((*Request)(nil), "goproto.protoc.grpc.Request") + proto.RegisterType((*Response)(nil), "goproto.protoc.grpc.Response") +} + +func init() { proto.RegisterFile("grpc/grpc.proto", fileDescriptor_81ea47a3f88c2082) } + +var fileDescriptor_81ea47a3f88c2082 = []byte{ + // 211 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4f, 0x2f, 0x2a, 0x48, + 0xd6, 0x07, 0x11, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0xc2, 0xe9, 0xf9, 0x60, 0x06, 0x84, + 0x9b, 0xac, 0x07, 0x92, 0x52, 0xe2, 0xe4, 0x62, 0x0f, 0x4a, 0x2d, 0x2c, 0x4d, 0x2d, 0x2e, 0x51, + 0xe2, 0xe2, 0xe2, 0x08, 0x4a, 0x2d, 0x2e, 0xc8, 0xcf, 0x2b, 0x4e, 0x35, 0xda, 0xc8, 0xc4, 0xc5, + 0x12, 0x92, 0x5a, 0x5c, 0x22, 0xe4, 0xc1, 0xc5, 0x19, 0x9a, 0x97, 0x58, 0x54, 0xe9, 0x9c, 0x98, + 0x93, 0x23, 0x24, 0xa3, 0x87, 0xc5, 0x08, 0x3d, 0xa8, 0x7e, 0x29, 0x59, 0x1c, 0xb2, 0x10, 0x23, + 0x85, 0xbc, 0xb9, 0xb8, 0x5c, 0xf2, 0xcb, 0xf3, 0x8a, 0x4b, 0x8a, 0x52, 0x13, 0x73, 0x29, 0x32, + 0xca, 0x80, 0x51, 0xc8, 0x93, 0x8b, 0x23, 0xb4, 0x80, 0x0a, 0x46, 0x69, 0x30, 0x0a, 0xb9, 0x73, + 0xb1, 0x38, 0x65, 0xa6, 0x64, 0x52, 0x68, 0x8c, 0x01, 0xa3, 0x93, 0x7d, 0x94, 0x6d, 0x7a, 0x66, + 0x49, 0x46, 0x69, 0x92, 0x5e, 0x72, 0x7e, 0xae, 0x7e, 0x7a, 0x7e, 0x4e, 0x62, 0x5e, 0xba, 0x3e, + 0x58, 0x75, 0x52, 0x69, 0x9a, 0x7e, 0x99, 0x91, 0x7e, 0x72, 0x6e, 0x0a, 0x84, 0x9f, 0xac, 0x9b, + 0x9e, 0x9a, 0xa7, 0x9b, 0x9e, 0xaf, 0x5f, 0x92, 0x5a, 0x5c, 0x92, 0x92, 0x58, 0x92, 0x08, 0x8e, + 0xa6, 0x24, 0x36, 0xb0, 0xa4, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0x29, 0xd5, 0xc4, 0xd0, 0xba, + 0x01, 0x00, 0x00, +} diff --git a/cmd/protoc-gen-go-grpc/testdata/grpc/grpc.proto b/cmd/protoc-gen-go-grpc/testdata/grpc/grpc.proto new file mode 100644 index 00000000..c484ebe1 --- /dev/null +++ b/cmd/protoc-gen-go-grpc/testdata/grpc/grpc.proto @@ -0,0 +1,25 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +syntax = "proto3"; + +package goproto.protoc.grpc; + +option go_package = "github.com/golang/protobuf/v2/cmd/protoc-gen-go/testdata/grpc"; + +message Request {} +message Response {} + +service Test { + rpc UnaryCall(Request) returns (Response); + + // This RPC streams from the server only. + rpc Downstream(Request) returns (stream Response); + + // This RPC streams from the client. + rpc Upstream(stream Request) returns (Response); + + // This one streams in both directions. + rpc Bidi(stream Request) returns (stream Response); +} diff --git a/cmd/protoc-gen-go-grpc/testdata/grpc/grpc_grpc.pb.go b/cmd/protoc-gen-go-grpc/testdata/grpc/grpc_grpc.pb.go new file mode 100644 index 00000000..9db0a3af --- /dev/null +++ b/cmd/protoc-gen-go-grpc/testdata/grpc/grpc_grpc.pb.go @@ -0,0 +1,279 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package grpc + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// TestClient is the client API for Test service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type TestClient interface { + UnaryCall(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) + // This RPC streams from the server only. + Downstream(ctx context.Context, in *Request, opts ...grpc.CallOption) (Test_DownstreamClient, error) + // This RPC streams from the client. + Upstream(ctx context.Context, opts ...grpc.CallOption) (Test_UpstreamClient, error) + // This one streams in both directions. + Bidi(ctx context.Context, opts ...grpc.CallOption) (Test_BidiClient, error) +} + +type testClient struct { + cc *grpc.ClientConn +} + +func NewTestClient(cc *grpc.ClientConn) TestClient { + return &testClient{cc} +} + +func (c *testClient) UnaryCall(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) { + out := new(Response) + err := c.cc.Invoke(ctx, "/goproto.protoc.grpc.Test/UnaryCall", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *testClient) Downstream(ctx context.Context, in *Request, opts ...grpc.CallOption) (Test_DownstreamClient, error) { + stream, err := c.cc.NewStream(ctx, &_Test_serviceDesc.Streams[0], "/goproto.protoc.grpc.Test/Downstream", opts...) + if err != nil { + return nil, err + } + x := &testDownstreamClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type Test_DownstreamClient interface { + Recv() (*Response, error) + grpc.ClientStream +} + +type testDownstreamClient struct { + grpc.ClientStream +} + +func (x *testDownstreamClient) Recv() (*Response, error) { + m := new(Response) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *testClient) Upstream(ctx context.Context, opts ...grpc.CallOption) (Test_UpstreamClient, error) { + stream, err := c.cc.NewStream(ctx, &_Test_serviceDesc.Streams[1], "/goproto.protoc.grpc.Test/Upstream", opts...) + if err != nil { + return nil, err + } + x := &testUpstreamClient{stream} + return x, nil +} + +type Test_UpstreamClient interface { + Send(*Request) error + CloseAndRecv() (*Response, error) + grpc.ClientStream +} + +type testUpstreamClient struct { + grpc.ClientStream +} + +func (x *testUpstreamClient) Send(m *Request) error { + return x.ClientStream.SendMsg(m) +} + +func (x *testUpstreamClient) CloseAndRecv() (*Response, error) { + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + m := new(Response) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *testClient) Bidi(ctx context.Context, opts ...grpc.CallOption) (Test_BidiClient, error) { + stream, err := c.cc.NewStream(ctx, &_Test_serviceDesc.Streams[2], "/goproto.protoc.grpc.Test/Bidi", opts...) + if err != nil { + return nil, err + } + x := &testBidiClient{stream} + return x, nil +} + +type Test_BidiClient interface { + Send(*Request) error + Recv() (*Response, error) + grpc.ClientStream +} + +type testBidiClient struct { + grpc.ClientStream +} + +func (x *testBidiClient) Send(m *Request) error { + return x.ClientStream.SendMsg(m) +} + +func (x *testBidiClient) Recv() (*Response, error) { + m := new(Response) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// TestServer is the server API for Test service. +type TestServer interface { + UnaryCall(context.Context, *Request) (*Response, error) + // This RPC streams from the server only. + Downstream(*Request, Test_DownstreamServer) error + // This RPC streams from the client. + Upstream(Test_UpstreamServer) error + // This one streams in both directions. + Bidi(Test_BidiServer) error +} + +func RegisterTestServer(s *grpc.Server, srv TestServer) { + s.RegisterService(&_Test_serviceDesc, srv) +} + +func _Test_UnaryCall_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TestServer).UnaryCall(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/goproto.protoc.grpc.Test/UnaryCall", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TestServer).UnaryCall(ctx, req.(*Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Test_Downstream_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(Request) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(TestServer).Downstream(m, &testDownstreamServer{stream}) +} + +type Test_DownstreamServer interface { + Send(*Response) error + grpc.ServerStream +} + +type testDownstreamServer struct { + grpc.ServerStream +} + +func (x *testDownstreamServer) Send(m *Response) error { + return x.ServerStream.SendMsg(m) +} + +func _Test_Upstream_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(TestServer).Upstream(&testUpstreamServer{stream}) +} + +type Test_UpstreamServer interface { + SendAndClose(*Response) error + Recv() (*Request, error) + grpc.ServerStream +} + +type testUpstreamServer struct { + grpc.ServerStream +} + +func (x *testUpstreamServer) SendAndClose(m *Response) error { + return x.ServerStream.SendMsg(m) +} + +func (x *testUpstreamServer) Recv() (*Request, error) { + m := new(Request) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func _Test_Bidi_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(TestServer).Bidi(&testBidiServer{stream}) +} + +type Test_BidiServer interface { + Send(*Response) error + Recv() (*Request, error) + grpc.ServerStream +} + +type testBidiServer struct { + grpc.ServerStream +} + +func (x *testBidiServer) Send(m *Response) error { + return x.ServerStream.SendMsg(m) +} + +func (x *testBidiServer) Recv() (*Request, error) { + m := new(Request) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +var _Test_serviceDesc = grpc.ServiceDesc{ + ServiceName: "goproto.protoc.grpc.Test", + HandlerType: (*TestServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "UnaryCall", + Handler: _Test_UnaryCall_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "Downstream", + Handler: _Test_Downstream_Handler, + ServerStreams: true, + }, + { + StreamName: "Upstream", + Handler: _Test_Upstream_Handler, + ClientStreams: true, + }, + { + StreamName: "Bidi", + Handler: _Test_Bidi_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "grpc/grpc.proto", +} diff --git a/cmd/protoc-gen-go/golden_test.go b/cmd/protoc-gen-go/golden_test.go index 80e2d8cc..e7efb3d9 100644 --- a/cmd/protoc-gen-go/golden_test.go +++ b/cmd/protoc-gen-go/golden_test.go @@ -2,129 +2,24 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// +build !race + package main import ( - "bytes" "flag" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" "testing" + + "github.com/golang/protobuf/v2/internal/protogen/goldentest" ) // Set --regenerate to regenerate the golden files. var regenerate = flag.Bool("regenerate", false, "regenerate golden files") -// When the environment variable RUN_AS_PROTOC_GEN_GO is set, we skip running -// tests and instead act as protoc-gen-go. This allows the test binary to -// pass itself to protoc. func init() { - if os.Getenv("RUN_AS_PROTOC_GEN_GO") != "" { - main() - os.Exit(0) - } + goldentest.Plugin(main) } func TestGolden(t *testing.T) { - workdir, err := ioutil.TempDir("", "proto-test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(workdir) - - // Find all the proto files we need to compile. We assume that each directory - // contains the files for a single package. - packages := map[string][]string{} - err = filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error { - if !strings.HasSuffix(path, ".proto") { - return nil - } - dir := filepath.Dir(path) - packages[dir] = append(packages[dir], path) - return nil - }) - if err != nil { - t.Fatal(err) - } - - // Compile each package, using this binary as protoc-gen-go. - for _, sources := range packages { - args := []string{"-Itestdata", "--go_out=plugins=grpc,paths=source_relative:" + workdir} - args = append(args, sources...) - protoc(t, args) - } - - // Compare each generated file to the golden version. - filepath.Walk(workdir, func(genPath string, info os.FileInfo, _ error) error { - if info.IsDir() { - return nil - } - - // For each generated file, figure out the path to the corresponding - // golden file in the testdata directory. - relPath, err := filepath.Rel(workdir, genPath) - if err != nil { - t.Errorf("filepath.Rel(%q, %q): %v", workdir, genPath, err) - return nil - } - if filepath.SplitList(relPath)[0] == ".." { - t.Errorf("generated file %q is not relative to %q", genPath, workdir) - } - goldenPath := filepath.Join("testdata", relPath) - - got, err := ioutil.ReadFile(genPath) - if err != nil { - t.Error(err) - return nil - } - if *regenerate { - // If --regenerate set, just rewrite the golden files. - err := ioutil.WriteFile(goldenPath, got, 0666) - if err != nil { - t.Error(err) - } - return nil - } - - want, err := ioutil.ReadFile(goldenPath) - if err != nil { - t.Error(err) - return nil - } - - want = fdescRE.ReplaceAll(want, nil) - got = fdescRE.ReplaceAll(got, nil) - if bytes.Equal(got, want) { - return nil - } - - cmd := exec.Command("diff", "-u", goldenPath, genPath) - out, _ := cmd.CombinedOutput() - t.Errorf("golden file differs: %v\n%v", relPath, string(out)) - return nil - }) -} - -var fdescRE = regexp.MustCompile(`(?ms)^var fileDescriptor.*}`) - -func protoc(t *testing.T, args []string) { - cmd := exec.Command("protoc", "--plugin=protoc-gen-go="+os.Args[0]) - cmd.Args = append(cmd.Args, args...) - // We set the RUN_AS_PROTOC_GEN_GO environment variable to indicate that - // the subprocess should act as a proto compiler rather than a test. - cmd.Env = append(os.Environ(), "RUN_AS_PROTOC_GEN_GO=1") - out, err := cmd.CombinedOutput() - if len(out) > 0 || err != nil { - t.Log("RUNNING: ", strings.Join(cmd.Args, " ")) - } - if len(out) > 0 { - t.Log(string(out)) - } - if err != nil { - t.Fatalf("protoc: %v", err) - } + goldentest.Run(t, *regenerate) } diff --git a/cmd/protoc-gen-go/internal_gengo/main.go b/cmd/protoc-gen-go/internal_gengo/main.go index 518f4279..a337c5ff 100644 --- a/cmd/protoc-gen-go/internal_gengo/main.go +++ b/cmd/protoc-gen-go/internal_gengo/main.go @@ -10,6 +10,7 @@ import ( "compress/gzip" "crypto/sha256" "encoding/hex" + "errors" "flag" "fmt" "math" @@ -33,12 +34,14 @@ const protoPackage = "github.com/golang/protobuf/proto" func Main() { var flags flag.FlagSet - // TODO: Decide what to do for backwards compatibility with plugins=grpc. - flags.String("plugins", "", "") + plugins := flags.String("plugins", "", "deprecated option") opts := &protogen.Options{ ParamFunc: flags.Set, } protogen.Run(opts, func(gen *protogen.Plugin) error { + if *plugins != "" { + return errors.New("protoc-gen-go: plugins are not supported; use 'protoc --go-grpc_out=...' to generate gRPC") + } for _, f := range gen.Files { if !f.Generate { continue @@ -138,7 +141,6 @@ func genFile(gen *protogen.Plugin, file *protogen.File) { } genInitFunction(gen, g, f) - genFileDescriptor(gen, g, f) } diff --git a/internal/protogen/goldentest/goldentest.go b/internal/protogen/goldentest/goldentest.go new file mode 100644 index 00000000..82468b79 --- /dev/null +++ b/internal/protogen/goldentest/goldentest.go @@ -0,0 +1,130 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package goldentest compares the output of a protoc plugin to golden files. +package goldentest + +import ( + "bytes" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "testing" +) + +// Plugin should be called at init time with a function that acts as a +// protoc plugin. +func Plugin(f func()) { + // When the environment variable RUN_AS_PROTOC_PLUGIN is set, we skip + // running tests and instead act as protoc-gen-go. This allows the + // test binary to pass itself to protoc. + if os.Getenv("RUN_AS_PROTOC_PLUGIN") != "" { + f() + os.Exit(0) + } +} + +// Run executes golden tests. +func Run(t *testing.T, regenerate bool) { + workdir, err := ioutil.TempDir("", "proto-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(workdir) + + // Find all the proto files we need to compile. We assume that each directory + // contains the files for a single package. + packages := map[string][]string{} + err = filepath.Walk("testdata", func(path string, info os.FileInfo, err error) error { + if !strings.HasSuffix(path, ".proto") { + return nil + } + dir := filepath.Dir(path) + packages[dir] = append(packages[dir], path) + return nil + }) + if err != nil { + t.Fatal(err) + } + + // Compile each package, using this binary as protoc-gen-go. + for _, sources := range packages { + args := []string{"-Itestdata", "--go_out=paths=source_relative:" + workdir} + args = append(args, sources...) + protoc(t, args) + } + + // Compare each generated file to the golden version. + filepath.Walk(workdir, func(genPath string, info os.FileInfo, _ error) error { + if info.IsDir() { + return nil + } + + // For each generated file, figure out the path to the corresponding + // golden file in the testdata directory. + relPath, err := filepath.Rel(workdir, genPath) + if err != nil { + t.Errorf("filepath.Rel(%q, %q): %v", workdir, genPath, err) + return nil + } + if filepath.SplitList(relPath)[0] == ".." { + t.Errorf("generated file %q is not relative to %q", genPath, workdir) + } + goldenPath := filepath.Join("testdata", relPath) + + got, err := ioutil.ReadFile(genPath) + if err != nil { + t.Error(err) + return nil + } + if regenerate { + // If --regenerate set, just rewrite the golden files. + err := ioutil.WriteFile(goldenPath, got, 0666) + if err != nil { + t.Error(err) + } + return nil + } + + want, err := ioutil.ReadFile(goldenPath) + if err != nil { + t.Error(err) + return nil + } + + want = fdescRE.ReplaceAll(want, nil) + got = fdescRE.ReplaceAll(got, nil) + if bytes.Equal(got, want) { + return nil + } + + cmd := exec.Command("diff", "-u", goldenPath, genPath) + out, _ := cmd.CombinedOutput() + t.Errorf("golden file differs: %v\n%v", relPath, string(out)) + return nil + }) +} + +var fdescRE = regexp.MustCompile(`(?ms)^var fileDescriptor.*}`) + +func protoc(t *testing.T, args []string) { + cmd := exec.Command("protoc", "--plugin=protoc-gen-go="+os.Args[0]) + cmd.Args = append(cmd.Args, args...) + // We set the RUN_AS_PROTOC_PLUGIN environment variable to indicate that + // the subprocess should act as a proto compiler rather than a test. + cmd.Env = append(os.Environ(), "RUN_AS_PROTOC_PLUGIN=1") + out, err := cmd.CombinedOutput() + if len(out) > 0 || err != nil { + t.Log("RUNNING: ", strings.Join(cmd.Args, " ")) + } + if len(out) > 0 { + t.Log(string(out)) + } + if err != nil { + t.Fatalf("protoc: %v", err) + } +} diff --git a/protogen/protogen.go b/protogen/protogen.go index 03a2ff01..87e643cf 100644 --- a/protogen/protogen.go +++ b/protogen/protogen.go @@ -349,6 +349,7 @@ type File struct { Messages []*Message // top-level message declarations Enums []*Enum // top-level enum declarations Extensions []*Extension // top-level extension declarations + Services []*Service // top-level service declarations Generate bool // true if we should generate code for this file // GeneratedFilenamePrefix is used to construct filenames for generated @@ -401,6 +402,9 @@ func newFile(gen *Plugin, p *descpb.FileDescriptorProto, packageName GoPackageNa for i, extdescs := 0, desc.Extensions(); i < extdescs.Len(); i++ { f.Extensions = append(f.Extensions, newField(gen, f, nil, extdescs.Get(i))) } + for i, sdescs := 0, desc.Services(); i < sdescs.Len(); i++ { + f.Services = append(f.Services, newService(gen, f, sdescs.Get(i))) + } for _, message := range f.Messages { if err := message.init(gen); err != nil { return nil, err @@ -411,6 +415,13 @@ func newFile(gen *Plugin, p *descpb.FileDescriptorProto, packageName GoPackageNa return nil, err } } + for _, service := range f.Services { + for _, method := range service.Methods { + if err := method.init(gen); err != nil { + return nil, err + } + } + } return f, nil } @@ -723,6 +734,68 @@ func (gen *Plugin) NewGeneratedFile(filename string, goImportPath GoImportPath) return g } +// A Service describes a service. +type Service struct { + Desc protoreflect.ServiceDescriptor + + GoName string + Path []int32 // location path of this service + Methods []*Method // service method definitions +} + +func newService(gen *Plugin, f *File, desc protoreflect.ServiceDescriptor) *Service { + service := &Service{ + Desc: desc, + GoName: camelCase(string(desc.Name())), + Path: []int32{fileServiceField, int32(desc.Index())}, + } + for i, mdescs := 0, desc.Methods(); i < mdescs.Len(); i++ { + service.Methods = append(service.Methods, newMethod(gen, f, service, mdescs.Get(i))) + } + return service +} + +// A Method describes a method in a service. +type Method struct { + Desc protoreflect.MethodDescriptor + + GoName string + ParentService *Service + Path []int32 // location path of this method + InputType *Message + OutputType *Message +} + +func newMethod(gen *Plugin, f *File, service *Service, desc protoreflect.MethodDescriptor) *Method { + method := &Method{ + Desc: desc, + GoName: camelCase(string(desc.Name())), + ParentService: service, + Path: pathAppend(service.Path, serviceMethodField, int32(desc.Index())), + } + return method +} + +func (method *Method) init(gen *Plugin) error { + desc := method.Desc + + inName := desc.InputType().FullName() + in, ok := gen.messagesByName[inName] + if !ok { + return fmt.Errorf("method %v: no descriptor for type %v", desc.FullName(), inName) + } + method.InputType = in + + outName := desc.OutputType().FullName() + out, ok := gen.messagesByName[outName] + if !ok { + return fmt.Errorf("method %v: no descriptor for type %v", desc.FullName(), outName) + } + method.OutputType = out + + return nil +} + // P prints a line to the generated output. It converts each parameter to a // string following the same rules as fmt.Print. It never inserts spaces // between parameters. @@ -843,6 +916,7 @@ const ( filePackageField = 2 // package fileMessageField = 4 // message_type fileEnumField = 5 // enum_type + fileServiceField = 6 // service fileExtensionField = 7 // extension // field numbers in DescriptorProto messageFieldField = 2 // field @@ -852,6 +926,9 @@ const ( messageOneofField = 8 // oneof_decl // field numbers in EnumDescriptorProto enumValueField = 2 // value + // field numbers in ServiceDescriptorProto + serviceMethodField = 2 // method + serviceStreamField = 4 // stream ) // pathAppend appends elements to a location path. diff --git a/regenerate.bash b/regenerate.bash index d6c86524..af3e4876 100755 --- a/regenerate.bash +++ b/regenerate.bash @@ -11,24 +11,19 @@ trap 'rm -rf $tmpdir' EXIT mkdir -p $tmpdir/bin PATH=$tmpdir/bin:$PATH GOBIN=$tmpdir/bin go install ./cmd/protoc-gen-go - -# Public imports require at least Go 1.9. -supportTypeAliases="" -if go list -f '{{context.ReleaseTags}}' runtime | grep -q go1.9; then - supportTypeAliases=1 -fi +GOBIN=$tmpdir/bin go install ./cmd/protoc-gen-go-grpc # Generate various test protos. PROTO_DIRS=( cmd/protoc-gen-go/testdata + cmd/protoc-gen-go-grpc/testdata ) for dir in ${PROTO_DIRS[@]}; do for p in `find $dir -name "*.proto"`; do - if [[ $p == */import_public/* && ! $supportTypeAliases ]]; then - echo "# $p (skipped)" - continue; - fi echo "# $p" - protoc -I$dir --go_out=paths=source_relative:$dir $p + protoc -I$dir \ + --go_out=paths=source_relative:$dir \ + --go-grpc_out=paths=source_relative:$dir \ + $p done done diff --git a/test.bash b/test.bash index 8d25906d..7c625a5f 100755 --- a/test.bash +++ b/test.bash @@ -85,6 +85,7 @@ for GO_VERSION in ${GO_VERSIONS[@]}; do } go build ./... + go test ./... go test -race ./... go test -race -tags purego ./... go test -race -tags proto1_legacy ./...