/* * * Copyright 2015 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ #include <cctype> #include <map> #include <sstream> #include <vector> #include "src/compiler/config.h" #include "src/compiler/csharp_generator.h" #include "src/compiler/csharp_generator_helpers.h" using google::protobuf::compiler::csharp::GetClassName; using google::protobuf::compiler::csharp::GetFileNamespace; using google::protobuf::compiler::csharp::GetReflectionClassName; using grpc::protobuf::Descriptor; using grpc::protobuf::FileDescriptor; using grpc::protobuf::MethodDescriptor; using grpc::protobuf::ServiceDescriptor; using grpc::protobuf::io::Printer; using grpc::protobuf::io::StringOutputStream; using grpc_generator::GetMethodType; using grpc_generator::METHODTYPE_BIDI_STREAMING; using grpc_generator::METHODTYPE_CLIENT_STREAMING; using grpc_generator::METHODTYPE_NO_STREAMING; using grpc_generator::METHODTYPE_SERVER_STREAMING; using grpc_generator::MethodType; using grpc_generator::StringReplace; using std::map; using std::vector; namespace grpc_csharp_generator { namespace { // This function is a massaged version of // https://github.com/google/protobuf/blob/master/src/google/protobuf/compiler/csharp/csharp_doc_comment.cc // Currently, we cannot easily reuse the functionality as // google/protobuf/compiler/csharp/csharp_doc_comment.h is not a public header. // TODO(jtattermusch): reuse the functionality from google/protobuf. bool GenerateDocCommentBodyImpl(grpc::protobuf::io::Printer* printer, grpc::protobuf::SourceLocation location) { grpc::string comments = location.leading_comments.empty() ? location.trailing_comments : location.leading_comments; if (comments.empty()) { return false; } // XML escaping... no need for apostrophes etc as the whole text is going to // be a child // node of a summary element, not part of an attribute. comments = grpc_generator::StringReplace(comments, "&", "&", true); comments = grpc_generator::StringReplace(comments, "<", "<", true); std::vector<grpc::string> lines; grpc_generator::Split(comments, '\n', &lines); // TODO: We really should work out which part to put in the summary and which // to put in the remarks... // but that needs to be part of a bigger effort to understand the markdown // better anyway. printer->Print("/// <summary>\n"); bool last_was_empty = false; // We squash multiple blank lines down to one, and remove any trailing blank // lines. We need // to preserve the blank lines themselves, as this is relevant in the // markdown. // Note that we can't remove leading or trailing whitespace as *that's* // relevant in markdown too. // (We don't skip "just whitespace" lines, either.) for (std::vector<grpc::string>::iterator it = lines.begin(); it != lines.end(); ++it) { grpc::string line = *it; if (line.empty()) { last_was_empty = true; } else { if (last_was_empty) { printer->Print("///\n"); } last_was_empty = false; printer->Print("///$line$\n", "line", *it); } } printer->Print("/// </summary>\n"); return true; } template <typename DescriptorType> bool GenerateDocCommentBody(grpc::protobuf::io::Printer* printer, const DescriptorType* descriptor) { grpc::protobuf::SourceLocation location; if (!descriptor->GetSourceLocation(&location)) { return false; } return GenerateDocCommentBodyImpl(printer, location); } void GenerateDocCommentServerMethod(grpc::protobuf::io::Printer* printer, const MethodDescriptor* method) { if (GenerateDocCommentBody(printer, method)) { if (method->client_streaming()) { printer->Print( "/// <param name=\"requestStream\">Used for reading requests from " "the client.</param>\n"); } else { printer->Print( "/// <param name=\"request\">The request received from the " "client.</param>\n"); } if (method->server_streaming()) { printer->Print( "/// <param name=\"responseStream\">Used for sending responses back " "to the client.</param>\n"); } printer->Print( "/// <param name=\"context\">The context of the server-side call " "handler being invoked.</param>\n"); if (method->server_streaming()) { printer->Print( "/// <returns>A task indicating completion of the " "handler.</returns>\n"); } else { printer->Print( "/// <returns>The response to send back to the client (wrapped by a " "task).</returns>\n"); } } } void GenerateDocCommentClientMethod(grpc::protobuf::io::Printer* printer, const MethodDescriptor* method, bool is_sync, bool use_call_options) { if (GenerateDocCommentBody(printer, method)) { if (!method->client_streaming()) { printer->Print( "/// <param name=\"request\">The request to send to the " "server.</param>\n"); } if (!use_call_options) { printer->Print( "/// <param name=\"headers\">The initial metadata to send with the " "call. This parameter is optional.</param>\n"); printer->Print( "/// <param name=\"deadline\">An optional deadline for the call. The " "call will be cancelled if deadline is hit.</param>\n"); printer->Print( "/// <param name=\"cancellationToken\">An optional token for " "canceling the call.</param>\n"); } else { printer->Print( "/// <param name=\"options\">The options for the call.</param>\n"); } if (is_sync) { printer->Print( "/// <returns>The response received from the server.</returns>\n"); } else { printer->Print("/// <returns>The call object.</returns>\n"); } } } std::string GetServiceClassName(const ServiceDescriptor* service) { return service->name(); } std::string GetClientClassName(const ServiceDescriptor* service) { return service->name() + "Client"; } std::string GetServerClassName(const ServiceDescriptor* service) { return service->name() + "Base"; } std::string GetCSharpMethodType(MethodType method_type) { switch (method_type) { case METHODTYPE_NO_STREAMING: return "grpc::MethodType.Unary"; case METHODTYPE_CLIENT_STREAMING: return "grpc::MethodType.ClientStreaming"; case METHODTYPE_SERVER_STREAMING: return "grpc::MethodType.ServerStreaming"; case METHODTYPE_BIDI_STREAMING: return "grpc::MethodType.DuplexStreaming"; } GOOGLE_LOG(FATAL) << "Can't get here."; return ""; } std::string GetServiceNameFieldName() { return "__ServiceName"; } std::string GetMarshallerFieldName(const Descriptor* message) { return "__Marshaller_" + grpc_generator::StringReplace(message->full_name(), ".", "_", true); } std::string GetMethodFieldName(const MethodDescriptor* method) { return "__Method_" + method->name(); } std::string GetMethodRequestParamMaybe(const MethodDescriptor* method, bool invocation_param = false) { if (method->client_streaming()) { return ""; } if (invocation_param) { return "request, "; } return GetClassName(method->input_type()) + " request, "; } std::string GetAccessLevel(bool internal_access) { return internal_access ? "internal" : "public"; } std::string GetMethodReturnTypeClient(const MethodDescriptor* method) { switch (GetMethodType(method)) { case METHODTYPE_NO_STREAMING: return "grpc::AsyncUnaryCall<" + GetClassName(method->output_type()) + ">"; case METHODTYPE_CLIENT_STREAMING: return "grpc::AsyncClientStreamingCall<" + GetClassName(method->input_type()) + ", " + GetClassName(method->output_type()) + ">"; case METHODTYPE_SERVER_STREAMING: return "grpc::AsyncServerStreamingCall<" + GetClassName(method->output_type()) + ">"; case METHODTYPE_BIDI_STREAMING: return "grpc::AsyncDuplexStreamingCall<" + GetClassName(method->input_type()) + ", " + GetClassName(method->output_type()) + ">"; } GOOGLE_LOG(FATAL) << "Can't get here."; return ""; } std::string GetMethodRequestParamServer(const MethodDescriptor* method) { switch (GetMethodType(method)) { case METHODTYPE_NO_STREAMING: case METHODTYPE_SERVER_STREAMING: return GetClassName(method->input_type()) + " request"; case METHODTYPE_CLIENT_STREAMING: case METHODTYPE_BIDI_STREAMING: return "grpc::IAsyncStreamReader<" + GetClassName(method->input_type()) + "> requestStream"; } GOOGLE_LOG(FATAL) << "Can't get here."; return ""; } std::string GetMethodReturnTypeServer(const MethodDescriptor* method) { switch (GetMethodType(method)) { case METHODTYPE_NO_STREAMING: case METHODTYPE_CLIENT_STREAMING: return "global::System.Threading.Tasks.Task<" + GetClassName(method->output_type()) + ">"; case METHODTYPE_SERVER_STREAMING: case METHODTYPE_BIDI_STREAMING: return "global::System.Threading.Tasks.Task"; } GOOGLE_LOG(FATAL) << "Can't get here."; return ""; } std::string GetMethodResponseStreamMaybe(const MethodDescriptor* method) { switch (GetMethodType(method)) { case METHODTYPE_NO_STREAMING: case METHODTYPE_CLIENT_STREAMING: return ""; case METHODTYPE_SERVER_STREAMING: case METHODTYPE_BIDI_STREAMING: return ", grpc::IServerStreamWriter<" + GetClassName(method->output_type()) + "> responseStream"; } GOOGLE_LOG(FATAL) << "Can't get here."; return ""; } // Gets vector of all messages used as input or output types. std::vector<const Descriptor*> GetUsedMessages( const ServiceDescriptor* service) { std::set<const Descriptor*> descriptor_set; std::vector<const Descriptor*> result; // vector is to maintain stable ordering for (int i = 0; i < service->method_count(); i++) { const MethodDescriptor* method = service->method(i); if (descriptor_set.find(method->input_type()) == descriptor_set.end()) { descriptor_set.insert(method->input_type()); result.push_back(method->input_type()); } if (descriptor_set.find(method->output_type()) == descriptor_set.end()) { descriptor_set.insert(method->output_type()); result.push_back(method->output_type()); } } return result; } void GenerateMarshallerFields(Printer* out, const ServiceDescriptor* service) { std::vector<const Descriptor*> used_messages = GetUsedMessages(service); for (size_t i = 0; i < used_messages.size(); i++) { const Descriptor* message = used_messages[i]; out->Print( "static readonly grpc::Marshaller<$type$> $fieldname$ = " "grpc::Marshallers.Create((arg) => " "global::Google.Protobuf.MessageExtensions.ToByteArray(arg), " "$type$.Parser.ParseFrom);\n", "fieldname", GetMarshallerFieldName(message), "type", GetClassName(message)); } out->Print("\n"); } void GenerateStaticMethodField(Printer* out, const MethodDescriptor* method) { out->Print( "static readonly grpc::Method<$request$, $response$> $fieldname$ = new " "grpc::Method<$request$, $response$>(\n", "fieldname", GetMethodFieldName(method), "request", GetClassName(method->input_type()), "response", GetClassName(method->output_type())); out->Indent(); out->Indent(); out->Print("$methodtype$,\n", "methodtype", GetCSharpMethodType(GetMethodType(method))); out->Print("$servicenamefield$,\n", "servicenamefield", GetServiceNameFieldName()); out->Print("\"$methodname$\",\n", "methodname", method->name()); out->Print("$requestmarshaller$,\n", "requestmarshaller", GetMarshallerFieldName(method->input_type())); out->Print("$responsemarshaller$);\n", "responsemarshaller", GetMarshallerFieldName(method->output_type())); out->Print("\n"); out->Outdent(); out->Outdent(); } void GenerateServiceDescriptorProperty(Printer* out, const ServiceDescriptor* service) { std::ostringstream index; index << service->index(); out->Print("/// <summary>Service descriptor</summary>\n"); out->Print( "public static global::Google.Protobuf.Reflection.ServiceDescriptor " "Descriptor\n"); out->Print("{\n"); out->Print(" get { return $umbrella$.Descriptor.Services[$index$]; }\n", "umbrella", GetReflectionClassName(service->file()), "index", index.str()); out->Print("}\n"); out->Print("\n"); } void GenerateServerClass(Printer* out, const ServiceDescriptor* service) { out->Print( "/// <summary>Base class for server-side implementations of " "$servicename$</summary>\n", "servicename", GetServiceClassName(service)); out->Print("public abstract partial class $name$\n", "name", GetServerClassName(service)); out->Print("{\n"); out->Indent(); for (int i = 0; i < service->method_count(); i++) { const MethodDescriptor* method = service->method(i); GenerateDocCommentServerMethod(out, method); out->Print( "public virtual $returntype$ " "$methodname$($request$$response_stream_maybe$, " "grpc::ServerCallContext context)\n", "methodname", method->name(), "returntype", GetMethodReturnTypeServer(method), "request", GetMethodRequestParamServer(method), "response_stream_maybe", GetMethodResponseStreamMaybe(method)); out->Print("{\n"); out->Indent(); out->Print( "throw new grpc::RpcException(" "new grpc::Status(grpc::StatusCode.Unimplemented, \"\"));\n"); out->Outdent(); out->Print("}\n\n"); } out->Outdent(); out->Print("}\n"); out->Print("\n"); } void GenerateClientStub(Printer* out, const ServiceDescriptor* service) { out->Print("/// <summary>Client for $servicename$</summary>\n", "servicename", GetServiceClassName(service)); out->Print("public partial class $name$ : grpc::ClientBase<$name$>\n", "name", GetClientClassName(service)); out->Print("{\n"); out->Indent(); // constructors out->Print( "/// <summary>Creates a new client for $servicename$</summary>\n" "/// <param name=\"channel\">The channel to use to make remote " "calls.</param>\n", "servicename", GetServiceClassName(service)); out->Print("public $name$(grpc::Channel channel) : base(channel)\n", "name", GetClientClassName(service)); out->Print("{\n"); out->Print("}\n"); out->Print( "/// <summary>Creates a new client for $servicename$ that uses a custom " "<c>CallInvoker</c>.</summary>\n" "/// <param name=\"callInvoker\">The callInvoker to use to make remote " "calls.</param>\n", "servicename", GetServiceClassName(service)); out->Print( "public $name$(grpc::CallInvoker callInvoker) : base(callInvoker)\n", "name", GetClientClassName(service)); out->Print("{\n"); out->Print("}\n"); out->Print( "/// <summary>Protected parameterless constructor to allow creation" " of test doubles.</summary>\n"); out->Print("protected $name$() : base()\n", "name", GetClientClassName(service)); out->Print("{\n"); out->Print("}\n"); out->Print( "/// <summary>Protected constructor to allow creation of configured " "clients.</summary>\n" "/// <param name=\"configuration\">The client configuration.</param>\n"); out->Print( "protected $name$(ClientBaseConfiguration configuration)" " : base(configuration)\n", "name", GetClientClassName(service)); out->Print("{\n"); out->Print("}\n\n"); for (int i = 0; i < service->method_count(); i++) { const MethodDescriptor* method = service->method(i); MethodType method_type = GetMethodType(method); if (method_type == METHODTYPE_NO_STREAMING) { // unary calls have an extra synchronous stub method GenerateDocCommentClientMethod(out, method, true, false); out->Print( "public virtual $response$ $methodname$($request$ request, " "grpc::Metadata " "headers = null, global::System.DateTime? deadline = null, " "global::System.Threading.CancellationToken " "cancellationToken = " "default(global::System.Threading.CancellationToken))\n", "methodname", method->name(), "request", GetClassName(method->input_type()), "response", GetClassName(method->output_type())); out->Print("{\n"); out->Indent(); out->Print( "return $methodname$(request, new grpc::CallOptions(headers, " "deadline, " "cancellationToken));\n", "methodname", method->name()); out->Outdent(); out->Print("}\n"); // overload taking CallOptions as a param GenerateDocCommentClientMethod(out, method, true, true); out->Print( "public virtual $response$ $methodname$($request$ request, " "grpc::CallOptions options)\n", "methodname", method->name(), "request", GetClassName(method->input_type()), "response", GetClassName(method->output_type())); out->Print("{\n"); out->Indent(); out->Print( "return CallInvoker.BlockingUnaryCall($methodfield$, null, options, " "request);\n", "methodfield", GetMethodFieldName(method)); out->Outdent(); out->Print("}\n"); } std::string method_name = method->name(); if (method_type == METHODTYPE_NO_STREAMING) { method_name += "Async"; // prevent name clash with synchronous method. } GenerateDocCommentClientMethod(out, method, false, false); out->Print( "public virtual $returntype$ " "$methodname$($request_maybe$grpc::Metadata " "headers = null, global::System.DateTime? deadline = null, " "global::System.Threading.CancellationToken " "cancellationToken = " "default(global::System.Threading.CancellationToken))\n", "methodname", method_name, "request_maybe", GetMethodRequestParamMaybe(method), "returntype", GetMethodReturnTypeClient(method)); out->Print("{\n"); out->Indent(); out->Print( "return $methodname$($request_maybe$new grpc::CallOptions(headers, " "deadline, " "cancellationToken));\n", "methodname", method_name, "request_maybe", GetMethodRequestParamMaybe(method, true)); out->Outdent(); out->Print("}\n"); // overload taking CallOptions as a param GenerateDocCommentClientMethod(out, method, false, true); out->Print( "public virtual $returntype$ " "$methodname$($request_maybe$grpc::CallOptions " "options)\n", "methodname", method_name, "request_maybe", GetMethodRequestParamMaybe(method), "returntype", GetMethodReturnTypeClient(method)); out->Print("{\n"); out->Indent(); switch (GetMethodType(method)) { case METHODTYPE_NO_STREAMING: out->Print( "return CallInvoker.AsyncUnaryCall($methodfield$, null, options, " "request);\n", "methodfield", GetMethodFieldName(method)); break; case METHODTYPE_CLIENT_STREAMING: out->Print( "return CallInvoker.AsyncClientStreamingCall($methodfield$, null, " "options);\n", "methodfield", GetMethodFieldName(method)); break; case METHODTYPE_SERVER_STREAMING: out->Print( "return CallInvoker.AsyncServerStreamingCall($methodfield$, null, " "options, request);\n", "methodfield", GetMethodFieldName(method)); break; case METHODTYPE_BIDI_STREAMING: out->Print( "return CallInvoker.AsyncDuplexStreamingCall($methodfield$, null, " "options);\n", "methodfield", GetMethodFieldName(method)); break; default: GOOGLE_LOG(FATAL) << "Can't get here."; } out->Outdent(); out->Print("}\n"); } // override NewInstance method out->Print( "/// <summary>Creates a new instance of client from given " "<c>ClientBaseConfiguration</c>.</summary>\n"); out->Print( "protected override $name$ NewInstance(ClientBaseConfiguration " "configuration)\n", "name", GetClientClassName(service)); out->Print("{\n"); out->Indent(); out->Print("return new $name$(configuration);\n", "name", GetClientClassName(service)); out->Outdent(); out->Print("}\n"); out->Outdent(); out->Print("}\n"); out->Print("\n"); } void GenerateBindServiceMethod(Printer* out, const ServiceDescriptor* service) { out->Print( "/// <summary>Creates service definition that can be registered with a " "server</summary>\n"); out->Print( "/// <param name=\"serviceImpl\">An object implementing the server-side" " handling logic.</param>\n"); out->Print( "public static grpc::ServerServiceDefinition BindService($implclass$ " "serviceImpl)\n", "implclass", GetServerClassName(service)); out->Print("{\n"); out->Indent(); out->Print("return grpc::ServerServiceDefinition.CreateBuilder()"); out->Indent(); out->Indent(); for (int i = 0; i < service->method_count(); i++) { const MethodDescriptor* method = service->method(i); out->Print("\n.AddMethod($methodfield$, serviceImpl.$methodname$)", "methodfield", GetMethodFieldName(method), "methodname", method->name()); } out->Print(".Build();\n"); out->Outdent(); out->Outdent(); out->Outdent(); out->Print("}\n"); out->Print("\n"); } void GenerateService(Printer* out, const ServiceDescriptor* service, bool generate_client, bool generate_server, bool internal_access) { GenerateDocCommentBody(out, service); out->Print("$access_level$ static partial class $classname$\n", "access_level", GetAccessLevel(internal_access), "classname", GetServiceClassName(service)); out->Print("{\n"); out->Indent(); out->Print("static readonly string $servicenamefield$ = \"$servicename$\";\n", "servicenamefield", GetServiceNameFieldName(), "servicename", service->full_name()); out->Print("\n"); GenerateMarshallerFields(out, service); for (int i = 0; i < service->method_count(); i++) { GenerateStaticMethodField(out, service->method(i)); } GenerateServiceDescriptorProperty(out, service); if (generate_server) { GenerateServerClass(out, service); } if (generate_client) { GenerateClientStub(out, service); } if (generate_server) { GenerateBindServiceMethod(out, service); } out->Outdent(); out->Print("}\n"); } } // anonymous namespace grpc::string GetServices(const FileDescriptor* file, bool generate_client, bool generate_server, bool internal_access) { grpc::string output; { // Scope the output stream so it closes and finalizes output to the string. StringOutputStream output_stream(&output); Printer out(&output_stream, '$'); // Don't write out any output if there no services, to avoid empty service // files being generated for proto files that don't declare any. if (file->service_count() == 0) { return output; } // Write out a file header. out.Print("// <auto-generated>\n"); out.Print( "// Generated by the protocol buffer compiler. DO NOT EDIT!\n"); out.Print("// source: $filename$\n", "filename", file->name()); out.Print("// </auto-generated>\n"); // use C++ style as there are no file-level XML comments in .NET grpc::string leading_comments = GetCsharpComments(file, true); if (!leading_comments.empty()) { out.Print("// Original file comments:\n"); out.PrintRaw(leading_comments.c_str()); } out.Print("#pragma warning disable 0414, 1591\n"); out.Print("#region Designer generated code\n"); out.Print("\n"); out.Print("using grpc = global::Grpc.Core;\n"); out.Print("\n"); grpc::string file_namespace = GetFileNamespace(file); if (file_namespace != "") { out.Print("namespace $namespace$ {\n", "namespace", file_namespace); out.Indent(); } for (int i = 0; i < file->service_count(); i++) { GenerateService(&out, file->service(i), generate_client, generate_server, internal_access); } if (file_namespace != "") { out.Outdent(); out.Print("}\n"); } out.Print("#endregion\n"); } return output; } } // namespace grpc_csharp_generator