diff --git a/Cargo.lock b/Cargo.lock index 61b6d890d..1fee83f48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "basic-toml" version = "0.1.1" @@ -37,6 +43,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "block-sys" version = "0.2.0" @@ -129,6 +141,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "header-translator" version = "0.1.0" @@ -179,6 +197,16 @@ dependencies = [ "objc2", ] +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "itoa" version = "1.0.5" @@ -207,6 +235,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "linkme" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22bcb06ef182e7557cf18d85bd151319d657bd8f699d381435781871f3027af8" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83f2011c1121c45eb4d9639cf5dcbae9622d2978fc5e922a346bfdc6c46700b5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "log" version = "0.4.17" @@ -231,6 +279,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "nom8" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" +dependencies = [ + "memchr", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -241,6 +298,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "num_enum" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d829733185c1ca374f17e52b762f24f535ec625d2cc1f070e34c8a9068f341b" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be1598bf1c313dcdd12092e3f1920f463462525a21b7b4e11b4168353d0123e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "objc-sys" version = "0.3.0" @@ -252,8 +330,11 @@ dependencies = [ name = "objc2" version = "0.3.0-beta.5" dependencies = [ + "bitflags", "iai", + "linkme", "malloc_buf", + "num_enum", "objc-sys", "objc2-encode", "objc2-proc-macros", @@ -266,6 +347,13 @@ version = "2.0.0-pre.4" [[package]] name = "objc2-proc-macros" version = "0.1.1" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] [[package]] name = "once_cell" @@ -291,6 +379,40 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +[[package]] +name = "proc-macro-crate" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66618389e4ec1c7afe67d51a9bf34ff9236480f8d51e7489b7d5ab0303c13f34" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.50" @@ -554,6 +676,23 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5" + +[[package]] +name = "toml_edit" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b" +dependencies = [ + "indexmap", + "nom8", + "toml_datetime", +] + [[package]] name = "tracing" version = "0.1.37" @@ -639,6 +778,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "winapi" version = "0.3.9" diff --git a/crates/objc2-proc-macros/Cargo.toml b/crates/objc2-proc-macros/Cargo.toml index 6ca19ff8e..dcabed675 100644 --- a/crates/objc2-proc-macros/Cargo.toml +++ b/crates/objc2-proc-macros/Cargo.toml @@ -33,3 +33,10 @@ gnustep-2-1 = ["gnustep-2-0"] [package.metadata.docs.rs] default-target = "x86_64-apple-darwin" + +[dependencies] +heck = "0.4" +proc-macro2 = "1.0" +proc-macro-error = "1.0" +quote = "1.0" +syn = { version = "1.0", features = ["full"] } diff --git a/crates/objc2-proc-macros/src/constant.rs b/crates/objc2-proc-macros/src/constant.rs new file mode 100644 index 000000000..69db5b571 --- /dev/null +++ b/crates/objc2-proc-macros/src/constant.rs @@ -0,0 +1,30 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens, TokenStreamExt}; + +pub(crate) fn item_static( + attr: TokenStream, + item_static: crate::objc::ItemStatic, +) -> syn::Result { + if !attr.is_empty() { + return Err(crate::objc::error_unexpected_arguments(attr)); + } + let mut tokens = TokenStream::new(); + tokens.append_all(item_static.attrs); + item_static.vis.to_tokens(&mut tokens); + item_static.static_token.to_tokens(&mut tokens); + item_static.mutability.to_tokens(&mut tokens); + item_static.ident.to_tokens(&mut tokens); + item_static.colon_token.to_tokens(&mut tokens); + item_static.ty.to_tokens(&mut tokens); + if let Some((eq_token, expr)) = &item_static.body { + eq_token.to_tokens(&mut tokens); + expr.to_tokens(&mut tokens); + } + item_static.semi_token.to_tokens(&mut tokens); + let tokens = if item_static.body.is_some() { + tokens + } else { + quote!(extern "C" { #tokens }) + }; + Ok(tokens) +} diff --git a/crates/objc2-proc-macros/src/enumeration.rs b/crates/objc2-proc-macros/src/enumeration.rs new file mode 100644 index 000000000..b7dee211b --- /dev/null +++ b/crates/objc2-proc-macros/src/enumeration.rs @@ -0,0 +1,380 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens, TokenStreamExt}; +use syn::{ + parse::{Parse, ParseStream, Parser}, + spanned::Spanned, +}; + +// TODO: derive options +// TODO: naming options for generated constants/variants + +enum Extensibility { + Closed, + Open, +} + +impl ToTokens for Extensibility { + fn to_tokens(&self, tokens: &mut TokenStream) { + tokens.append_all(match self { + Extensibility::Closed => quote!(), + Extensibility::Open => quote!(#[non_exhaustive]), + }) + } +} + +mod macro_args { + pub(super) mod keyword { + syn::custom_keyword!(closed_enum); + syn::custom_keyword!(error_enum); + syn::custom_keyword!(domain); + syn::custom_keyword!(options); + syn::custom_keyword!(typed_enum); + syn::custom_keyword!(typed_extensible_enum); + syn::custom_keyword!(repr); + } +} + +// NOTE: unused for spans +#[allow(unused)] +enum MacroArgs { + Options { + options_token: macro_args::keyword::options, + repr: KeyVal, + }, + Enum { + closed_enum_token: macro_args::keyword::closed_enum, + repr: KeyVal, + }, + ErrorEnum { + error_enum_token: macro_args::keyword::error_enum, + domain: KeyVal, + }, + OpenEnum { + enum_token: syn::Token![enum], + repr: KeyVal, + }, + TypedEnum { + typed_enum_token: macro_args::keyword::typed_enum, + repr: KeyVal, + }, + OpenTypedEnum { + typed_extensible_enum_token: macro_args::keyword::typed_extensible_enum, + repr: KeyVal, + }, +} + +impl Parse for MacroArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + let lookahead = input.lookahead1(); + + if lookahead.peek(macro_args::keyword::options) { + return Ok(MacroArgs::Options { + options_token: input.parse()?, + repr: input.parse()?, + }); + } + if lookahead.peek(macro_args::keyword::closed_enum) { + return Ok(MacroArgs::Enum { + closed_enum_token: input.parse()?, + repr: input.parse()?, + }); + } + if lookahead.peek(syn::Token![enum]) { + return Ok(MacroArgs::OpenEnum { + enum_token: input.parse()?, + repr: input.parse()?, + }); + } + if lookahead.peek(macro_args::keyword::typed_enum) { + return Ok(MacroArgs::TypedEnum { + typed_enum_token: input.parse()?, + repr: input.parse()?, + }); + } + if lookahead.peek(macro_args::keyword::typed_extensible_enum) { + return Ok(MacroArgs::OpenTypedEnum { + typed_extensible_enum_token: input.parse()?, + repr: input.parse()?, + }); + } + if lookahead.peek(macro_args::keyword::error_enum) { + return Ok(MacroArgs::ErrorEnum { + error_enum_token: input.parse()?, + domain: input.parse()?, + }); + } + + let span = input.span(); + let message = "#[objc]: must specify type of enum as first argument"; + let mut error = syn::Error::new(span, message); + error.combine(lookahead.error()); + Err(error) + } +} + +// NOTE: unused for spans +#[allow(unused)] +struct KeyVal { + comma_token: syn::Token![,], + key_token: K, + eq_token: syn::Token![=], + val: V, +} + +impl Parse for KeyVal { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(KeyVal { + comma_token: input.parse()?, + key_token: input.parse()?, + eq_token: input.parse()?, + val: input.parse()?, + }) + } +} + +pub(crate) fn item_enum(attr: TokenStream, item_enum: syn::ItemEnum) -> syn::Result { + use Extensibility::*; + + if !item_enum.generics.params.is_empty() { + let span = item_enum.generics.span(); + let message = "#[objc]: enums with generics are not supported"; + return Err(syn::Error::new(span, message)); + } + + if item_enum.generics.where_clause.is_some() { + let span = item_enum.generics.where_clause.span(); + let message = "#[objc]: enums with `where` clauses are not supported"; + return Err(syn::Error::new(span, message)); + } + + for variant in &item_enum.variants { + if !matches!(variant.fields, syn::Fields::Unit) { + let span = variant.fields.span(); + let message = "#[objc]: variants with fields are not supported"; + return Err(syn::Error::new(span, message)); + } + } + + let mut args = Parser::parse2(MacroArgs::parse, attr)?; + + // NOTE: special case handling for `#[repr(NSU?Integer)]` + match &mut args { + MacroArgs::Enum { + repr: + KeyVal { + val: syn::Type::Path(tp), + .. + }, + .. + } + | MacroArgs::OpenEnum { + repr: + KeyVal { + val: syn::Type::Path(tp), + .. + }, + .. + } + | MacroArgs::Options { + repr: + KeyVal { + val: syn::Type::Path(tp), + .. + }, + .. + } => { + if false { + } else if tp.path.is_ident("NSInteger") { + // rewrite `NSInteger` as `isize` + *tp = syn::parse_str("isize")?; + } else if tp.path.is_ident("NSUInteger") { + // rewrite `NSUnteger` as `usize` + *tp = syn::parse_str("usize")?; + } + } + _ => {} + } + + match args { + MacroArgs::Options { repr, .. } => ns_options(item_enum, repr.val), + MacroArgs::Enum { repr, .. } => ns_enum(item_enum, Closed, repr.val), + MacroArgs::OpenEnum { repr, .. } => ns_enum(item_enum, Open, repr.val), + MacroArgs::TypedEnum { repr, .. } => ns_typed_enum(item_enum, Closed, repr.val), + MacroArgs::OpenTypedEnum { repr, .. } => ns_typed_enum(item_enum, Open, repr.val), + MacroArgs::ErrorEnum { domain, .. } => ns_error_enum(item_enum, domain.val), + } +} + +fn ns_options(item_enum: syn::ItemEnum, ty: syn::Type) -> syn::Result { + let syn::ItemEnum { + attrs, + vis, + ident: enum_ident, + variants, + .. + } = item_enum; + + let mut options = TokenStream::new(); + for variant in variants { + let span = variant.span(); + let attrs = variant.attrs; + let variant_ident = variant.ident; + if let Some((_, expr)) = variant.discriminant { + options.append_all(quote!( + #(#attrs)* + const #variant_ident = #expr; + )); + } else { + let message = + "#[objc]: all options variants must have explicit values (` = `)"; + return Err(syn::Error::new(span, message)); + } + } + + Ok(quote!(objc2::bitflags::bitflags! { + #(#attrs)* + #vis struct #enum_ident : #ty { + #options + } + })) +} + +fn ns_enum( + item_enum: syn::ItemEnum, + extensibility: Extensibility, + ty: syn::Type, +) -> syn::Result { + Ok(quote!( + #[derive(objc2::num_enum::IntoPrimitive)] + #[repr(#ty)] + #extensibility + #item_enum + )) +} + +fn ns_typed_enum( + item_enum: syn::ItemEnum, + extensibility: Extensibility, + ty: syn::Type, +) -> syn::Result { + let syn::ItemEnum { + // FIXME: what to do with attrs? + // attrs, + vis, + ident: struct_ident, + variants, + .. + } = item_enum; + + let enum_mod_ident = { + use heck::ToSnakeCase; + let struct_ident = struct_ident.to_string(); + let string = struct_ident.to_snake_case(); + format_ident!("r#{}", string) + }; + let enum_ident = quote!(#enum_mod_ident::Cases); + + let (into_enum_ty, into_enum_case_default) = match extensibility { + Extensibility::Closed => ( + quote!(#enum_ident), + quote!(_wrapper => unreachable!("unexpected case in (closed) typed enum"),), + ), + Extensibility::Open => ( + quote!(core::option::Option<#enum_ident>), + quote!(_ => None,), + ), + }; + + let mut struct_constants = TokenStream::new(); + let mut enum_variants = TokenStream::new(); + let mut into_cases = TokenStream::new(); + let mut from_cases = TokenStream::new(); + + // TODO: check for value uniqueness? + for variant in variants { + let span = variant.span(); + // FIXME: what to do with attrs? + let variant_ident = variant.ident; + if let Some((_, expr)) = variant.discriminant { + struct_constants.append_all(quote!( + const #variant_ident: Self = Self(#expr); + )); + enum_variants.append_all(quote!( + #variant_ident, + )); + into_cases.append_all(quote!( + &Self::#variant_ident => #enum_ident::#variant_ident.into(), + )); + from_cases.append_all(quote!( + #enum_ident::#variant_ident => #struct_ident::#variant_ident, + )); + } else { + let message = + "#[objc]: all typed enum variants must have explicit values (` = `)"; + return Err(syn::Error::new(span, message)); + } + } + + Ok(quote!( + #[derive(Eq, PartialEq)] + #[repr(transparent)] + #vis struct #struct_ident(#ty); + impl #struct_ident { + #![allow(non_upper_case_globals)] + #struct_constants + pub fn cases(&self) -> #into_enum_ty { + match self { + #into_cases + #into_enum_case_default + } + } + #[inline] + pub fn peek(&self) -> &#ty { + &self.0 + } + #[inline] + pub fn take(self) -> #ty { + self.0 + } + } + impl From<#enum_ident> for #struct_ident { + fn from(case: #enum_ident) -> Self { + match case { + #from_cases + } + } + } + impl core::convert::From<#struct_ident> for #ty { + #[inline] + fn from(wrapper: #struct_ident) -> Self { + wrapper.take() + } + } + impl From<#enum_ident> for #ty { + #[inline] + fn from(case: #enum_ident) -> Self { + Self::from(#struct_ident::from(case)) + } + } + + #[allow(non_snake_case)] + #vis mod #enum_mod_ident { + #[allow(non_camel_case_types)] + #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] + #extensibility + pub enum Cases { + #enum_variants + } + } + unsafe impl objc2::Enum for #struct_ident { + type Cases = #enum_ident; + } + )) +} + +fn ns_error_enum(item_enum: syn::ItemEnum, _domain: syn::Expr) -> syn::Result { + let extensibility = Extensibility::Open; + let ty = syn::parse_str("isize")?; + ns_typed_enum(item_enum, extensibility, ty) + // TODO +} diff --git a/crates/objc2-proc-macros/src/function.rs b/crates/objc2-proc-macros/src/function.rs new file mode 100644 index 000000000..428e17732 --- /dev/null +++ b/crates/objc2-proc-macros/src/function.rs @@ -0,0 +1,53 @@ +use proc_macro2::TokenStream; +use quote::{quote, TokenStreamExt}; +use syn::spanned::Spanned; + +pub(crate) fn item_fn(attr: TokenStream, item_fn: syn::ItemFn) -> syn::Result { + if !attr.is_empty() { + return Err(crate::objc::error_unexpected_arguments(attr)); + } + let mut tokens = TokenStream::new(); + + let syn::ItemFn { + mut attrs, + vis, + mut sig, + .. + } = item_fn; + + if sig.unsafety.is_some() { + sig.unsafety = None; + for attr in attrs.iter_mut() { + attr.style = syn::AttrStyle::Outer; + } + tokens.append_all(quote!( + extern "C" { + #(#attrs)* + #vis #sig; + } + )); + } else if sig.abi.is_none() { + let syn::Signature { ident, inputs, .. } = &sig; + let args = inputs.iter().filter_map(|arg| match arg { + syn::FnArg::Receiver(_) => None, + syn::FnArg::Typed(pat_type) => Some(&*pat_type.pat), + }); + tokens.append_all(quote!( + #(#attrs)* + #vis extern "C" #sig { + extern "C" { + #sig; + } + unsafe { + #ident(#(#args,)*) + } + } + )); + } else { + let span = sig.abi.span(); + let message = "#[objc]: cannot be applied to functions with an explicit ABI"; + return Err(syn::Error::new(span, message)); + } + + Ok(tokens) +} diff --git a/crates/objc2-proc-macros/src/implementation.rs b/crates/objc2-proc-macros/src/implementation.rs new file mode 100644 index 000000000..c4731b725 --- /dev/null +++ b/crates/objc2-proc-macros/src/implementation.rs @@ -0,0 +1,26 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::{parse::Parser, punctuated::Punctuated, spanned::Spanned}; + +pub(crate) fn item_impl( + attr: TokenStream, + mut item_impl: syn::ItemImpl, +) -> syn::Result { + let meta = Parser::parse2( + Punctuated::::parse_terminated, + attr, + )?; + match meta.first() { + Some(syn::Meta::Path(path)) if path.is_ident("implementation") => {} + _ => { + let span = meta.span(); + let message = "#[objc]: use `#[objc(implementation)] impl C { ... }` for `impl` items"; + return Err(syn::Error::new(span, message)); + } + } + // NOTE: adjustment since inherent `impl` cannot be unsafe + if item_impl.trait_.is_none() && item_impl.unsafety.is_some() { + item_impl.unsafety = None; + } + Ok(item_impl.to_token_stream()) +} diff --git a/crates/objc2-proc-macros/src/interface.rs b/crates/objc2-proc-macros/src/interface.rs new file mode 100644 index 000000000..a31404664 --- /dev/null +++ b/crates/objc2-proc-macros/src/interface.rs @@ -0,0 +1,54 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{parse::Parser, punctuated::Punctuated, spanned::Spanned}; + +pub(crate) fn item_struct( + attr: TokenStream, + item_struct: syn::ItemStruct, +) -> syn::Result { + let meta = Parser::parse2( + Punctuated::::parse_terminated, + attr, + )?; + match meta.first() { + Some(syn::Meta::Path(path)) if path.is_ident("interface") => {} + _ => { + let span = meta.span(); + let message = "#[objc]: use `#[objc(interface)] struct C { ... }` for `struct` items"; + return Err(syn::Error::new(span, message)); + } + } + Ok(item_struct.to_token_stream()) +} + +pub(crate) fn item_type( + attr: TokenStream, + item_type: crate::objc::ItemType, +) -> syn::Result { + let meta = Parser::parse2( + Punctuated::::parse_terminated, + attr, + )?; + match meta.first() { + Some(syn::Meta::Path(path)) if path.is_ident("interface") => {} + _ => { + let span = meta.span(); + let message = "#[objc]: use `#[objc(interface)] type C;` for `type` items"; + return Err(syn::Error::new(span, message)); + } + } + let crate::objc::ItemType { + attrs, + vis, + ident, + generics, + semi_token, + .. + } = item_type; + let mut tokens = TokenStream::new(); + tokens.append_all(quote!( + #(#attrs)* + #vis struct #ident #generics #semi_token + )); + Ok(tokens) +} diff --git a/crates/objc2-proc-macros/src/lib.rs b/crates/objc2-proc-macros/src/lib.rs index 1c905f20f..55157d5de 100644 --- a/crates/objc2-proc-macros/src/lib.rs +++ b/crates/objc2-proc-macros/src/lib.rs @@ -24,6 +24,14 @@ use proc_macro::Literal; use proc_macro::TokenStream; use proc_macro::TokenTree; +mod constant; +mod enumeration; +mod function; +mod implementation; +mod interface; +mod objc; +mod protocol; + /// Extract all identifiers in the given tokenstream. fn get_idents(input: TokenStream) -> impl Iterator { input.into_iter().flat_map(|token| { @@ -72,3 +80,14 @@ pub fn __hash_idents(input: TokenStream) -> TokenStream { let s = format!("{:016x}", hasher.finish()); TokenTree::Literal(Literal::string(&s)).into() } + +#[allow(missing_docs)] +#[proc_macro_attribute] +pub fn objc(attr: TokenStream, item: TokenStream) -> TokenStream { + let attr = attr.into(); + let item = item.into(); + match crate::objc::objc(attr, item) { + Ok(value) => value.into(), + Err(error) => error.to_compile_error().into(), + } +} diff --git a/crates/objc2-proc-macros/src/objc.rs b/crates/objc2-proc-macros/src/objc.rs new file mode 100644 index 000000000..4472f0f7f --- /dev/null +++ b/crates/objc2-proc-macros/src/objc.rs @@ -0,0 +1,557 @@ +use proc_macro2::{Punct, Spacing, TokenStream, TokenTree}; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + spanned::Spanned, +}; + +pub(crate) fn objc(attr: TokenStream, item: TokenStream) -> syn::Result { + let item = syn::parse2::(item)?; + match item { + ItemObjC::ItemEnum(item_enum) => crate::enumeration::item_enum(attr, item_enum), + ItemObjC::ItemFn(item_fn) => crate::function::item_fn(attr, item_fn), + ItemObjC::ItemImpl(item_impl) => crate::implementation::item_impl(attr, item_impl), + ItemObjC::ItemStatic(item_static) => crate::constant::item_static(attr, item_static), + ItemObjC::ItemStruct(item_struct) => crate::interface::item_struct(attr, item_struct), + ItemObjC::ItemTrait(item_trait) => crate::protocol::item_trait(attr, item_trait), + ItemObjC::ItemType(item_type) => crate::interface::item_type(attr, item_type), + } +} + +pub(crate) enum ItemObjC { + ItemEnum(syn::ItemEnum), + ItemFn(syn::ItemFn), + ItemImpl(syn::ItemImpl), + ItemStatic(self::ItemStatic), + ItemStruct(syn::ItemStruct), + ItemTrait(syn::ItemTrait), + ItemType(self::ItemType), +} + +impl Parse for ItemObjC { + fn parse(input: ParseStream<'_>) -> syn::Result { + let attrs = input.call(syn::Attribute::parse_outer)?; + let vis = input.parse()?; + + let mut lookahead = input.lookahead1(); + + if lookahead.peek(syn::Token![enum]) { + let item_enum = parse_rest_of_enum(input, attrs, vis)?; + return Ok(ItemObjC::ItemEnum(item_enum)); + } + if lookahead.peek(syn::Token![static]) { + let item_static = parse_rest_of_static(input, attrs, vis)?; + return Ok(ItemObjC::ItemStatic(item_static)); + } + if lookahead.peek(syn::Token![struct]) { + let item_struct = parse_rest_of_struct(input, attrs, vis)?; + return Ok(ItemObjC::ItemStruct(item_struct)); + } + if lookahead.peek(syn::Token![type]) { + let item_type = parse_rest_of_type(input, attrs, vis)?; + return Ok(ItemObjC::ItemType(item_type)); + } + + let mut constness = None; + let mut asyncness = None; + let mut defaultness = None; + let mut unsafety = None; + let mut abi = None; + let mut auto_token = None; + + if lookahead.peek(syn::Token![const]) { + constness = input.parse()?; + lookahead = input.lookahead1(); + } + if lookahead.peek(syn::Token![async]) { + asyncness = input.parse()?; + lookahead = input.lookahead1(); + } + if lookahead.peek(syn::Token![default]) { + defaultness = input.parse()?; + lookahead = input.lookahead1(); + } + if lookahead.peek(syn::Token![unsafe]) { + unsafety = input.parse()?; + lookahead = input.lookahead1(); + } + if lookahead.peek(syn::Token![auto]) { + auto_token = input.parse()?; + lookahead = input.lookahead1(); + } + if lookahead.peek(syn::Token![extern]) { + abi = input.parse()?; + lookahead = input.lookahead1(); + } + + if lookahead.peek(syn::Token![fn]) { + let item_fn = parse_rest_of_fn(input, attrs, vis, constness, asyncness, unsafety, abi)?; + return Ok(ItemObjC::ItemFn(item_fn)); + } + if lookahead.peek(syn::Token![impl]) { + let item_impl = parse_rest_of_impl(input, attrs, defaultness, unsafety)?; + return Ok(ItemObjC::ItemImpl(item_impl)); + } + if lookahead.peek(syn::Token![trait]) { + let item_trait = parse_rest_of_trait(input, attrs, vis, unsafety, auto_token)?; + return Ok(ItemObjC::ItemTrait(item_trait)); + } + + let span = input.span(); + let message = + "#[objc]: can only apply to `enum`, `fn`, `impl`, `static`, `struct`, `trait`, or `type` items"; + let mut error = syn::Error::new(span, message); + error.combine(lookahead.error()); + + Err(error) + } +} + +pub(crate) struct ItemStatic { + pub(crate) attrs: Vec, + pub(crate) vis: syn::Visibility, + pub(crate) static_token: syn::Token![static], + pub(crate) mutability: Option, + pub(crate) ident: syn::Ident, + pub(crate) colon_token: syn::Token![:], + pub(crate) ty: Box, + pub(crate) body: Option<(syn::Token![=], Box)>, + pub(crate) semi_token: syn::Token![;], +} + +#[allow(unused)] +pub(crate) struct ItemType { + pub(crate) attrs: Vec, + pub(crate) vis: syn::Visibility, + pub(crate) type_token: syn::Token![type], + pub(crate) ident: syn::Ident, + pub(crate) generics: syn::Generics, + pub(crate) semi_token: syn::Token![;], +} + +fn data_enum( + input: ParseStream<'_>, +) -> syn::Result<( + Option, + syn::token::Brace, + syn::punctuated::Punctuated, +)> { + let where_clause = input.parse()?; + + let content; + let brace = syn::braced!(content in input); + let variants = content.parse_terminated(syn::Variant::parse)?; + + Ok((where_clause, brace, variants)) +} + +fn data_struct( + input: ParseStream<'_>, +) -> syn::Result<( + Option, + syn::Fields, + Option, +)> { + let mut lookahead = input.lookahead1(); + let mut where_clause = None; + if lookahead.peek(syn::Token![where]) { + where_clause = Some(input.parse()?); + lookahead = input.lookahead1(); + } + + if where_clause.is_none() && lookahead.peek(syn::token::Paren) { + let fields = input.parse()?; + + lookahead = input.lookahead1(); + if lookahead.peek(syn::Token![where]) { + where_clause = Some(input.parse()?); + lookahead = input.lookahead1(); + } + + if lookahead.peek(syn::Token![;]) { + let semi = input.parse()?; + Ok((where_clause, syn::Fields::Unnamed(fields), Some(semi))) + } else { + Err(lookahead.error()) + } + } else if lookahead.peek(syn::token::Brace) { + let fields = input.parse()?; + Ok((where_clause, syn::Fields::Named(fields), None)) + } else if lookahead.peek(syn::Token![;]) { + let semi = input.parse()?; + Ok((where_clause, syn::Fields::Unit, Some(semi))) + } else { + Err(lookahead.error()) + } +} + +fn parse_fn_args(input: ParseStream<'_>) -> syn::Result> { + let mut args = Punctuated::new(); + let mut has_receiver = false; + + while !input.is_empty() { + let attrs = input.call(syn::Attribute::parse_outer)?; + + let arg = if let Some(dots) = input.parse::>()? { + syn::FnArg::Typed(syn::PatType { + attrs, + pat: Box::new(syn::Pat::Verbatim(variadic_to_tokens(&dots))), + colon_token: syn::Token![:](dots.spans[0]), + ty: Box::new(syn::Type::Verbatim(variadic_to_tokens(&dots))), + }) + } else { + let mut arg: syn::FnArg = input.parse()?; + match &mut arg { + syn::FnArg::Receiver(receiver) if has_receiver => { + return Err(syn::Error::new( + receiver.self_token.span, + "unexpected second method receiver", + )); + } + syn::FnArg::Receiver(receiver) if !args.is_empty() => { + return Err(syn::Error::new( + receiver.self_token.span, + "unexpected method receiver", + )); + } + syn::FnArg::Receiver(receiver) => { + has_receiver = true; + receiver.attrs = attrs; + } + syn::FnArg::Typed(arg) => arg.attrs = attrs, + } + arg + }; + args.push_value(arg); + + if input.is_empty() { + break; + } + + let comma: syn::Token![,] = input.parse()?; + args.push_punct(comma); + } + + Ok(args) +} + +fn parse_rest_of_enum( + input: ParseStream<'_>, + attrs: Vec, + vis: syn::Visibility, +) -> syn::Result { + let enum_token = input.parse::()?; + let ident = input.parse::()?; + let generics = input.parse::()?; + let (where_clause, brace_token, variants) = data_enum(input)?; + Ok(syn::ItemEnum { + attrs, + vis, + enum_token, + ident, + generics: syn::Generics { + where_clause, + ..generics + }, + brace_token, + variants, + }) +} + +fn parse_rest_of_fn( + input: ParseStream<'_>, + mut attrs: Vec, + vis: syn::Visibility, + constness: Option, + asyncness: Option, + unsafety: Option, + abi: Option, +) -> syn::Result { + let sig = { + let fn_token = input.parse()?; + let ident = input.parse()?; + let mut generics = input.parse::()?; + let content; + let paren_token = syn::parenthesized!(content in input); + let mut inputs = parse_fn_args(&content)?; + let variadic = pop_variadic(&mut inputs); + let output = input.parse()?; + generics.where_clause = input.parse()?; + syn::Signature { + constness, + asyncness, + unsafety, + abi, + fn_token, + ident, + generics, + paren_token, + inputs, + variadic, + output, + } + }; + let block = if let Some(semi) = input.parse::>()? { + let mut punct = Punct::new(';', Spacing::Alone); + punct.set_span(semi.span); + let tokens = TokenStream::from_iter(vec![TokenTree::Punct(punct)]); + syn::Block { + brace_token: syn::token::Brace { span: semi.span }, + stmts: vec![syn::Stmt::Item(syn::Item::Verbatim(tokens))], + } + } else { + let content; + let brace_token = syn::braced!(content in input); + attrs.extend(content.call(syn::Attribute::parse_inner)?); + syn::Block { + brace_token, + stmts: content.call(syn::Block::parse_within)?, + } + }; + Ok(syn::ItemFn { + attrs, + vis, + sig, + block: Box::new(block), + }) +} + +fn parse_rest_of_impl( + input: ParseStream<'_>, + mut attrs: Vec, + defaultness: Option, + unsafety: Option, +) -> syn::Result { + let impl_token = input.parse::()?; + + let has_generics = input.peek(syn::Token![<]) + && (input.peek2(syn::Token![>]) + || input.peek2(syn::Token![#]) + || (input.peek2(syn::Ident) || input.peek2(syn::Lifetime)) + && (input.peek3(syn::Token![:]) + || input.peek3(syn::Token![,]) + || input.peek3(syn::Token![>]) + || input.peek3(syn::Token![=])) + || input.peek2(syn::Token![const])); + let mut generics: syn::Generics = if has_generics { + input.parse()? + } else { + syn::Generics::default() + }; + + let self_ty: syn::Type = input.parse()?; + + if input.peek(syn::Token![for]) { + let span = impl_token.span(); + let message = "[objc]: expected inherent impl"; + return Err(syn::Error::new(span, message)); + } + + generics.where_clause = input.parse()?; + + let content; + let brace_token = syn::braced!(content in input); + attrs.extend(syn::Attribute::parse_inner(&content)?); + + let mut items = Vec::new(); + while !content.is_empty() { + items.push(content.parse()?); + } + return Ok(syn::ItemImpl { + attrs, + defaultness, + unsafety, + impl_token, + generics, + trait_: None, + self_ty: Box::new(self_ty), + brace_token, + items, + }); +} + +fn parse_rest_of_static( + input: ParseStream<'_>, + attrs: Vec, + vis: syn::Visibility, +) -> syn::Result { + let static_token = input.parse()?; + let mutability = input.parse()?; + let ident = input.parse()?; + let colon_token = input.parse()?; + let ty = input.parse()?; + let mut lookahead = input.lookahead1(); + let mut body = None; + if lookahead.peek(syn::Token![=]) { + body = Some((input.parse()?, input.parse()?)); + lookahead = input.lookahead1(); + } + if lookahead.peek(syn::Token![;]) { + let semi_token = input.parse()?; + Ok(ItemStatic { + attrs, + vis, + static_token, + mutability, + ident, + colon_token, + ty, + body, + semi_token, + }) + } else { + Err(lookahead.error()) + } +} + +fn parse_rest_of_struct( + input: ParseStream<'_>, + attrs: Vec, + vis: syn::Visibility, +) -> syn::Result { + let struct_token = input.parse()?; + let ident = input.parse()?; + let generics = input.parse()?; + let (where_clause, fields, semi_token) = data_struct(input)?; + return Ok(syn::ItemStruct { + attrs, + vis, + struct_token, + ident, + generics: syn::Generics { + where_clause, + ..generics + }, + fields, + semi_token, + }); +} + +fn parse_rest_of_trait( + input: ParseStream<'_>, + mut attrs: Vec, + vis: syn::Visibility, + unsafety: Option, + auto_token: Option, +) -> syn::Result { + let trait_token = input.parse()?; + let ident = input.parse()?; + let mut generics = input.parse::()?; + + let colon_token: Option = input.parse()?; + + let mut supertraits = syn::punctuated::Punctuated::new(); + if colon_token.is_some() { + loop { + if input.peek(syn::Token![where]) || input.peek(syn::token::Brace) { + break; + } + supertraits.push_value(input.parse()?); + if input.peek(syn::Token![where]) || input.peek(syn::token::Brace) { + break; + } + supertraits.push_punct(input.parse()?); + } + } + + generics.where_clause = input.parse()?; + + let content; + let brace_token = syn::braced!(content in input); + attrs.extend(syn::Attribute::parse_inner(&content)?); + let mut items = Vec::new(); + while !content.is_empty() { + items.push(content.parse()?); + } + + Ok(syn::ItemTrait { + attrs, + vis, + unsafety, + auto_token, + trait_token, + ident, + generics, + colon_token, + supertraits, + brace_token, + items, + }) +} + +fn parse_rest_of_type( + input: ParseStream<'_>, + attrs: Vec, + vis: syn::Visibility, +) -> syn::Result { + let type_token = input.parse()?; + let ident = input.parse()?; + let generics = { + let mut generics = input.parse::()?; + generics.where_clause = input.parse()?; + generics + }; + let semi_token = input.parse()?; + Ok(ItemType { + attrs, + vis, + type_token, + ident, + generics, + semi_token, + }) +} + +fn pop_variadic(args: &mut Punctuated) -> Option { + let trailing_punct = args.trailing_punct(); + + let last = match args.last_mut()? { + syn::FnArg::Typed(last) => last, + _ => return None, + }; + + let ty = match last.ty.as_ref() { + syn::Type::Verbatim(ty) => ty, + _ => return None, + }; + + let mut variadic = syn::Variadic { + attrs: Vec::new(), + dots: syn::parse2(ty.clone()).ok()?, + }; + + if let syn::Pat::Verbatim(pat) = last.pat.as_ref() { + if pat.to_string() == "..." && !trailing_punct { + variadic.attrs = std::mem::replace(&mut last.attrs, Vec::new()); + args.pop(); + } + } + + Some(variadic) +} + +fn variadic_to_tokens(dots: &syn::Token![...]) -> TokenStream { + TokenStream::from_iter(vec![ + TokenTree::Punct({ + let mut dot = Punct::new('.', Spacing::Joint); + dot.set_span(dots.spans[0]); + dot + }), + TokenTree::Punct({ + let mut dot = Punct::new('.', Spacing::Joint); + dot.set_span(dots.spans[1]); + dot + }), + TokenTree::Punct({ + let mut dot = Punct::new('.', Spacing::Alone); + dot.set_span(dots.spans[2]); + dot + }), + ]) +} + +pub(crate) fn error_unexpected_arguments(attr: TokenStream) -> syn::Error { + let span = attr.span(); + let message = format!("#[objc]: unexpected arguments: `{attr}`"); + syn::Error::new(span, message) +} diff --git a/crates/objc2-proc-macros/src/protocol.rs b/crates/objc2-proc-macros/src/protocol.rs new file mode 100644 index 000000000..fd264989c --- /dev/null +++ b/crates/objc2-proc-macros/src/protocol.rs @@ -0,0 +1,22 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::{parse::Parser, punctuated::Punctuated, spanned::Spanned}; + +pub(crate) fn item_trait( + attr: TokenStream, + item_trait: syn::ItemTrait, +) -> syn::Result { + let meta = Parser::parse2( + Punctuated::::parse_terminated, + attr, + )?; + match meta.first() { + Some(syn::Meta::Path(path)) if path.is_ident("protocol") => {} + _ => { + let span = meta.span(); + let message = "#[objc]: use `#[objc(protocol)] trait P { ... }` for `trait` items"; + return Err(syn::Error::new(span, message)); + } + } + Ok(item_trait.to_token_stream()) +} diff --git a/crates/objc2/Cargo.toml b/crates/objc2/Cargo.toml index 2bd7b7085..5ea9e23e6 100644 --- a/crates/objc2/Cargo.toml +++ b/crates/objc2/Cargo.toml @@ -20,7 +20,8 @@ license = "MIT" # NOTE: 'unstable' features are _not_ considered part of the SemVer contract, # and may be removed in a minor release. [features] -default = ["std", "apple"] +default = ["std", "apple", "objc2-proc-macros"] +objc2-proc-macros = ["dep:objc2-proc-macros", "bitflags", "linkme", "num_enum"] # Currently not possible to turn off, put here for forwards compatibility. std = ["alloc", "objc2-encode/std", "objc-sys/std"] @@ -76,7 +77,10 @@ gnustep-2-1 = ["gnustep-2-0", "objc-sys/gnustep-2-1"] unstable-compiler-rt = ["apple"] [dependencies] +bitflags = { version = "1.3", optional = true } +linkme = { version = "0.3", optional = true } malloc_buf = { version = "1.0", optional = true } +num_enum = { version = "0.5", optional = true } objc-sys = { path = "../objc-sys", version = "0.3.0", default-features = false } objc2-encode = { path = "../objc2-encode", version = "=2.0.0-pre.4", default-features = false } objc2-proc-macros = { path = "../objc2-proc-macros", version = "0.1.1", optional = true } diff --git a/crates/objc2/src/__macro_helpers.rs b/crates/objc2/src/__macro_helpers.rs index c11c0503f..ee672135e 100644 --- a/crates/objc2/src/__macro_helpers.rs +++ b/crates/objc2/src/__macro_helpers.rs @@ -630,6 +630,15 @@ impl ClassProtocolMethodsBuilder<'_, '_> { } } +/// Helper for proc-macro for NS_TYPED_ENUM, NS_TYPED_EXTENSIBLE_ENUM, NS_ERROR_ENUM. +/// This allows us to write `::Cases::Variant` for pattern matching. +/// NOTE: this should go away when inherent associated types stabilize +/// NOTE: see https://github.com/rust-lang/rust/issues/8995 +/// NOTE: once that lands, we can inline into `impl T` and write `T::Cases::Variant` directly +pub unsafe trait Enum { + type Cases; +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/objc2/src/declare.rs b/crates/objc2/src/declare.rs index bf373f4aa..2dc30a465 100644 --- a/crates/objc2/src/declare.rs +++ b/crates/objc2/src/declare.rs @@ -445,6 +445,43 @@ impl ClassBuilder { assert!(success.as_bool(), "Failed to add method {sel:?}"); } + #[allow(missing_docs)] + pub unsafe fn add_method_from_raw_parts( + &mut self, + sel: Sel, + enc_args: &[Encoding], + enc_ret: Encoding, + imp: Imp, + ) { + let sel_args = sel.number_of_arguments(); + assert_eq!( + sel_args, + enc_args.len(), + "Selector {:?} accepts {} arguments, but function accepts {}", + sel, + sel_args, + enc_args.len(), + ); + + // Verify that, if the method is present on the superclass, that the + // encoding is correct. + #[cfg(debug_assertions)] + if let Some(superclass) = self.superclass() { + if let Some(method) = superclass.instance_method(sel) { + if let Err(err) = crate::verify::verify_method_signature(method, enc_args, &enc_ret) + { + panic!("declared invalid method -[{} {sel:?}]: {err}", self.name()) + } + } + } + + let types = method_type_encoding(&enc_ret, enc_args); + let success = Bool::from_raw(unsafe { + ffi::class_addMethod(self.as_mut_ptr(), sel.as_ptr(), Some(imp), types.as_ptr()) + }); + assert!(success.as_bool(), "Failed to add method {sel:?}"); + } + fn metaclass_mut(&mut self) -> *mut ffi::objc_class { unsafe { ffi::object_getClass(self.as_mut_ptr().cast()) as *mut ffi::objc_class } } diff --git a/crates/objc2/src/lib.rs b/crates/objc2/src/lib.rs index c7a62ee36..eabc03112 100644 --- a/crates/objc2/src/lib.rs +++ b/crates/objc2/src/lib.rs @@ -191,6 +191,20 @@ pub use self::message::{Message, MessageArguments, MessageReceiver}; pub use self::protocol::{ImplementedBy, ProtocolObject, ProtocolType}; pub use self::verify::VerificationError; +#[cfg(feature = "objc2-proc-macros")] +pub use __macro_helpers::Enum; +#[cfg(feature = "objc2-proc-macros")] +#[doc(hidden)] +pub use bitflags; +#[cfg(feature = "objc2-proc-macros")] +#[doc(hidden)] +pub use linkme; +#[cfg(feature = "objc2-proc-macros")] +#[doc(hidden)] +pub use num_enum; +#[cfg(feature = "objc2-proc-macros")] +pub use objc2_proc_macros::objc; + #[cfg(feature = "objc2-proc-macros")] #[doc(hidden)] pub use objc2_proc_macros::__hash_idents; diff --git a/crates/objc2/tests/proc_macros.rs b/crates/objc2/tests/proc_macros.rs new file mode 100644 index 000000000..9ca13ee36 --- /dev/null +++ b/crates/objc2/tests/proc_macros.rs @@ -0,0 +1,324 @@ +#![cfg(feature = "objc2-proc-macros")] + +use objc2::objc; + +#[test] +#[allow(unused)] +fn impl_implementation() { + // TODO + #[objc(interface)] + struct C {}; + + // TODO + #[objc(implementation)] + unsafe impl C {} +} + +#[test] +#[allow(unused)] +fn struct_interface() { + // TODO + #[objc(interface)] + struct C {} +} + +#[test] +#[allow(unused)] +fn type_interface() { + // TODO + #[objc(interface)] + type C; + + // TODO + #[objc(implementation)] + unsafe impl C {} +} + +#[test] +#[allow(unused)] +fn trait_protocol() { + // TODO + #[objc(protocol)] + trait P {} +} + +#[test] +#[allow(non_upper_case_globals)] +fn ns_options_convert() { + #[objc(options, repr = NSUInteger)] + enum Enum { + Var0 = 0b0001, + Var1 = 0b0010, + Var2 = 0b0100, + Var3 = 0b1000, + } + assert_eq!(Enum::Var0.bits(), 0b0001); + assert_eq!(Enum::Var1.bits(), 0b0010); + assert_eq!(Enum::Var2.bits(), 0b0100); + assert_eq!(Enum::Var3.bits(), 0b1000); +} + +#[test] +#[allow(non_upper_case_globals)] +fn ns_options_ops() { + #[objc(options, repr = NSUInteger)] + enum Enum { + Var0 = 0b0001, + Var1 = 0b0010, + Var2 = 0b0100, + Var3 = 0b1000, + } + assert_eq!((Enum::Var1 | Enum::Var2).bits(), 0b0110); + assert_eq!(Enum::Var1.bits() | Enum::Var2.bits(), 0b0110); +} + +#[test] +fn ns_closed_enum_convert() { + #[objc(closed_enum, repr = NSUInteger)] + enum Enum { + Var0, + Var1, + Var2 = 8, + Var3, + } + assert_eq!(usize::from(Enum::Var0), 0); + assert_eq!(usize::from(Enum::Var1), 1); + assert_eq!(usize::from(Enum::Var2), 8); + assert_eq!(usize::from(Enum::Var3), 9); +} + +#[test] +#[allow(dead_code)] +fn ns_closed_enum_match() { + #[objc(closed_enum, repr = NSUInteger)] + enum Enum { + Var0, + Var1, + Var2 = 8, + Var3, + } + let value = Enum::Var2; + let did_match = match value { + Enum::Var0 => false, + Enum::Var1 => false, + Enum::Var2 => true, + Enum::Var3 => false, + }; + assert!(did_match); +} + +#[test] +fn ns_enum_convert() { + #[objc(enum, repr = NSUInteger)] + enum Enum { + Var0, + Var1, + Var2 = 8, + Var3, + } + assert_eq!(usize::from(Enum::Var0), 0); + assert_eq!(usize::from(Enum::Var1), 1); + assert_eq!(usize::from(Enum::Var2), 8); + assert_eq!(usize::from(Enum::Var3), 9); +} + +#[test] +#[allow(dead_code)] +fn ns_enum_match() { + #[objc(enum, repr = NSUInteger)] + enum Enum { + Var0, + Var1, + Var2 = 8, + Var3, + } + let value = Enum::Var2; + #[rustfmt::skip] + let did_match = match value { + Enum::Var0 => false, + Enum::Var1 => false, + Enum::Var2 => true, + Enum::Var3 => false, + // NOTE: outside of defining crate (e.g., `icrate`), a default case is needed here + }; + assert!(did_match); +} + +#[test] +#[allow(dead_code)] +fn ns_typed_enum_convert() { + #[objc(typed_enum, type = &'static str)] + enum EnumStr { + Var0 = "var0", + Var1 = "var1", + Var2 = "var2", + Var3 = "var3", + } + assert_eq!(EnumStr::Var0.take(), "var0"); + assert_eq!(EnumStr::Var1.take(), "var1"); + assert_eq!(EnumStr::Var2.take(), "var2"); + assert_eq!(EnumStr::Var3.take(), "var3"); + + #[objc(typed_enum, type = (char, usize))] + enum EnumTuple { + Var0 = ('a', 0), + Var1 = ('b', 1), + Var2 = ('c', 2), + Var3 = ('d', 3), + } + assert_eq!(EnumTuple::Var0.peek(), &('a', 0)); + assert_eq!(EnumTuple::Var1.peek(), &('b', 1)); + assert_eq!(EnumTuple::Var2.peek(), &('c', 2)); + assert_eq!(EnumTuple::Var3.peek(), &('d', 3)); +} + +#[test] +#[allow(dead_code)] +fn ns_typed_enum_match() { + #[objc(typed_enum, type = &'static str)] + enum Enum { + Var0 = "var0", + Var1 = "var1", + Var2 = "var2", + Var3 = "var3", + } + let value = Enum::Var2; + let did_match = match value.cases() { + r#enum::Cases::Var0 => false, + r#enum::Cases::Var1 => false, + r#enum::Cases::Var2 => true, + r#enum::Cases::Var3 => false, + }; + assert!(did_match); + + #[objc(typed_enum, type = &'static str)] + enum Example { + Var0 = "var0", + Var1 = "var1", + Var2 = "var2", + Var3 = "var3", + } + let value = Example::Var2; + let did_match = match value.cases() { + example::Cases::Var0 => false, + example::Cases::Var1 => false, + example::Cases::Var2 => true, + example::Cases::Var3 => false, + }; + assert!(did_match); +} + +#[test] +#[allow(dead_code)] +fn ns_typed_extensible_enum_convert() { + #[objc(typed_extensible_enum, type = &'static str)] + enum EnumStr { + Var0 = "var0", + Var1 = "var1", + Var2 = "var2", + Var3 = "var3", + } + assert_eq!(EnumStr::Var0.take(), "var0"); + assert_eq!(EnumStr::Var1.take(), "var1"); + assert_eq!(EnumStr::Var2.take(), "var2"); + assert_eq!(EnumStr::Var3.take(), "var3"); + + #[objc(typed_extensible_enum, type = (char, usize))] + enum EnumTuple { + Var0 = ('a', 0), + Var1 = ('b', 1), + Var2 = ('c', 2), + Var3 = ('d', 3), + } + assert_eq!(EnumTuple::Var0.peek(), &('a', 0)); + assert_eq!(EnumTuple::Var1.peek(), &('b', 1)); + assert_eq!(EnumTuple::Var2.peek(), &('c', 2)); + assert_eq!(EnumTuple::Var3.peek(), &('d', 3)); +} + +#[test] +#[allow(dead_code)] +fn ns_typed_extensible_enum_match() { + #[objc(typed_extensible_enum, type = &'static str)] + enum Enum { + Var0 = "var0", + Var1 = "var1", + Var2 = "var2", + Var3 = "var3", + } + let value = Enum::Var2; + let did_match = match value.cases() { + Some(some) => match some { + r#enum::Cases::Var0 => false, + r#enum::Cases::Var1 => false, + r#enum::Cases::Var2 => true, + r#enum::Cases::Var3 => false, + }, + None => false, + }; + assert!(did_match); +} + +#[test] +#[allow(dead_code)] +fn ns_typed_enum_trait() { + #[objc(typed_enum, type = &'static str)] + enum Example { + Var0 = "var0", + Var1 = "var1", + Var2 = "var2", + Var3 = "var3", + } + let value = Example::Var2; + let did_match = match value.cases() { + ::Cases::Var0 => false, + ::Cases::Var1 => false, + ::Cases::Var2 => true, + ::Cases::Var3 => false, + }; + assert!(did_match); +} + +#[test] +#[allow(dead_code)] +fn ns_error_enum() { + #[objc(error_enum, domain = "MyError")] + enum MyError { + Code0 = 1000, + Code1 = -1000, + Code2 = 1100, + Code3 = 2000, + } +} + +#[test] +fn objc_fn() { + #[objc] + fn f(); + + #[objc] + fn g() {} +} + +#[test] +fn objc_unsafe_fn() { + #[objc] + unsafe fn f(); + + #[objc] + unsafe fn g() {} +} + +#[test] +#[allow(dead_code)] +fn objc_static() { + #[objc] + static FOO: *const u8; +} + +#[test] +#[allow(dead_code)] +fn objc_static_defined() { + #[objc] + static FOO: &'static str = "foo"; +}