From 5e208d9da2e7741250376004bc7c97c15c65b2f3 Mon Sep 17 00:00:00 2001 From: Lucio Franco Date: Thu, 15 Aug 2019 17:50:25 -0400 Subject: [PATCH] First pass at macro --- Cargo.toml | 1 + tonic-build/Cargo.toml | 13 ++ tonic-build/src/lib.rs | 95 ++++++++ tonic-examples/Cargo.toml | 3 + tonic-examples/build.rs | 8 +- tonic-examples/src/helloworld/server.rs | 22 +- tonic-macros/Cargo.toml | 10 +- tonic-macros/src/lib.rs | 298 +++++++++++++----------- tonic-macros/src/service.rs | 169 ++++++++++++++ tonic/src/lib.rs | 6 +- 10 files changed, 470 insertions(+), 155 deletions(-) create mode 100644 tonic-build/Cargo.toml create mode 100644 tonic-build/src/lib.rs create mode 100644 tonic-macros/src/service.rs diff --git a/Cargo.toml b/Cargo.toml index 664b3aa..76604db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "tonic", "tonic-macros", + "tonic-build", "tonic-examples", "tower-h2" ] diff --git a/tonic-build/Cargo.toml b/tonic-build/Cargo.toml new file mode 100644 index 0000000..db4fcb1 --- /dev/null +++ b/tonic-build/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tonic-build" +version = "0.1.0" +authors = ["Lucio Franco "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +prost-build = "0.5" +codegen = "0.1" +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } diff --git a/tonic-build/src/lib.rs b/tonic-build/src/lib.rs new file mode 100644 index 0000000..dad097f --- /dev/null +++ b/tonic-build/src/lib.rs @@ -0,0 +1,95 @@ +use prost_build::Config; +use serde::Serialize; +use std::{io, path}; + +pub fn compile_protos

