diff --git a/.gitignore b/.gitignore index 49bdc4424..6724e2745 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,5 @@ # Generated by Cargo Cargo.lock -/target/ +target/ diff --git a/Cargo.toml b/Cargo.toml index 3cb6ae949..5911073c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "capnpc", "capnp-futures", "capnp-rpc", + "capnp-conv", # testing and examples "benchmark", @@ -20,4 +21,5 @@ members = [ default-members = [ "capnp", "capnpc", -] \ No newline at end of file + "capnp-conv", +] diff --git a/capnp-conv/Cargo.toml b/capnp-conv/Cargo.toml new file mode 100644 index 000000000..1a2c870a4 --- /dev/null +++ b/capnp-conv/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "capnp-conv" +version = "0.11.0" +authors = [ "David Renshaw ", "real "] +license = "MIT" +edition = "2018" + +[dependencies] + +capnp_conv_derive = { path = "capnp-conv-derive", version = "0.1.0", package = "capnp-conv-derive" } + +capnp = { path = "../capnp", package = "capnp" } +derive_more = "0.15.0" + +[build-dependencies] +capnpc = { path = "../capnpc", package = "capnpc" } + +[dev-dependencies] + +byteorder = {version = "1.3.2", features = ["i128"]} diff --git a/capnp-conv/build.rs b/capnp-conv/build.rs new file mode 100644 index 000000000..1ef2c362e --- /dev/null +++ b/capnp-conv/build.rs @@ -0,0 +1,7 @@ +fn main() { + capnpc::CompilerCommand::new() + .src_prefix("tests/") + .file("tests/capnp/test.capnp") + .run() + .unwrap(); +} diff --git a/capnp-conv/capnp-conv-derive/Cargo.toml b/capnp-conv/capnp-conv-derive/Cargo.toml new file mode 100644 index 000000000..7641df45c --- /dev/null +++ b/capnp-conv/capnp-conv-derive/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "capnp-conv-derive" +version = "0.1.0" +authors = [ "David Renshaw ", "real "] +license = "MIT" +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] + +quote = "0.6.12" +syn = "0.15.38" +proc-macro2 = "0.4" +heck = "0.3.1" + diff --git a/capnp-conv/capnp-conv-derive/src/derive_enum.rs b/capnp-conv/capnp-conv-derive/src/derive_enum.rs new file mode 100644 index 000000000..bd4962054 --- /dev/null +++ b/capnp-conv/capnp-conv-derive/src/derive_enum.rs @@ -0,0 +1,277 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::spanned::Spanned; +// use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, Index}; +use syn::{DataEnum, Fields, Ident, Path, Variant}; + +use heck::SnakeCase; + +use crate::util::{ + capnp_result_shim, gen_list_read_iter, gen_list_write_iter, get_vec, is_data, is_primitive, + usize_to_u32_shim, CapnpWithAttribute, +}; + +// TODO: Deal with the case of multiple with attributes (Should report error) +/// Get the path from a with style field attribute. +/// Example: +/// ```text +/// #[capnp_conv(with = Wrapper)] +/// ``` +/// Will return the path `Wrapper` +fn get_with_attribute(variant: &syn::Variant) -> Option { + for attr in &variant.attrs { + if attr.path.is_ident("capnp_conv") { + let tts: proc_macro::TokenStream = attr.tts.clone().into(); + let capnp_with_attr = syn::parse::(tts).unwrap(); + return Some(capnp_with_attr.path); + } + } + None +} + +fn gen_type_write(variant: &Variant, assign_defaults: impl Fn(&mut syn::Path)) -> TokenStream { + let opt_with_path = get_with_attribute(variant); + // let variant_ident = &variant.ident; + let variant_name = &variant.ident; + let variant_snake_name = variant_name.to_string().to_snake_case(); + + match &variant.fields { + Fields::Unnamed(fields_unnamed) => { + let unnamed = &fields_unnamed.unnamed; + if unnamed.len() != 1 { + unimplemented!("gen_type_write: Amount of unnamed fields is not 1!"); + } + + let pair = unnamed.last().unwrap(); + let last_ident = match pair { + syn::punctuated::Pair::End(last_ident) => last_ident, + _ => unreachable!(), + }; + + let path = match opt_with_path { + Some(with_path) => with_path, + None => match &last_ident.ty { + syn::Type::Path(type_path) => type_path.path.clone(), + _ => { + panic!("{:?}", opt_with_path); + } + }, + }; + /* + let path = opt_with_path.clone().unwrap_or(match &last_ident.ty { + syn::Type::Path(type_path) => type_path.path.clone(), + _ => { + panic!("{:?}", opt_with_path); + // unimplemented!("gen_type_write: last ident is not a path!"), + } + }); + */ + + let mut path = path; + assign_defaults(&mut path); + + if is_primitive(&path) { + let set_method = + syn::Ident::new(&format!("set_{}", &variant_snake_name), variant.span()); + return quote! { + #variant_name(x) => writer.#set_method(<#path>::from(x.clone())), + }; + } + + if is_data(&path) { + let set_method = + syn::Ident::new(&format!("set_{}", &variant_snake_name), variant.span()); + return quote! { + #variant_name(x) => writer.#set_method(&<#path>::from(x.clone())), + }; + } + + if path.is_ident("String") { + let set_method = + syn::Ident::new(&format!("set_{}", &variant_snake_name), variant.span()); + return quote! { + #variant_name(x) => writer.#set_method(x), + }; + } + + // The case of list: + if let Some(inner_path) = get_vec(&path) { + let init_method = + syn::Ident::new(&format!("init_{}", &variant_snake_name), variant.span()); + let list_write_iter = gen_list_write_iter(&inner_path); + + // In the cases of more complicated types, list_builder needs to be mutable. + let let_list_builder = + if is_primitive(&path) || path.is_ident("String") || is_data(&path) { + quote! { let list_builder } + } else { + quote! { let mut list_builder } + }; + + let usize_to_u32 = usize_to_u32_shim(); + return quote! { + #variant_name(vec) => { + #usize_to_u32 + #let_list_builder = writer + .reborrow() + .#init_method(usize_to_u32(vec.len()).unwrap()); + + for (index, item) in vec.iter().enumerate() { + #list_write_iter + } + }, + }; + } + + let init_method = + syn::Ident::new(&format!("init_{}", &variant_snake_name), variant.span()); + quote! { + #variant_name(x) => <#path>::from(x.clone()).write_capnp(&mut writer.reborrow().#init_method()), + } + } + + Fields::Unit => { + let set_method = + syn::Ident::new(&format!("set_{}", &variant_snake_name), variant.span()); + quote! { + #variant_name => writer.#set_method(()), + } + } + // Rust enum variants don't have named fields (?) + Fields::Named(_) => unreachable!(), + } +} + +pub fn gen_write_capnp_enum( + data_enum: &DataEnum, + rust_enum: &Ident, + capnp_struct: &Path, + assign_defaults: impl Fn(&mut syn::Path), +) -> TokenStream { + let recurse = data_enum.variants.iter().map(|variant| { + let type_write = gen_type_write(&variant, &assign_defaults); + quote! { + #rust_enum::#type_write + } + }); + + quote! { + impl<'a> WriteCapnp<'a> for #rust_enum { + type WriterType = #capnp_struct::Builder<'a>; + fn write_capnp(&self, writer: &mut Self::WriterType) { + match &self { + #(#recurse)* + }; + } + } + } +} + +fn gen_type_read( + variant: &Variant, + rust_enum: &Ident, + assign_defaults: impl Fn(&mut syn::Path), +) -> TokenStream { + let opt_with_path = get_with_attribute(variant); + let variant_name = &variant.ident; + // let variant_snake_name = variant_name.to_string().to_snake_case(); + + match &variant.fields { + Fields::Unnamed(fields_unnamed) => { + let unnamed = &fields_unnamed.unnamed; + if unnamed.len() != 1 { + unimplemented!("gen_type_read: Amount of unnamed fields is not 1!"); + } + + let pair = unnamed.last().unwrap(); + let last_ident = match pair { + syn::punctuated::Pair::End(last_ident) => last_ident, + _ => unreachable!(), + }; + + let mut path = match opt_with_path { + Some(with_path) => with_path, + None => match &last_ident.ty { + syn::Type::Path(type_path) => type_path.path.clone(), + _ => { + panic!("{:?}", opt_with_path); + } + }, + }; + + assign_defaults(&mut path); + + if is_primitive(&path) { + return quote! { + #variant_name(x) => #rust_enum::#variant_name(x.into()), + }; + } + + if is_data(&path) || path.is_ident("String") { + return quote! { + #variant_name(x) => #rust_enum::#variant_name(x?.into()), + }; + } + + if let Some(inner_path) = get_vec(&path) { + // The case of a list: + let list_read_iter = gen_list_read_iter(&inner_path); + return quote! { + #variant_name(list_reader) => { + let mut res_vec = Vec::new(); + for item_reader in list_reader? { + // res_vec.push_back(read_named_relay_address(&named_relay_address)?); + #list_read_iter + } + #rust_enum::#variant_name(res_vec) + } + }; + } + + let capnp_result = capnp_result_shim(); + + quote! { + #variant_name(variant_reader) => { + #capnp_result + + let variant_reader = CapnpResult::from(variant_reader).into_result()?; + #rust_enum::#variant_name(<#path>::read_capnp(&variant_reader)?.into()) + }, + } + } + + Fields::Unit => { + quote! { + #variant_name(()) => #rust_enum::#variant_name, + } + } + // Rust enum variants don't have named fields (?) + Fields::Named(_) => unreachable!(), + } +} + +pub fn gen_read_capnp_enum( + data_enum: &DataEnum, + rust_enum: &Ident, + capnp_struct: &Path, + assign_defaults: impl Fn(&mut syn::Path), +) -> TokenStream { + let recurse = data_enum.variants.iter().map(|variant| { + let type_read = gen_type_read(&variant, rust_enum, &assign_defaults); + quote! { + #capnp_struct::#type_read + } + }); + + quote! { + impl<'a> ReadCapnp<'a> for #rust_enum { + type ReaderType = #capnp_struct::Reader<'a>; + + fn read_capnp(reader: &Self::ReaderType) -> Result { + Ok(match reader.which()? { + #(#recurse)* + }) + } + } + } +} diff --git a/capnp-conv/capnp-conv-derive/src/derive_struct.rs b/capnp-conv/capnp-conv-derive/src/derive_struct.rs new file mode 100644 index 000000000..bde1bc0c3 --- /dev/null +++ b/capnp-conv/capnp-conv-derive/src/derive_struct.rs @@ -0,0 +1,205 @@ +use proc_macro2::TokenStream; +use quote::{quote, quote_spanned}; + +use syn::spanned::Spanned; +use syn::{FieldsNamed, Ident, Path}; + +use crate::util::{ + capnp_result_shim, gen_list_read_iter, gen_list_write_iter, get_vec, is_data, is_primitive, + usize_to_u32_shim, CapnpWithAttribute, +}; + +// TODO: Deal with the case of multiple with attributes (Should report error) +/// Get the path from a with style field attribute. +/// Example: +/// ```text +/// #[capnp_conv(with = Wrapper)] +/// ``` +/// Will return the path `Wrapper` +fn get_with_attribute(field: &syn::Field) -> Option { + for attr in &field.attrs { + if attr.path.is_ident("capnp_conv") { + let tts: proc_macro::TokenStream = attr.tts.clone().into(); + let capnp_with_attr = syn::parse::(tts).unwrap(); + return Some(capnp_with_attr.path); + } + } + None +} + +fn gen_type_write(field: &syn::Field, assign_defaults: impl Fn(&mut syn::Path)) -> TokenStream { + let opt_with_path = get_with_attribute(field); + match &field.ty { + syn::Type::Path(type_path) => { + if type_path.qself.is_some() { + // Self qualifier? + unimplemented!("self qualifier"); + } + + let mut path = type_path.path.clone(); + assign_defaults(&mut path); + let path = opt_with_path.unwrap_or(path); + + let name = &field.ident.as_ref().unwrap(); + + if is_primitive(&path) { + let set_method = syn::Ident::new(&format!("set_{}", &name), name.span()); + return quote_spanned! {field.span() => + writer.reborrow().#set_method(<#path>::from(self.#name)); + }; + } + + if path.is_ident("String") || is_data(&path) { + let set_method = syn::Ident::new(&format!("set_{}", &name), name.span()); + return quote_spanned! {field.span() => + writer.reborrow().#set_method(&<#path>::from(self.#name.clone())); + }; + } + + if let Some(inner_path) = get_vec(&path) { + let init_method = syn::Ident::new(&format!("init_{}", &name), name.span()); + let list_write_iter = gen_list_write_iter(&inner_path); + + // In the cases of more complicated types, list_builder needs to be mutable. + let let_list_builder = + if is_primitive(&path) || path.is_ident("String") || is_data(&path) { + quote! { let list_builder } + } else { + quote! { let mut list_builder } + }; + + let usize_to_u32 = usize_to_u32_shim(); + + return quote_spanned! {field.span() => + { + #usize_to_u32 + + #let_list_builder = { + writer + .reborrow() + .#init_method(usize_to_u32(self.#name.len()).unwrap()) + }; + + for (index, item) in self.#name.iter().enumerate() { + #list_write_iter + } + } + }; + } + + // Generic type: + let init_method = syn::Ident::new(&format!("init_{}", &name), name.span()); + quote_spanned! {field.span() => + <#path>::from(self.#name.clone()).write_capnp(&mut writer.reborrow().#init_method()); + } + } + _ => unimplemented!(), + } +} + +fn gen_type_read(field: &syn::Field, assign_defaults: impl Fn(&mut syn::Path)) -> TokenStream { + let opt_with_path = get_with_attribute(field); + + match &field.ty { + syn::Type::Path(type_path) => { + if type_path.qself.is_some() { + // Self qualifier? + unimplemented!("self qualifier"); + } + + let mut path = type_path.path.clone(); + assign_defaults(&mut path); + + let path = opt_with_path.unwrap_or(path); + + let name = &field.ident.as_ref().unwrap(); + + if is_primitive(&path) { + let get_method = syn::Ident::new(&format!("get_{}", &name), name.span()); + return quote_spanned! {field.span() => + #name: reader.#get_method().into() + }; + } + + if path.is_ident("String") || is_data(&path) { + let get_method = syn::Ident::new(&format!("get_{}", &name), name.span()); + return quote_spanned! {field.span() => + #name: reader.#get_method()?.into() + }; + } + + if let Some(inner_path) = get_vec(&path) { + let get_method = syn::Ident::new(&format!("get_{}", &name), name.span()); + let list_read_iter = gen_list_read_iter(&inner_path); + return quote_spanned! {field.span() => + #name: { + let mut res_vec = Vec::new(); + for item_reader in reader.#get_method()? { + // res_vec.push_back(read_named_relay_address(&named_relay_address)?); + #list_read_iter + } + res_vec.into() + } + }; + } + + // Generic type: + let get_method = syn::Ident::new(&format!("get_{}", &name), name.span()); + let capnp_result = capnp_result_shim(); + quote_spanned! {field.span() => + #name: { + #capnp_result + let inner_reader = CapnpResult::from(reader.#get_method()).into_result()?; + <#path>::read_capnp(&inner_reader)?.into() + } + } + } + _ => unimplemented!(), + } +} + +pub fn gen_write_capnp_named_struct( + fields_named: &FieldsNamed, + rust_struct: &Ident, + capnp_struct: &Path, + assign_defaults: impl Fn(&mut syn::Path), +) -> TokenStream { + let recurse = fields_named + .named + .iter() + .map(|field| gen_type_write(&field, &assign_defaults)); + + quote! { + impl<'a> WriteCapnp<'a> for #rust_struct { + type WriterType = #capnp_struct::Builder<'a>; + + fn write_capnp(&self, writer: &mut Self::WriterType) { + #(#recurse)* + } + } + } +} + +pub fn gen_read_capnp_named_struct( + fields_named: &FieldsNamed, + rust_struct: &Ident, + capnp_struct: &Path, + assign_defaults: impl Fn(&mut syn::Path), +) -> TokenStream { + let recurse = fields_named + .named + .iter() + .map(|field| gen_type_read(field, &assign_defaults)); + + quote! { + impl<'a> ReadCapnp<'a> for #rust_struct { + type ReaderType = #capnp_struct::Reader<'a>; + + fn read_capnp(reader: &Self::ReaderType) -> Result { + Ok(#rust_struct { + #(#recurse,)* + }) + } + } + } +} diff --git a/capnp-conv/capnp-conv-derive/src/lib.rs b/capnp-conv/capnp-conv-derive/src/lib.rs new file mode 100644 index 000000000..713fdfc28 --- /dev/null +++ b/capnp-conv/capnp-conv-derive/src/lib.rs @@ -0,0 +1,110 @@ +#![crate_type = "lib"] +#![recursion_limit = "128"] +#![deny(trivial_numeric_casts, warnings)] +#![allow(intra_doc_link_resolution_failure)] +#![allow( + clippy::too_many_arguments, + clippy::implicit_hasher, + clippy::module_inception, + clippy::new_without_default +)] +#![allow(unreachable_code)] + +extern crate proc_macro; + +mod derive_enum; +mod derive_struct; +mod util; + +use quote::quote; +use syn::{parse_macro_input, Data, DeriveInput, Fields, Path}; + +use self::derive_enum::{gen_read_capnp_enum, gen_write_capnp_enum}; +use self::derive_struct::{gen_read_capnp_named_struct, gen_write_capnp_named_struct}; +use self::util::{assign_defaults_path, extract_defaults, remove_with_attributes}; + +/// Generate code for conversion between Rust and capnp structs. +#[proc_macro_attribute] +pub fn capnp_conv( + args: proc_macro::TokenStream, + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + // See: https://github.com/dtolnay/syn/issues/86 + // for information about arguments. + + // Name of capnp struct: + let capnp_struct = parse_macro_input!(args as Path); + let input = parse_macro_input!(input as DeriveInput); + + let defaults = extract_defaults(&input.generics); + let assign_defaults = |path: &mut syn::Path| assign_defaults_path(path, &defaults); + + // Name of local struct: + let rust_struct = &input.ident; + + let conversion = match input.data { + Data::Struct(ref data) => match data.fields { + Fields::Named(ref fields_named) => { + // Example: + // struct Point { + // x: u32, + // y: u32, + // } + + let write_capnp = gen_write_capnp_named_struct( + fields_named, + rust_struct, + &capnp_struct, + &assign_defaults, + ); + let read_capnp = gen_read_capnp_named_struct( + fields_named, + rust_struct, + &capnp_struct, + &assign_defaults, + ); + + quote! { + #[allow(clippy::all)] + #write_capnp + #[allow(clippy::all)] + #read_capnp + } + } + Fields::Unnamed(_) | Fields::Unit => unimplemented!(), + }, + Data::Enum(ref data_enum) => { + // Example: + // enum MyEnum { + // Type1(u32), + // Type2, + // Type3(MyStruct), + // } + let write_capnp = + gen_write_capnp_enum(data_enum, rust_struct, &capnp_struct, &assign_defaults); + let read_capnp = + gen_read_capnp_enum(data_enum, rust_struct, &capnp_struct, &assign_defaults); + + quote! { + #[allow(clippy::all)] + #write_capnp + #[allow(clippy::all)] + #read_capnp + } + } + Data::Union(_) => unimplemented!(), + }; + + // Remove all of our `#[capnp_conv(with = ... )]` attributes from the input: + let mut input = input; + remove_with_attributes(&mut input); + + let expanded = quote! { + // Original structure + #input + // Generated mutual From conversion code: + #conversion + }; + + proc_macro::TokenStream::from(expanded) +} diff --git a/capnp-conv/capnp-conv-derive/src/util.rs b/capnp-conv/capnp-conv-derive/src/util.rs new file mode 100644 index 000000000..0c8b0f600 --- /dev/null +++ b/capnp-conv/capnp-conv-derive/src/util.rs @@ -0,0 +1,336 @@ +// use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, Index}; +use syn; + +use std::collections::HashMap; + +use proc_macro2::TokenStream; +use quote::quote; + +/// A shim for converting usize to u32 +pub fn usize_to_u32_shim() -> TokenStream { + quote! { + pub fn usize_to_u32(num: usize) -> Option { + if num > 0xffffffff as usize { + None + } else { + Some(num as u32) + } + } + } +} + +/// Is a primitive type? +pub fn is_primitive(path: &syn::Path) -> bool { + path.is_ident("u8") + || path.is_ident("u16") + || path.is_ident("u32") + || path.is_ident("u64") + || path.is_ident("i8") + || path.is_ident("i16") + || path.is_ident("i32") + || path.is_ident("i64") + || path.is_ident("f32") + || path.is_ident("f64") + || path.is_ident("bool") +} + +/// Check if the path represents a Vec +pub fn is_data(path: &syn::Path) -> bool { + let last_segment = match path.segments.last().unwrap() { + syn::punctuated::Pair::End(last_segment) => last_segment, + _ => unreachable!(), + }; + if &last_segment.ident.to_string() != "Vec" { + return false; + } + let angle = match &last_segment.arguments { + syn::PathArguments::AngleBracketed(angle) => { + if angle.args.len() > 1 { + unreachable!("Too many arguments for Vec!"); + } + angle + } + _ => unreachable!("Vec with arguments that are not angle bracketed!"), + }; + let last_arg = match angle.args.last().unwrap() { + syn::punctuated::Pair::End(last_arg) => last_arg, + _ => return false, + }; + + let arg_ty = match last_arg { + syn::GenericArgument::Type(arg_ty) => arg_ty, + _ => return false, + }; + + let arg_ty_path = match arg_ty { + syn::Type::Path(arg_ty_path) => arg_ty_path, + _ => return false, + }; + + if arg_ty_path.qself.is_some() { + return false; + } + + if !arg_ty_path.path.is_ident("u8") { + return false; + } + + true +} + +/// Check if the path represents a Vec, where SomeStruct != u8 +pub fn get_vec(path: &syn::Path) -> Option { + let last_segment = match path.segments.last().unwrap() { + syn::punctuated::Pair::End(last_segment) => last_segment, + _ => unreachable!(), + }; + if &last_segment.ident.to_string() != "Vec" { + return None; + } + let angle = match &last_segment.arguments { + syn::PathArguments::AngleBracketed(angle) => { + if angle.args.len() > 1 { + unreachable!("Too many arguments for Vec!"); + } + angle + } + _ => unreachable!("Vec with arguments that are not angle bracketed!"), + }; + let last_arg = match angle.args.last().unwrap() { + syn::punctuated::Pair::End(last_arg) => last_arg, + _ => return None, + }; + + let arg_ty = match last_arg { + syn::GenericArgument::Type(arg_ty) => arg_ty, + _ => return None, + }; + + let arg_ty_path = match arg_ty { + syn::Type::Path(arg_ty_path) => arg_ty_path, + _ => return None, + }; + + if arg_ty_path.qself.is_some() { + return None; + } + + // Make sure that we don't deal with Vec: + if arg_ty_path.path.is_ident("u8") { + return None; + } + + Some(arg_ty_path.path.clone()) +} + +pub fn gen_list_write_iter(path: &syn::Path) -> TokenStream { + if is_primitive(path) || path.is_ident("String") || is_data(path) { + // A primitive list: + quote! { + list_builder + .reborrow() + .set(usize_to_u32(index).unwrap(), item.clone().into()); + } + } else { + // Not a primitive list: + quote! { + let mut item_builder = list_builder + .reborrow() + .get(usize_to_u32(index).unwrap()); + + item.clone().write_capnp(&mut item_builder); + } + } + // TODO: It seems like we do not support List(List(...)) at the moment. + // How to support it? +} + +pub fn gen_list_read_iter(path: &syn::Path) -> TokenStream { + if is_primitive(path) || path.is_ident("String") || is_data(path) { + // A primitive list: + quote! { + res_vec.push(item_reader.into()); + } + } else { + // Not a primitive list: + quote! { + res_vec.push(<#path>::read_capnp(&item_reader)?); + } + } + // TODO: It seems like we do not support List(List(...)) at the moment. + // How to support it? +} + +/// A shim allowing to merge cases where either +/// Result> or a T is returned. +pub fn capnp_result_shim() -> TokenStream { + quote! { + pub enum CapnpResult { + Ok(T), + Err(CapnpConvError), + } + + impl CapnpResult { + pub fn into_result(self) -> Result { + match self { + CapnpResult::Ok(t) => Ok(t), + CapnpResult::Err(e) => Err(e), + } + } + } + + impl From for CapnpResult { + fn from(input: T) -> Self { + CapnpResult::Ok(input) + } + } + + impl From> for CapnpResult + where + E: Into, + { + fn from(input: Result) -> Self { + match input { + Ok(t) => CapnpResult::Ok(t), + Err(e) => CapnpResult::Err(e.into()), + } + } + } + } +} + +/// Obtain a map of default values from generics. +/// Example: +/// +/// ```text +/// struct MyStruct { ... } +/// ``` +/// +/// We expect to get a map, mapping A -> u32, B -> u64. +/// +pub fn extract_defaults(generics: &syn::Generics) -> HashMap { + let mut defaults = HashMap::new(); + for param in &generics.params { + let type_param = match *param { + syn::GenericParam::Type(ref type_param) => type_param, + _ => continue, + }; + + if type_param.eq_token.is_none() { + continue; + }; + + let default_type = match &type_param.default { + Some(default_type) => default_type, + None => continue, + }; + + let default_type_path = match default_type { + syn::Type::Path(default_type_path) => default_type_path, + _ => unimplemented!("Only paths default params are supported"), + }; + + if default_type_path.qself.is_some() { + unimplemented!("qself is not implemented!"); + } + + defaults.insert(type_param.ident.clone(), default_type_path.path.clone()); + } + defaults +} + +/// For every generic along a path, assign a default value if possible +pub fn assign_defaults_path(path: &mut syn::Path, defaults: &HashMap) { + // Deal with the case of a single Ident: `T` + + if path.segments.len() == 1 { + let last_segment = match path.segments.last_mut().unwrap() { + syn::punctuated::Pair::End(last_segment) => last_segment, + _ => unreachable!(), + }; + + if let syn::PathArguments::None = last_segment.arguments { + if let Some(default_path) = defaults.get(&last_segment.ident) { + let _ = std::mem::replace(path, default_path.clone()); + return; + } + } + } + + // Deal with the more general case of a Path with various arguments + // that should be assigned their default value + + for segment in path.segments.iter_mut() { + let args = match &mut segment.arguments { + syn::PathArguments::None => continue, + syn::PathArguments::AngleBracketed(angle_bracketed) => &mut angle_bracketed.args, + _ => unimplemented!("Only angle bracketed arguments are supported!"), + }; + + for generic_arg in args.iter_mut() { + let ty = match generic_arg { + syn::GenericArgument::Type(ty) => ty, + _ => unimplemented!(), + }; + + let type_path = match ty { + syn::Type::Path(type_path) => type_path, + _ => unimplemented!(), + }; + + if type_path.qself.is_some() { + unimplemented!(); + } + + // Recursively replace default arguments: + assign_defaults_path(&mut type_path.path, defaults); + } + } +} + +/// Remove all of our `#[capnp_conv(with = ...)]` attributes +pub fn remove_with_attributes(input: &mut syn::DeriveInput) { + match input.data { + syn::Data::Struct(ref mut data) => match data.fields { + syn::Fields::Named(ref mut fields_named) => { + for field in fields_named.named.iter_mut() { + // Remove all the attributes that look like: `capnp_conv(...)` + field.attrs.retain(|attr| !attr.path.is_ident("capnp_conv")); + } + } + syn::Fields::Unnamed(_) | syn::Fields::Unit => unimplemented!(), + }, + syn::Data::Enum(ref mut data_enum) => { + for variant in data_enum.variants.iter_mut() { + // Remove all the attributes that look like: `capnp_conv(...)` + variant + .attrs + .retain(|attr| !attr.path.is_ident("capnp_conv")); + } + } + + syn::Data::Union(_) => unimplemented!(), + }; +} + +#[derive(Debug)] +pub struct CapnpWithAttribute { + #[allow(dead_code)] + pub paren_token: syn::token::Paren, + pub with_ident: syn::Ident, + pub eq_token: syn::Token![=], + pub path: syn::Path, +} + +impl syn::parse::Parse for CapnpWithAttribute { + fn parse(input: syn::parse::ParseStream) -> syn::parse::Result { + let content; + let paren_token = syn::parenthesized!(content in input); + Ok(Self { + paren_token, + with_ident: content.parse()?, + eq_token: content.parse()?, + path: content.parse()?, + }) + } +} diff --git a/capnp-conv/src/lib.rs b/capnp-conv/src/lib.rs new file mode 100644 index 000000000..185554e54 --- /dev/null +++ b/capnp-conv/src/lib.rs @@ -0,0 +1,88 @@ +#![crate_type = "lib"] +#![deny(trivial_numeric_casts, warnings)] +#![allow(intra_doc_link_resolution_failure)] +#![allow( + clippy::too_many_arguments, + clippy::implicit_hasher, + clippy::module_inception, + clippy::new_without_default +)] + +use std::io; + +use capnp; + +use derive_more::From; + +pub use capnp_conv_derive::capnp_conv; + +#[derive(Debug, From)] +pub enum CapnpConvError { + CapnpError(capnp::Error), + NotInSchema(capnp::NotInSchema), + IoError(io::Error), +} + +/// Convert Rust struct to Capnp. +pub trait WriteCapnp<'a> { + /// The corresponding Capnp writer type. + // type WriterType: capnp::traits::FromPointerBuilder<'a>; + type WriterType: capnp::traits::FromPointerBuilder<'a>; + + /// Converts a Rust struct to corresponding Capnp struct. This should not fail. + fn write_capnp(&self, writer: &mut Self::WriterType); +} + +/// Convert Capnp struct to Rust. +pub trait ReadCapnp<'a>: Sized { + /// The corresponding Capnp reader type. + type ReaderType: capnp::traits::FromPointerReader<'a>; + + /// Converts a Capnp struct to corresponding Rust struct. + fn read_capnp(reader: &Self::ReaderType) -> Result; +} + +pub trait ToCapnpBytes { + /// Serialize a Rust struct into bytes using Capnp + fn to_capnp_bytes(&self) -> Vec; +} + +pub trait FromCapnpBytes: Sized { + /// Deserialize a Rust struct from bytes using Capnp + fn from_capnp_bytes(bytes: &[u8]) -> Result; +} + +impl ToCapnpBytes for T +where + T: for<'a> WriteCapnp<'a>, +{ + fn to_capnp_bytes(&self) -> Vec { + let mut builder = capnp::message::Builder::new_default(); + + // A trick to avoid borrow checker issues: + { + let mut struct_builder = builder.init_root::(); + self.write_capnp(&mut struct_builder); + } + + let mut data = Vec::new(); + // Should never really fail: + capnp::serialize_packed::write_message(&mut data, &builder).unwrap(); + data + } +} + +impl FromCapnpBytes for T +where + T: for<'a> ReadCapnp<'a>, +{ + fn from_capnp_bytes(bytes: &[u8]) -> Result { + let mut cursor = io::Cursor::new(&bytes); + let reader = capnp::serialize_packed::read_message( + &mut cursor, + capnp::message::ReaderOptions::new(), + )?; + let struct_reader = reader.get_root::()?; + Ok(Self::read_capnp(&struct_reader)?) + } +} diff --git a/capnp-conv/tests/capnp/test.capnp b/capnp-conv/tests/capnp/test.capnp new file mode 100644 index 000000000..61ca67a6b --- /dev/null +++ b/capnp-conv/tests/capnp/test.capnp @@ -0,0 +1,109 @@ +@0xc90daeac68e62b2a; + +struct TestStructInner { + innerU8 @0: UInt8; +} + +struct TestUnion { + union { + variantOne @0: UInt64; + variantTwo @1: TestStructInner; + variantThree @2: Void; + variantFour @3: Text; + } +} + +struct ListUnion { + union { + empty @0: Void; + withList @1: List(TestStructInner); + withData @2: Data; + testUnion @3: TestUnion; + inlineInnerUnion: union { + ab @4: UInt32; + cd @5: UInt64; + } + } +} + +struct TestStruct { + myBool @0: Bool; + myInt8 @1: Int8; + myInt16 @2: Int16; + myInt32 @3: Int32; + myInt64 @4: Int64; + myUint8 @5: UInt8; + myUint16 @6: UInt16; + myUint32 @7: UInt32; + myUint64 @8: UInt64; + # my_float32: f32, + # my_float64: f64, + myText @9: Text; + myData @10: Data; + structInner @11: TestStructInner; + myPrimitiveList @12: List(UInt16); + myList @13: List(TestStructInner); + inlineUnion: union { + firstVariant @14: UInt64; + secondVariant @15: TestStructInner; + thirdVariant @16: Void; + } + externalUnion @17: TestUnion; + listUnion @18: ListUnion; +} + +struct FloatStruct { + myFloat32 @0: Float32; + myFloat64 @1: Float64; +} + +struct GenericStruct { + a @0: UInt32; + b @1: UInt64; + c @2: UInt8; + d @3: Data; + e @4: List(TestStructInner); + f @5: TestStructInner; +} + +struct GenericEnum { + union { + varA @0: UInt32; + varB @1: TestStructInner; + varC @2: UInt64; + varD @3: Data; + } +} + +struct InnerGeneric { + a @0: UInt32; +} + +struct ListGeneric { + list @0: List(InnerGeneric); +} + +# A custom made 128 bit data structure. +struct Buffer128 { + x0 @0: UInt64; + x1 @1: UInt64; +} + +# Unsigned 128 bit integer +struct CustomUInt128 { + inner @0: Buffer128; +} + + +struct TestWithStruct { + a @0: CustomUInt128; + b @1: UInt64; +} + +struct TestWithEnum { + union { + varA @0: CustomUInt128; + varB @1: UInt64; + varC @2: Void; + } +} diff --git a/capnp-conv/tests/derive.rs b/capnp-conv/tests/derive.rs new file mode 100644 index 000000000..4f5e27187 --- /dev/null +++ b/capnp-conv/tests/derive.rs @@ -0,0 +1,217 @@ +use capnp_conv::{capnp_conv, CapnpConvError, FromCapnpBytes, ReadCapnp, ToCapnpBytes, WriteCapnp}; + +#[allow(unused)] +mod test_capnp { + include!(concat!(env!("OUT_DIR"), "/capnp/test_capnp.rs")); +} + +#[capnp_conv(test_capnp::test_struct_inner)] +#[derive(Debug, Clone, PartialEq)] +struct TestStructInner { + inner_u8: u8, +} + +#[capnp_conv(test_capnp::test_struct::inline_union)] +#[derive(Debug, Clone, PartialEq)] +enum InlineUnion { + FirstVariant(u64), + SecondVariant(TestStructInner), + ThirdVariant, +} + +#[capnp_conv(test_capnp::test_union)] +#[derive(Debug, Clone, PartialEq)] +enum TestUnion { + VariantOne(u64), + VariantTwo(TestStructInner), + VariantThree, + VariantFour(String), +} + +#[capnp_conv(test_capnp::list_union::inline_inner_union)] +#[derive(Debug, Clone, PartialEq)] +enum InlineInnerUnion { + Ab(u32), + Cd(u64), +} + +#[capnp_conv(test_capnp::list_union)] +#[derive(Debug, Clone, PartialEq)] +enum ListUnion { + Empty, + WithList(Vec), + WithData(Vec), + TestUnion(TestUnion), + InlineInnerUnion(InlineInnerUnion), +} + +#[capnp_conv(test_capnp::test_struct)] +#[derive(Debug, Clone, PartialEq)] +struct TestStruct { + my_bool: bool, + my_int8: i8, + my_int16: i16, + my_int32: i32, + my_int64: i64, + my_uint8: u8, + my_uint16: u16, + my_uint32: u32, + my_uint64: u64, + my_text: String, + my_data: Vec, + struct_inner: TestStructInner, + my_primitive_list: Vec, + my_list: Vec, + inline_union: InlineUnion, + external_union: TestUnion, + list_union: ListUnion, +} + +#[test] +fn capnp_serialize_basic_struct() { + let test_struct = TestStruct { + my_bool: true, + my_int8: -1i8, + my_int16: 1i16, + my_int32: -1i32, + my_int64: 1i64, + my_uint8: 1u8, + my_uint16: 2u16, + my_uint32: 3u32, + my_uint64: 4u64, + my_text: "my_text".to_owned(), + my_data: vec![1, 2, 3, 4, 5u8], + struct_inner: TestStructInner { inner_u8: 1u8 }, + my_primitive_list: vec![10, 11, 12, 13, 14u16], + my_list: vec![ + TestStructInner { inner_u8: 2u8 }, + TestStructInner { inner_u8: 3u8 }, + TestStructInner { inner_u8: 4u8 }, + ], + inline_union: InlineUnion::SecondVariant(TestStructInner { inner_u8: 5u8 }), + external_union: TestUnion::VariantOne(6u64), + list_union: ListUnion::WithList(vec![ + TestStructInner { inner_u8: 10u8 }, + TestStructInner { inner_u8: 11u8 }, + ]), + }; + + let data = test_struct.to_capnp_bytes(); + let test_struct2 = TestStruct::from_capnp_bytes(&data).unwrap(); + + assert_eq!(test_struct, test_struct2); +} + +#[capnp_conv(test_capnp::float_struct)] +#[derive(Debug, Clone)] +struct FloatStruct { + my_float32: f32, + my_float64: f64, +} + +/// We test floats separately, because in Rust floats to not implement PartialEq +#[test] +fn capnp_serialize_floats() { + let float_struct = FloatStruct { + my_float32: -0.5f32, + my_float64: 0.5f64, + }; + + let data = float_struct.to_capnp_bytes(); + let float_struct2 = FloatStruct::from_capnp_bytes(&data).unwrap(); + + // Sloppily check that the floats are close enough (We can't compare them directly, as they + // don't implement PartialEq) + assert_eq!( + (float_struct.my_float32 * 10000.0).trunc(), + (float_struct2.my_float32 * 10000.0).trunc() + ); + + assert_eq!( + (float_struct.my_float64 * 10000.0).trunc(), + (float_struct2.my_float64 * 10000.0).trunc() + ); +} + +#[capnp_conv(test_capnp::generic_struct)] +#[derive(Debug, Clone, PartialEq)] +struct GenericStruct { + a: A, + pub b: B, + c: u8, + pub d: Vec, + e: Vec, + f: E, +} + +#[test] +fn capnp_serialize_generic_struct() { + let generic_struct = GenericStruct { + a: 1u32, + b: 2u64, + c: 3u8, + d: vec![1, 2, 3, 4u8], + e: vec![ + TestStructInner { inner_u8: 2u8 }, + TestStructInner { inner_u8: 3u8 }, + TestStructInner { inner_u8: 4u8 }, + ], + f: TestStructInner { inner_u8: 5u8 }, + }; + + let data = generic_struct.to_capnp_bytes(); + let generic_struct2 = GenericStruct::from_capnp_bytes(&data).unwrap(); + + assert_eq!(generic_struct, generic_struct2); +} + +#[capnp_conv(test_capnp::generic_enum)] +#[derive(Debug, Clone, PartialEq)] +enum GenericEnum> { + VarA(A), + VarB(B), + VarC(u64), + VarD(V), +} + +#[test] +fn capnp_serialize_generic_enum() { + for generic_enum in &[ + GenericEnum::VarA(1u32), + GenericEnum::VarB(TestStructInner { inner_u8: 2u8 }), + GenericEnum::VarC(3u64), + GenericEnum::VarD(vec![1, 2, 3, 4u8]), + ] { + let data = generic_enum.to_capnp_bytes(); + let generic_enum2 = GenericEnum::from_capnp_bytes(&data).unwrap(); + assert_eq!(generic_enum.clone(), generic_enum2); + } +} + +#[capnp_conv(test_capnp::inner_generic)] +#[derive(Debug, Clone, PartialEq)] +struct InnerGeneric { + a: A, +} + +#[capnp_conv(test_capnp::list_generic)] +#[derive(Debug, Clone, PartialEq)] +struct ListGeneric { + list: Vec>, +} + +#[test] +fn capnp_serialize_generic_list() { + let list_generic = ListGeneric { + list: vec![ + InnerGeneric { a: 1u32 }, + InnerGeneric { a: 2u32 }, + InnerGeneric { a: 3u32 }, + ], + }; + + let data = list_generic.to_capnp_bytes(); + let list_generic2 = ListGeneric::from_capnp_bytes(&data).unwrap(); + + assert_eq!(list_generic, list_generic2); +} diff --git a/capnp-conv/tests/with.rs b/capnp-conv/tests/with.rs new file mode 100644 index 000000000..6dcf177f7 --- /dev/null +++ b/capnp-conv/tests/with.rs @@ -0,0 +1,89 @@ +use byteorder::{BigEndian, ByteOrder, ReadBytesExt, WriteBytesExt}; +use capnp_conv::{capnp_conv, CapnpConvError, FromCapnpBytes, ReadCapnp, ToCapnpBytes, WriteCapnp}; + +#[allow(unused)] +mod test_capnp { + include!(concat!(env!("OUT_DIR"), "/capnp/test_capnp.rs")); +} + +#[derive(derive_more::Constructor, Debug, Clone, Copy, PartialEq, Eq)] +pub struct Wrapper(T); + +impl From for Wrapper { + fn from(t: T) -> Self { + Wrapper(t) + } +} + +impl Into for Wrapper { + fn into(self) -> u128 { + self.0 + } +} + +impl<'a> WriteCapnp<'a> for Wrapper { + type WriterType = crate::test_capnp::custom_u_int128::Builder<'a>; + + fn write_capnp(&self, writer: &mut Self::WriterType) { + let mut inner = writer.reborrow().get_inner().unwrap(); + + let mut data_bytes = Vec::new(); + + data_bytes + .write_u128::(self.clone().into()) + .unwrap(); + let mut cursor = std::io::Cursor::new(AsRef::<[u8]>::as_ref(&data_bytes)); + + inner.set_x0(cursor.read_u64::().unwrap()); + inner.set_x1(cursor.read_u64::().unwrap()); + } +} + +impl<'a> ReadCapnp<'a> for Wrapper { + type ReaderType = crate::test_capnp::custom_u_int128::Reader<'a>; + + fn read_capnp(reader: &Self::ReaderType) -> Result { + let inner = reader.get_inner()?; + let mut vec = Vec::new(); + vec.write_u64::(inner.get_x0())?; + vec.write_u64::(inner.get_x1())?; + Ok(Wrapper::new(BigEndian::read_u128(&vec[..]))) + } +} + +#[capnp_conv(test_capnp::test_with_struct)] +#[derive(Debug, Clone, PartialEq)] +struct TestWithStruct { + #[capnp_conv(with = Wrapper)] + a: u128, + b: u64, +} + +#[test] +fn capnp_serialize_with_struct() { + let test_with_struct = TestWithStruct { a: 1u128, b: 2u64 }; + + let data = test_with_struct.to_capnp_bytes(); + let test_with_struct2 = TestWithStruct::from_capnp_bytes(&data).unwrap(); + + assert_eq!(test_with_struct, test_with_struct2); +} + +#[capnp_conv(test_capnp::test_with_enum)] +#[derive(Debug, Clone, PartialEq)] +enum TestWithEnum { + #[capnp_conv(with = Wrapper)] + VarA(u128), + VarB(u64), + VarC, +} + +#[test] +fn capnp_serialize_with_enum() { + let test_with_enum = TestWithEnum::VarA(1u128); + + let data = test_with_enum.to_capnp_bytes(); + let test_with_enum2 = TestWithEnum::from_capnp_bytes(&data).unwrap(); + + assert_eq!(test_with_enum, test_with_enum2); +}