From 2246a59863e78dba8c586c6423b2020d52924d69 Mon Sep 17 00:00:00 2001 From: Jiayu Ye Date: Wed, 11 Dec 2019 13:03:48 +0800 Subject: [PATCH] feat(core/binding-macro): Add `read` and `write` proc-macro. (#49) --- Cargo.lock | 8 ++ Cargo.toml | 2 + core/binding-macro/Cargo.toml | 11 ++ core/binding-macro/src/lib.rs | 75 +++++++++++- core/binding-macro/src/read_write.rs | 173 +++++++++++++++++++++++++++ core/binding-macro/tests/mod.rs | 79 ++++++++++++ 6 files changed, 342 insertions(+), 6 deletions(-) create mode 100644 core/binding-macro/src/read_write.rs create mode 100644 core/binding-macro/tests/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 3bc980637..a7a1b0d7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -503,6 +503,14 @@ dependencies = [ [[package]] name = "core-binding-macro" version = "0.1.0" +dependencies = [ + "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", + "json 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "protocol 0.1.0", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] name = "core-consensus" diff --git a/Cargo.toml b/Cargo.toml index 9687fa37c..89e267c8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,8 @@ members = [ "core/mempool", "core/network", "core/storage", + "core/binding", + "core/binding-macro", "protocol", ] diff --git a/core/binding-macro/Cargo.toml b/core/binding-macro/Cargo.toml index 4daa5ba34..2c3128fa3 100644 --- a/core/binding-macro/Cargo.toml +++ b/core/binding-macro/Cargo.toml @@ -6,4 +6,15 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +proc-macro = true + [dependencies] +syn = { version = "1.0", features = ["full"] } +proc-macro2 = "1.0" +quote = "1.0" + +[dev-dependencies] +protocol = { path = "../../protocol" } +json = "0.12" +bytes = "0.4" diff --git a/core/binding-macro/src/lib.rs b/core/binding-macro/src/lib.rs index 31e1bb209..eb246ad83 100644 --- a/core/binding-macro/src/lib.rs +++ b/core/binding-macro/src/lib.rs @@ -1,7 +1,70 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } +extern crate proc_macro; + +mod read_write; + +use proc_macro::TokenStream; + +use crate::read_write::verify_read_or_write; + +// `#[read]` marks a service method as readable. +// +// Methods marked with this macro will have: +// Methods with this macro allow access (readable) from outside (RPC or other +// services). +// +// - Verification +// 1. Is it a struct method marked with #[service]? +// 2. Is visibility private? +// 3. Does function generics constrain `fn f`? +// 4. Parameter signature contains `&self and ctx:Context`? +// 5. Is the return value `ProtocolResult `? +// +// example: +// +// struct Service; +// #[service] +// impl Service { +// #[read] +// fn test_read_fn( +// &self, +// _ctx: Context, +// ) -> ProtocolResult { +// Ok(JsonValue::Null) +// } +// } +#[proc_macro_attribute] +pub fn read(_: TokenStream, item: TokenStream) -> TokenStream { + verify_read_or_write(item, false) +} + +// `#[write]` marks a service method as writeable. +// +// Methods marked with this macro will have: +// - Accessibility +// Methods with this macro allow access (writeable) from outside (RPC or other +// services). +// +// - Verification +// 1. Is it a struct method marked with #[service]? +// 2. Is visibility private? +// 3. Does function generics constrain `fn f`? +// 4. Parameter signature contains `&mut self and ctx:Context`? +// 5. Is the return value `ProtocolResult `? +// +// example: +// +// struct Service; +// #[service] +// impl Service { +// #[write] +// fn test_write_fn( +// &mut self, +// _ctx: Context, +// ) -> ProtocolResult { +// Ok(JsonValue::Null) +// } +// } +#[proc_macro_attribute] +pub fn write(_: TokenStream, item: TokenStream) -> TokenStream { + verify_read_or_write(item, true) } diff --git a/core/binding-macro/src/read_write.rs b/core/binding-macro/src/read_write.rs new file mode 100644 index 000000000..494f2ab3f --- /dev/null +++ b/core/binding-macro/src/read_write.rs @@ -0,0 +1,173 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::punctuated::Punctuated; +use syn::{ + parse_macro_input, FnArg, GenericArgument, GenericParam, Generics, ImplItemMethod, Path, + PathArguments, ReturnType, Token, Type, TypeParamBound, Visibility, +}; + +pub fn verify_read_or_write(item: TokenStream, mutable: bool) -> TokenStream { + let method_item = parse_macro_input!(item as ImplItemMethod); + + let visibility = &method_item.vis; + let inputs = &method_item.sig.inputs; + let generics = &method_item.sig.generics; + let ret_type = &method_item.sig.output; + + // TODO(@yejiayu): verify #[service] + + verify_visibiity(visibility); + + verify_inputs(inputs, generics, mutable); + + verify_ret_type(ret_type); + + TokenStream::from(quote! {#method_item}) +} + +fn verify_visibiity(visibility: &Visibility) { + match visibility { + Visibility::Inherited => {} + _ => panic!("The visibility of read/write method must be private"), + }; +} + +fn verify_inputs(inputs: &Punctuated, generics: &Generics, mutable: bool) { + if inputs.len() < 2 { + panic!("The two required parameters are missing: `&self/&mut self` and `RequestContext`.") + } + + if mutable { + if !arg_is_mutable_receiver(&inputs[0]) { + panic!("The receiver must be `&mut self`.") + } + } else { + if !arg_is_inmutable_receiver(&inputs[0]) { + panic!("The receiver must be `&self`.") + } + } + arg_is_request_context(&inputs[1], generics); +} + +fn verify_ret_type(ret_type: &ReturnType) { + let real_ret_type = match ret_type { + ReturnType::Type(_, t) => t.as_ref(), + _ => panic!("The return type of read/write method must be protocol::ProtocolResult"), + }; + + match real_ret_type { + Type::Path(type_path) => { + let path = &type_path.path; + let args = get_protocol_result_args(&path) + .expect("The return type of read/write method must be protocol::ProtocolResult"); + + match args { + PathArguments::AngleBracketed(arg) => { + let generic_args = &arg.args; + let generic_type = &generic_args[0]; + generic_type_is_jsonvalue(generic_type); + } + _ => panic!("The return value of read/write method must be json::JsonValue"), + }; + } + _ => panic!("The return type of read/write method must be protocol::ProtocolResult"), + } +} + +fn get_protocol_result_args(path: &Path) -> Option<&PathArguments> { + // :::: + if path.leading_colon.is_some() { + return None; + } + + // ProtocolResult + if path.segments.len() == 1 && path.segments[0].ident == "ProtocolResult" { + return Some(&path.segments[0].arguments); + } + + return None; +} + +fn path_is_jsonvalue(path: &Path) -> bool { + // :::: + if path.leading_colon.is_some() { + return false; + } + + // JsonValue + path.segments.len() == 1 && path.segments[0].ident == "JsonValue" +} + +fn path_is_request_context(path: &Path, bound_name: &str) -> bool { + // :::: + if path.leading_colon.is_some() { + return false; + } + + // RequestContext + path.segments.len() == 1 && path.segments[0].ident == bound_name +} + +fn generic_type_is_jsonvalue(generic_type: &GenericArgument) -> bool { + match generic_type { + GenericArgument::Type(t) => match t { + Type::Path(type_path) => path_is_jsonvalue(&type_path.path), + _ => false, + }, + _ => false, + } +} + +// expect fn foo +fn get_bounds_name_of_request_context(generics: &Generics) -> Option { + if generics.params.len() != 1 { + return None; + } + + let generics_type = &generics.params[0]; + + if let GenericParam::Type(t) = generics_type { + let bound_name = t.ident.to_string(); + + if let TypeParamBound::Trait(bound_trait) = &t.bounds[0] { + let ident = &bound_trait.path.segments[0].ident; + if ident == "RequestContext" { + return Some(bound_name); + } + } + } + + None +} + +// expect fn foo(&self, ctx: Context) +fn arg_is_request_context(fn_arg: &FnArg, generics: &Generics) -> bool { + let ty = match fn_arg { + FnArg::Typed(pat_type) => &*pat_type.ty, + _ => return false, + }; + + let bound_name = get_bounds_name_of_request_context(generics) + .expect("The `read/write` method must bound the trait `RequestContext`"); + + match ty { + Type::Path(type_path) => path_is_request_context(&type_path.path, &bound_name), + _ => false, + } +} + +// expect &mut self +fn arg_is_mutable_receiver(fn_arg: &FnArg) -> bool { + match fn_arg { + FnArg::Receiver(receiver) => receiver.reference.is_some() && receiver.mutability.is_some(), + _ => false, + } +} + +// expect &self +fn arg_is_inmutable_receiver(fn_arg: &FnArg) -> bool { + match fn_arg { + FnArg::Receiver(receiver) => receiver.reference.is_some() && receiver.mutability.is_none(), + _ => false, + } +} diff --git a/core/binding-macro/tests/mod.rs b/core/binding-macro/tests/mod.rs new file mode 100644 index 000000000..3ba31e2f8 --- /dev/null +++ b/core/binding-macro/tests/mod.rs @@ -0,0 +1,79 @@ +#[macro_use] +extern crate core_binding_macro; + +use bytes::Bytes; +use json::JsonValue; + +use protocol::traits::RequestContext; +use protocol::types::{Address, Hash}; +use protocol::ProtocolResult; + +#[test] +fn test_read() { + struct Tests; + + impl Tests { + #[read] + fn test_read_fn( + &self, + _ctx: Context, + ) -> ProtocolResult { + Ok(JsonValue::Null) + } + + #[write] + fn test_write_fn( + &mut self, + _ctx: Context, + ) -> ProtocolResult { + Ok(JsonValue::Null) + } + } + + let context = MockRequestContext {}; + + let mut t = Tests {}; + assert_eq!(t.test_read_fn(context.clone()).unwrap(), JsonValue::Null); + assert_eq!(t.test_write_fn(context.clone()).unwrap(), JsonValue::Null); +} + +#[derive(Clone)] +struct MockRequestContext; + +impl RequestContext for MockRequestContext { + fn sub_cycles(&self, _cycels: u64) -> ProtocolResult<()> { + Ok(()) + } + + fn get_cycles_price(&self) -> ProtocolResult { + Ok(0) + } + + fn get_cycles_limit(&self) -> ProtocolResult { + Ok(0) + } + + fn get_cycles_used(&self) -> ProtocolResult { + Ok(0) + } + + fn get_caller(&self) -> ProtocolResult
{ + Address::from_hash(Hash::digest(Bytes::from("test"))) + } + + fn get_current_epoch_id(&self) -> ProtocolResult { + Ok(0) + } + + fn get_service_name(&self) -> ProtocolResult<&str> { + Ok("service") + } + + fn get_service_method(&self) -> ProtocolResult<&str> { + Ok("method") + } + + fn get_payload(&self) -> ProtocolResult<&str> { + Ok("payload") + } +}