(protos: &[P], includes: &[P]) -> io::Result<()> +where + P: AsRef, +{ + let mut config = Config::new(); + + config.service_generator(Box::new(ServiceGenerator {})); + + config.compile_protos(protos, includes) +} + +pub struct ServiceGenerator {} + +impl prost_build::ServiceGenerator for ServiceGenerator { + fn generate(&mut self, service: prost_build::Service, _buf: &mut String) { + let file = format!( + "{}/{}.{}.json", + std::env::var("OUT_DIR").unwrap(), + service.package, + service.name + ); + + let svc = Service { + name: service.name, + proto_name: service.proto_name, + package: service.package, + methods: service + .methods + .into_iter() + .map(|m| Method { + name: m.name, + proto_name: m.proto_name, + input_type: m.input_type, + output_type: m.output_type, + input_proto_type: m.input_proto_type, + output_proto_type: m.output_proto_type, + client_streaming: m.client_streaming, + server_streaming: m.server_streaming, + }) + .collect(), + }; + + let json = serde_json::to_string(&svc).unwrap(); + + std::fs::write(file, json).unwrap(); + } + + // fn finalize(&mut self, buf: &mut String) { + // let mut fmt = codegen::Formatter::new(buf); + // self.scope + // .fmt(&mut fmt) + // .expect("formatting root scope failed!"); + // self.scope = codegen::Scope::new(); + // } +} + +/// A service descriptor. +#[derive(Debug, Serialize)] +pub struct Service { + /// The service name in Rust style. + pub name: String, + /// The service name as it appears in the .proto file. + pub proto_name: String, + /// The package name as it appears in the .proto file. + pub package: String, + /// The service methods. + pub methods: Vec, +} + +/// A service method descriptor. +#[derive(Debug, Serialize)] +pub struct Method { + /// The name of the method in Rust style. + pub name: String, + /// The name of the method as it appears in the .proto file. + pub proto_name: String, + /// The input Rust type. + pub input_type: String, + /// The output Rust type. + pub output_type: String, + /// The input Protobuf type. + pub input_proto_type: String, + /// The output Protobuf type. + pub output_proto_type: String, + // /// The method options. + // pub options: prost_types::MethodOptions, + /// Identifies if client streams multiple client messages. + pub client_streaming: bool, + /// Identifies if server streams multiple server messages. + pub server_streaming: bool, +} diff --git a/tonic-examples/Cargo.toml b/tonic-examples/Cargo.toml index daf4c9d..f78ae81 100644 --- a/tonic-examples/Cargo.toml +++ b/tonic-examples/Cargo.toml @@ -22,3 +22,6 @@ tokio = "=0.2.0-alpha.1" prost = "0.5" prost-derive = "0.5" bytes = "0.4" + +[build-dependencies] +tonic-build = { path = "../tonic-build" } diff --git a/tonic-examples/build.rs b/tonic-examples/build.rs index f328e4d..b6fc145 100644 --- a/tonic-examples/build.rs +++ b/tonic-examples/build.rs @@ -1 +1,7 @@ -fn main() {} +fn main() { + tonic_build::compile_protos( + &["proto/helloworld/helloworld.proto"], + &["proto/helloworld"], + ) + .unwrap(); +} diff --git a/tonic-examples/src/helloworld/server.rs b/tonic-examples/src/helloworld/server.rs index 971fa15..8e91c72 100644 --- a/tonic-examples/src/helloworld/server.rs +++ b/tonic-examples/src/helloworld/server.rs @@ -5,18 +5,8 @@ use tokio::{timer::Delay, net::TcpListener}; use tonic::{Request, Response, Status}; use tower_h2::Server; -mod proto { - #[derive(Clone, PartialEq, prost::Message)] - pub struct HelloRequest { - #[prost(string, tag = "1")] - pub name: std::string::String, - } - /// The response message containing the greetings - #[derive(Clone, PartialEq, prost::Message)] - pub struct HelloReply { - #[prost(string, tag = "1")] - pub message: std::string::String, - } +pub mod hello_world { + include!(concat!(env!("OUT_DIR"), "/helloworld.rs")); } #[derive(Default, Clone)] @@ -24,9 +14,9 @@ pub struct MyGreeter { data: String, } -#[tonic::server(service = "helloworld.Greeter", proto = "proto")] +#[tonic::server(service = "helloworld.Greeter", proto = "hello_world")] impl MyGreeter { - pub async fn say_hello(&self, request: Request) -> Result, Status> { + pub async fn say_hello(&self, request: Request) -> Result, Status> { println!("Got a request: {:?}", request); let string = &self.data; @@ -38,7 +28,7 @@ impl MyGreeter { Delay::new(when).await; - let reply = HelloReply { + let reply = hello_world::HelloReply { message: "Zomg, it works!".into(), }; Ok(Response::new(reply)) @@ -51,7 +41,7 @@ async fn main() -> Result<(), Box> { let mut bind = TcpListener::bind(&addr)?; let greeter = MyGreeter::default(); - let mut server = Server::new(GrpcServer::new(greeter), Default::default()); + let mut server = Server::new(GreeterServer::new(greeter), Default::default()); while let Ok((sock, _addr)) = bind.accept().await { if let Err(e) = sock.set_nodelay(true) { diff --git a/tonic-macros/Cargo.toml b/tonic-macros/Cargo.toml index 5ae8cf4..1d12a2a 100644 --- a/tonic-macros/Cargo.toml +++ b/tonic-macros/Cargo.toml @@ -8,11 +8,11 @@ edition = "2018" proc-macro = true [dependencies] -syn = { version = "0.15", features = ["full"] } -quote = "0.6" -proc-macro2 = "0.4" -prost-build = "0.5" -tower-service = { git = "https://github.com/tower-rs/tower", branch = "std-future" } +syn = { version = "1.0", features = ["full"] } +quote = "1.0" +proc-macro2 = "1.0" +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } [dev-dependencies] tokio = "=0.2.0-alpha.1" diff --git a/tonic-macros/src/lib.rs b/tonic-macros/src/lib.rs index a1eb8f9..3da3720 100644 --- a/tonic-macros/src/lib.rs +++ b/tonic-macros/src/lib.rs @@ -2,155 +2,193 @@ #![recursion_limit = "256"] extern crate proc_macro; + +mod service; + use proc_macro::TokenStream; -use prost_build::{Comments, Method, Service}; -use quote::quote; -use syn::{ImplItem, ImplItemMethod, ItemImpl, Type}; +use serde::Deserialize; +use syn::{AttributeArgs, ItemImpl}; #[proc_macro_attribute] pub fn server(attr: TokenStream, item: TokenStream) -> TokenStream { - let service = load_service(attr); let mut original = item.clone(); - let ItemImpl { self_ty, items, .. } = syn::parse_macro_input!(item as ItemImpl); + let item = syn::parse_macro_input!(item as ItemImpl); + let args = syn::parse_macro_input!(attr as AttributeArgs); - let s = if let Type::Path(t) = *self_ty { - t.path.segments.iter().next().unwrap().clone() - } else { - panic!("wrong type!") - }; + let service = load_service(args); + let service_def = service::parse_service_impl(item, service); + let output = service::generate(service_def); - let mut m_ident = None; - for item in items { - if let ImplItem::Method(method) = item { - // println!("{:?}", method); + original.extend(TokenStream::from(output)); + original - let ImplItemMethod { sig, .. } = method; + // let mut original = item.clone(); + // let ItemImpl { self_ty, items, .. } = syn::parse_macro_input!(item as ItemImpl); - if sig.asyncness.is_some() { - let name = format!("{}", sig.ident); + // let mut m_ident = None; + // for item in items { + // if let ImplItem::Method(method) = item { + // // println!("{:?}", method); - if let Some(_method) = service.methods.iter().find(|method| method.name == name) { - // println!("found method!"); - m_ident = Some(sig.ident.clone()); - } - } - } - } + // let ImplItemMethod { sig, .. } = method; + + // if sig.asyncness.is_some() { + // let name = format!("{}", sig.ident); + + // if let Some(_method) = service.methods.iter().find(|method| method.name == name) { + // // println!("found method!"); + // m_ident = Some(sig.ident.clone()); + // } + // } + // } + // } // let service_name = service.proto_name.clone(); - let ts = quote! { - use tonic::_codegen; - use proto::*; + // let ts = quote! { + // use tonic::_codegen; + // use proto::*; - #[derive(Clone)] - pub struct GrpcServer { - inner: std::sync::Arc<#s>, - } + // #[derive(Clone)] + // pub struct GrpcServer { + // inner: std::sync::Arc<#s>, + // } - impl GrpcServer { - fn new(t: #s) -> Self { - Self { inner: std::sync::Arc::new(t) } + // impl GrpcServer { + // fn new(t: #s) -> Self { + // Self { inner: std::sync::Arc::new(t) } + // } + // } + + // impl _codegen::Service<()> for GrpcServer { + // type Response = Self; + // type Error = tonic::error::Never; + // type Future = _codegen::Ready>; + + // fn poll_ready(&mut self, _cx: &mut _codegen::Context<'_>) -> _codegen::Poll> { + // std::task::Poll::Ready(Ok(())) + // } + + // fn call(&mut self, _: ()) -> Self::Future { + // _codegen::ok(self.clone()) + // } + // } + + // impl _codegen::Service<_codegen::http::Request> for GrpcServer { + // type Response = _codegen::http::Response; + // type Error = tonic::error::Never; + // type Future = _codegen::ResponseFuture2; + + // fn poll_ready(&mut self, _cx: &mut _codegen::Context<'_>) -> _codegen::Poll> { + // Ok(()).into() + // } + + // fn call(&mut self, request: _codegen::http::Request) -> Self::Future { + // let inner = self.inner.clone(); + + // match request.uri().path() { + // "/helloworld.Greeter/SayHello" => { + // use tonic::_codegen::*; + // use tonic::*; + + // pub struct SayHello(pub std::sync::Arc<#s>); + + // impl tonic::server::UnaryService for SayHello { + // type Response = HelloReply; + // type Future = Pin, Status>> + Send + 'static>>; + + // fn call(&mut self, request: Request) -> Self::Future { + // let inner = self.0.clone(); + // let fut = async move { + // inner.#m_ident(request).await + // }; + // Box::pin(fut) + // } + // } + + // let inner = self.inner.clone(); + + // let fut = async move { + // let method = SayHello(inner); + // let codec = tonic::codec::ProstCodec::new(); + // let mut grpc = tonic::server::Grpc::new(codec); + // let res = grpc.unary(method, request).await; + // Ok(res) + // }; + + // Box::pin(fut) + // }, + // _ => unimplemented!("use grpc unimplemented") + // } + // } + // } + + // }; +} + +fn load_service(attr: AttributeArgs) -> Service { + use syn::{Lit, Meta, MetaNameValue, NestedMeta}; + + let service = attr + .iter() + .filter_map(|i| match i { + NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. })) + if path.segments.first().unwrap().ident == "service" => + { + Some(lit.clone()) } - } - - impl _codegen::Service<()> for GrpcServer { - type Response = Self; - type Error = tonic::error::Never; - type Future = _codegen::Ready>; - - fn poll_ready(&mut self, _cx: &mut _codegen::Context<'_>) -> _codegen::Poll> { - std::task::Poll::Ready(Ok(())) - } - - fn call(&mut self, _: ()) -> Self::Future { - _codegen::ok(self.clone()) - } - } - - impl _codegen::Service<_codegen::http::Request> for GrpcServer { - type Response = _codegen::http::Response; - type Error = tonic::error::Never; - type Future = _codegen::ResponseFuture2; - - fn poll_ready(&mut self, _cx: &mut _codegen::Context<'_>) -> _codegen::Poll> { - Ok(()).into() - } - - fn call(&mut self, request: _codegen::http::Request) -> Self::Future { - let inner = self.inner.clone(); - - match request.uri().path() { - "/helloworld.Greeter/SayHello" => { - use tonic::_codegen::*; - use tonic::*; - - pub struct SayHello(pub std::sync::Arc<#s>); - - impl tonic::server::UnaryService for SayHello { - type Response = HelloReply; - type Future = Pin, Status>> + Send + 'static>>; - - fn call(&mut self, request: Request) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { - inner.#m_ident(request).await - }; - Box::pin(fut) - } - } - - let inner = self.inner.clone(); - - - let fut = async move { - let method = SayHello(inner); - let codec = tonic::codec::ProstCodec::new(); - let mut grpc = tonic::server::Grpc::new(codec); - let res = grpc.unary(method, request).await; - Ok(res) - }; - - Box::pin(fut) - }, - _ => unimplemented!("use grpc unimplemented") - } - } - } - + _ => None, + }) + .next(); + let service_name = match service { + Some(Lit::Str(s)) => s.value(), + Some(_) => panic!("expected a literal string"), + None => panic!("expected a `service = \"package.Service\" attribute"), }; - original.extend(TokenStream::from(ts)); - original + let file = format!( + "{}/{}.json", + std::env::var("OUT_DIR").unwrap(), + service_name + ); + let json = std::fs::read_to_string(file).unwrap(); + + serde_json::from_str(&json).unwrap() } -fn load_service(_attr: TokenStream) -> Service { - Service { - name: "Greeter".into(), - proto_name: "greeter".into(), - package: "helloworld".into(), - comments: Comments { - leading_detached: Vec::new(), - leading: Vec::new(), - trailing: Vec::new(), - }, - methods: vec![Method { - name: "say_hello".into(), - proto_name: "SayHello".into(), - comments: Comments { - leading_detached: Vec::new(), - leading: Vec::new(), - trailing: Vec::new(), - }, - input_type: "HelloRequest".into(), - output_type: "HelloResponse".into(), - input_proto_type: "HelloRequest".into(), - output_proto_type: "HelloResponse".into(), - options: Default::default(), - client_streaming: false, - server_streaming: false, - }], - options: Default::default(), - } +/// A service descriptor. +#[derive(Debug, Deserialize)] +pub(crate) struct Service { + /// The service name in Rust style. + pub name: String, + /// The service name as it appears in the .proto file. + pub proto_name: String, + /// The package name as it appears in the .proto file. + pub package: String, + /// The service methods. + pub methods: Vec, +} + +/// A service method descriptor. +#[derive(Debug, Deserialize)] +pub(crate) struct Method { + /// The name of the method in Rust style. + pub name: String, + /// The name of the method as it appears in the .proto file. + pub proto_name: String, + /// The input Rust type. + pub input_type: String, + /// The output Rust type. + pub output_type: String, + /// The input Protobuf type. + pub input_proto_type: String, + /// The output Protobuf type. + pub output_proto_type: String, + // /// The method options. + // pub options: prost_types::MethodOptions, + /// Identifies if client streams multiple client messages. + pub client_streaming: bool, + /// Identifies if server streams multiple server messages. + pub server_streaming: bool, } diff --git a/tonic-macros/src/service.rs b/tonic-macros/src/service.rs new file mode 100644 index 0000000..1e894b3 --- /dev/null +++ b/tonic-macros/src/service.rs @@ -0,0 +1,169 @@ +use crate::{Method, Service}; +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::{Ident, ImplItem, ImplItemMethod, ItemImpl, Lit, LitStr, Path, Type}; + +#[derive(Debug)] +pub struct ServiceDef { + name: Path, + name_str: String, + package: String, + proto_name: String, + methods: Vec<(Method, Ident)>, +} + +pub(crate) fn parse_service_impl(item: ItemImpl, mut service: Service) -> ServiceDef { + let ItemImpl { self_ty, items, .. } = item; + + let name = if let Type::Path(t) = *self_ty { + t.path.clone() + } else { + panic!("wrong type!") + }; + + let mut methods = Vec::new(); + + for item in items { + if let ImplItem::Method(method) = item { + let ImplItemMethod { sig, .. } = method; + + if sig.asyncness.is_some() { + let name = format!("{}", sig.ident); + + if let Some((i, _)) = service + .methods + .iter() + .enumerate() + .find(|(_, method)| method.name == name) + { + let method = service.methods.remove(i); + methods.push((method, sig.ident)); + } + } + } + } + + ServiceDef { + name, + name_str: service.name, + package: service.package, + proto_name: service.proto_name, + methods, + } +} + +pub(crate) fn generate(service: ServiceDef) -> TokenStream { + let service_server = Ident::new(&format!("{}Server", service.name_str), Span::call_site()); + + let service_impl = service.name.clone(); + let methods = generate_methods(&service); + + quote! { + use tonic::_codegen::*; + + // TODO: impl debug + #[derive(Clone)] + pub struct #service_server { + inner: std::sync::Arc<#service_impl>, + } + + impl #service_server { + pub fn new(t: #service_impl) -> Self { + let inner = std::sync::Arc::new(t); + Self { inner } + } + } + + impl Service<()> for #service_server { + type Response = Self; + type Error = tonic::error::Never; + type Future = Ready>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, _: ()) -> Self::Future { + ok(self.clone()) + } + } + + impl Service> for #service_server { + type Response = http::Response; + type Error = tonic::error::Never; + type Future = BoxFuture; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: http::Request) -> Self::Future { + let inner = self.inner.clone(); + + match req.uri().path() { + #methods + + _ => unimplemented!("use grpc unimplemented"), + } + } + } + } +} + +fn generate_methods(service: &ServiceDef) -> TokenStream { + let mut stream = TokenStream::new(); + + for (method, ident) in &service.methods { + let path = format!( + "/{}.{}/{}", + service.package, service.proto_name, method.proto_name + ); + let method_path = Lit::Str(LitStr::new(&path, Span::call_site())); + + let method_stream = generate_unary(method, ident.clone(), service.name.clone()); + + let method = quote! { + #method_path => { + #method_stream + } + }; + stream.extend(method); + } + + stream +} + +fn generate_unary(method: &Method, method_ident: Ident, service_impl: Path) -> TokenStream { + let service_ident = Ident::new(&method.proto_name, Span::call_site()); + + let request: Path = syn::parse_str(&format!("hello_world::{}", method.input_type)).unwrap(); + let response: Path = syn::parse_str(&format!("hello_world::{}", method.output_type)).unwrap(); + + quote! { + struct #service_ident(pub std::sync::Arc<#service_impl>); + + impl tonic::server::UnaryService<#request> for #service_ident { + type Response =#response; + type Future = BoxFuture, tonic::Status>; + + fn call(&mut self, request: tonic::Request<#request>) -> Self::Future { + let inner = self.0.clone(); + let fut = async move { + inner.#method_ident(request).await + }; + Box::pin(fut) + } + } + + let inner = self.inner.clone(); + let fut = async move { + let method = #service_ident(inner); + let codec = tonic::codec::ProstCodec::new(); + let mut grpc = tonic::server::Grpc::new(codec); + let res = grpc.unary(method, req).await; + Ok(res) + }; + + Box::pin(fut) + } +} diff --git a/tonic/src/lib.rs b/tonic/src/lib.rs index fc466ce..1868840 100644 --- a/tonic/src/lib.rs +++ b/tonic/src/lib.rs @@ -14,6 +14,7 @@ mod request; mod response; mod status; +pub use body::{BoxAsyncBody, BoxBody}; pub use request::Request; pub use response::Response; pub use status::{Code, Status}; @@ -39,9 +40,8 @@ pub mod _codegen { pub use std::pin::Pin; pub use std::task::{Context, Poll}; pub use tower_service::Service; - pub type ResponseFuture = - self::Pin> + Send + 'static>>; - pub type ResponseFuture2 = + + pub type BoxFuture = self::Pin> + Send + 'static>>; pub mod http {