diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39ff86ae0..65e89b53f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,7 +153,7 @@ jobs: ARGS: --no-default-features --features ${{ matrix.runtime || 'apple' }} ${{ matrix.args }} # Use --no-fail-fast, except with dinghy TESTARGS: ${{ matrix.dinghy && ' ' || '--no-fail-fast' }} ${{ matrix.test-args }} - FEATURES: ${{ matrix.features || 'malloc,block,exception,catch_all,verify_message' }} + FEATURES: ${{ matrix.features || 'malloc,block,exception,catch_all,verify_message,derive' }} UNSTABLE_FEATURES: ${{ matrix.unstable-features || 'unstable-autoreleasesafe' }} runs-on: ${{ matrix.os }} diff --git a/objc2-encode/Cargo.toml b/objc2-encode/Cargo.toml index f8c9d8d26..2b70870d1 100644 --- a/objc2-encode/Cargo.toml +++ b/objc2-encode/Cargo.toml @@ -18,6 +18,12 @@ repository = "https://github.com/madsmtm/objc2" documentation = "https://docs.rs/objc2-encode/" license = "MIT" +[features] +derive = ["objc2-proc-macros"] + +[dependencies] +objc2-proc-macros = { path = "../objc2-proc-macros", optional = true } + [package.metadata.docs.rs] default-target = "x86_64-apple-darwin" diff --git a/objc2-encode/src/lib.rs b/objc2-encode/src/lib.rs index ac4e170f8..d59ad56d6 100644 --- a/objc2-encode/src/lib.rs +++ b/objc2-encode/src/lib.rs @@ -115,3 +115,5 @@ mod static_str; pub use self::encode::{Encode, EncodeArguments, RefEncode}; pub use self::encoding::Encoding; +#[cfg(feature = "derive")] +pub use objc2_proc_macros::{Encode, RefEncode}; diff --git a/objc2-encode/tests/test_derive.rs b/objc2-encode/tests/test_derive.rs new file mode 100644 index 000000000..fe9f7f308 --- /dev/null +++ b/objc2-encode/tests/test_derive.rs @@ -0,0 +1,47 @@ +#![cfg(feature = "derive")] +use objc2_encode::{Encode, Encoding, RefEncode}; + +#[cfg(target_pointer_width = "32")] +type CGFloat = f32; + +#[cfg(target_pointer_width = "64")] +type CGFloat = f64; + +#[derive(Encode, RefEncode)] +#[repr(C)] +struct CGPoint { + x: CGFloat, + y: CGFloat, +} + +#[test] +fn cgpoint() { + let enc = Encoding::Struct("CGPoint", &[CGFloat::ENCODING, CGFloat::ENCODING]); + assert_eq!(CGPoint::ENCODING, enc); + assert_eq!(CGPoint::ENCODING_REF, Encoding::Pointer(&enc)); +} + +#[derive(Encode, RefEncode)] +#[repr(transparent)] +struct Transparent { + _inner: usize, +} + +#[test] +fn transparent() { + assert_eq!(Transparent::ENCODING, usize::ENCODING); + assert_eq!(Transparent::ENCODING_REF, usize::ENCODING_REF); +} + +#[derive(Encode, RefEncode)] +#[repr(usize)] +enum MyEnum { + _A = 1, + _B = 2, +} + +#[test] +fn enum_repr() { + assert_eq!(MyEnum::ENCODING, usize::ENCODING); + assert_eq!(MyEnum::ENCODING_REF, usize::ENCODING_REF); +} diff --git a/objc2-proc-macros/Cargo.toml b/objc2-proc-macros/Cargo.toml index 2e017b637..fd151d575 100644 --- a/objc2-proc-macros/Cargo.toml +++ b/objc2-proc-macros/Cargo.toml @@ -30,5 +30,10 @@ gnustep-1-9 = ["gnustep-1-8"] gnustep-2-0 = ["gnustep-1-9"] gnustep-2-1 = ["gnustep-2-0"] +[dependencies] +syn = "1.0" +quote = "1.0" +# TODO: Use `proc-macro-crate` or similar for better usage downstream? + [package.metadata.docs.rs] default-target = "x86_64-apple-darwin" diff --git a/objc2-proc-macros/src/derive.rs b/objc2-proc-macros/src/derive.rs new file mode 100644 index 000000000..db1c9e97e --- /dev/null +++ b/objc2-proc-macros/src/derive.rs @@ -0,0 +1,82 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{Data, DeriveInput}; + +use crate::utils::get_repr; + +pub(crate) fn impl_encode(ast: &DeriveInput) -> TokenStream { + let DeriveInput { + ident, data, attrs, .. + } = ast; + + let repr = match get_repr(&attrs) { + Some(repr) => repr, + None => panic!("Missing repr"), + }; + + let encoding = match data { + Data::Struct(data) => { + let fields = data + .fields + .iter() + .map(|field| &field.ty) + .collect::>(); + match &*repr.to_string() { + "transparent" => { + let field = fields[0]; + assert_eq!(fields.len(), 1, "Expected one item"); + quote!(<#field as ::objc2_encode::Encode>::ENCODING) + } + "C" => { + quote!( + ::objc2_encode::Encoding::Struct( + stringify!(#ident), + &[#(<#fields as ::objc2_encode::Encode>::ENCODING),*], + ) + ) + } + _ => panic!("Unknown repr"), + } + } + Data::Enum(_) => { + let ty = match &*repr.to_string() { + "usize" => quote! { core::primitive::usize }, + "isize" => quote! { core::primitive::isize }, + "u64" => quote! { core::primitive::u64 }, + "i64" => quote! { core::primitive::i64 }, + "u32" => quote! { core::primitive::u32 }, + "i32" => quote! { core::primitive::i32 }, + "u16" => quote! { core::primitive::u16 }, + "i16" => quote! { core::primitive::i16 }, + "u8" => quote! { core::primitive::u8 }, + "i8" => quote! { core::primitive::i8 }, + _ => panic!("Unknown repr"), + }; + quote! { <#ty as ::objc2_encode::Encode>::ENCODING } + } + Data::Union(_) => unimplemented!(), + }; + + // TODO: Generics + quote! { + unsafe impl ::objc2_encode::Encode for #ident { + const ENCODING: ::objc2_encode::Encoding<'static> = #encoding; + } + } + .into() +} + +pub(crate) fn impl_ref_encode(ast: &DeriveInput) -> TokenStream { + let DeriveInput { ident, .. } = ast; + // TODO: Generics + // TODO: Objects + + let gen = quote! { + unsafe impl ::objc2_encode::RefEncode for #ident { + const ENCODING_REF: ::objc2_encode::Encoding<'static> = ::objc2_encode::Encoding::Pointer( + &::ENCODING + ); + } + }; + gen.into() +} diff --git a/objc2-proc-macros/src/lib.rs b/objc2-proc-macros/src/lib.rs index d31452f14..5f13e13c5 100644 --- a/objc2-proc-macros/src/lib.rs +++ b/objc2-proc-macros/src/lib.rs @@ -16,3 +16,22 @@ #[cfg(doctest)] #[doc = include_str!("../README.md")] extern "C" {} + +use proc_macro::TokenStream; + +mod derive; +mod utils; + +/// TODO +#[proc_macro_derive(Encode)] +pub fn encode_derive(input: TokenStream) -> TokenStream { + let ast = syn::parse(input).unwrap(); + derive::impl_encode(&ast) +} + +/// TODO +#[proc_macro_derive(RefEncode)] +pub fn ref_encode_derive(input: TokenStream) -> TokenStream { + let ast = syn::parse(input).unwrap(); + derive::impl_ref_encode(&ast) +} diff --git a/objc2-proc-macros/src/utils.rs b/objc2-proc-macros/src/utils.rs new file mode 100644 index 000000000..571392922 --- /dev/null +++ b/objc2-proc-macros/src/utils.rs @@ -0,0 +1,30 @@ +use syn::{Attribute, Ident, Meta, NestedMeta}; + +// Taken from `nom-derive`: +// https://github.com/rust-bakery/nom-derive/blob/5315891a0016b15094d4d0201f7d3ac803e4fc57/nom-derive-impl/src/enums.rs#L60-L90 +pub(crate) fn get_repr(attrs: &[Attribute]) -> Option { + for attr in attrs { + if let Ok(Meta::List(metalist)) = attr.parse_meta() { + if let Some(ident) = metalist.path.get_ident() { + if ident == "repr" { + for n in metalist.nested.iter() { + match n { + NestedMeta::Meta(meta) => match meta { + Meta::Path(path) => { + if let Some(word) = path.get_ident() { + return Some(word.clone()); + } else { + panic!("unsupported nested type for 'repr'") + } + } + _ => panic!("unsupported nested type for 'repr'"), + }, + _ => panic!("unsupported meta type for 'repr'"), + } + } + } + } + } + } + None +}