From 46276677421b410512d73ca5f2d1d7469fcddec0 Mon Sep 17 00:00:00 2001 From: silvanshade Date: Wed, 15 Feb 2023 13:40:31 -0700 Subject: [PATCH] work-in-progress --- .github/workflows/ci.yml | 4 +- Cargo.lock | 145 +++ crates/icrate/Cargo.toml | 24 + crates/icrate/examples/ns_error_enum.rs | 230 +++++ crates/icrate/tests/proc_macros.rs | 32 + crates/objc2-proc-macros/Cargo.toml | 7 + crates/objc2-proc-macros/src/constant.rs | 30 + crates/objc2-proc-macros/src/enumeration.rs | 855 ++++++++++++++++++ crates/objc2-proc-macros/src/function.rs | 53 ++ .../objc2-proc-macros/src/implementation.rs | 26 + crates/objc2-proc-macros/src/interface.rs | 54 ++ crates/objc2-proc-macros/src/lib.rs | 19 + crates/objc2-proc-macros/src/objc.rs | 557 ++++++++++++ crates/objc2-proc-macros/src/protocol.rs | 22 + crates/objc2/Cargo.toml | 6 +- crates/objc2/src/__macro_helpers.rs | 30 + crates/objc2/src/declare.rs | 43 + crates/objc2/src/lib.rs | 14 + crates/objc2/tests/proc_macros.rs | 321 +++++++ 19 files changed, 2469 insertions(+), 3 deletions(-) create mode 100644 crates/icrate/examples/ns_error_enum.rs create mode 100644 crates/icrate/tests/proc_macros.rs create mode 100644 crates/objc2-proc-macros/src/constant.rs create mode 100644 crates/objc2-proc-macros/src/enumeration.rs create mode 100644 crates/objc2-proc-macros/src/function.rs create mode 100644 crates/objc2-proc-macros/src/implementation.rs create mode 100644 crates/objc2-proc-macros/src/interface.rs create mode 100644 crates/objc2-proc-macros/src/objc.rs create mode 100644 crates/objc2-proc-macros/src/protocol.rs create mode 100644 crates/objc2/tests/proc_macros.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19bc86b18..5ad308192 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -164,7 +164,7 @@ jobs: - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: - toolchain: '1.60' + toolchain: '1.62' profile: minimal override: true target: x86_64-apple-darwin @@ -317,7 +317,7 @@ jobs: # - lint env: - ARGS: --no-default-features --features=std,apple + ARGS: --no-default-features --features=std,apple,unstable-proc-macros steps: - uses: actions/checkout@v3 diff --git a/Cargo.lock b/Cargo.lock index 61b6d890d..9f3917669 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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0072973714303aa6e3631c7e8e777970cf4bdd25dc4932e41031027b8bcc4e" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0629cbd6b897944899b1f10496d9c4a7ac5878d45fd61bc22e9e79bfbbc29597" +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/icrate/Cargo.toml b/crates/icrate/Cargo.toml index 3d895c95a..f13f8e7d1 100644 --- a/crates/icrate/Cargo.toml +++ b/crates/icrate/Cargo.toml @@ -75,6 +75,12 @@ required-features = [ "unstable-example-browser" ] +[[example]] +name = "ns_error_enum" +required-features = [ + "unstable-example-ns_error_enum" +] + [features] default = ["std", "apple"] @@ -105,6 +111,8 @@ unstable-docsrs = [] # exist. unstable-private = [] +unstable-proc-macros = ["objc2/objc2-proc-macros"] + # Make the `ns_string!` macro create the string statically. # # Please test it, and report any issues you may find: @@ -131,6 +139,22 @@ unstable-example-speech_synthesis = [ "Foundation", "Foundation_NSString", ] +unstable-example-ns_error_enum = [ + "apple", + "unstable-proc-macros", + "Foundation", + "Foundation_NSData", + "Foundation_NSDictionary", + "Foundation_NSEnumerator", + "Foundation_NSError", + "Foundation_NSMutableDictionary", + "Foundation_NSNumber", + "Foundation_NSString", + "Foundation_NSURL", + "Foundation_NSURLResponse", + "Foundation_NSURLSession", + "Foundation_NSURLSessionDataTask", +] unstable-example-nspasteboard = [ "apple", "Foundation", diff --git a/crates/icrate/examples/ns_error_enum.rs b/crates/icrate/examples/ns_error_enum.rs new file mode 100644 index 000000000..2d17e2844 --- /dev/null +++ b/crates/icrate/examples/ns_error_enum.rs @@ -0,0 +1,230 @@ +#![allow(non_snake_case)] + +use block2::ConcreteBlock; +use icrate::{ + ns_string, + objc2::{objc, rc::Id, ClassType}, + Foundation::{ + NSData, NSNumber, NSObject, NSString, NSURLErrorBackgroundTaskCancelledReasonKey, + NSURLErrorDomain, NSURLErrorFailingURLErrorKey, NSURLErrorFailingURLStringErrorKey, + NSURLErrorNetworkUnavailableReasonKey, NSURLResponse, NSURLSessionDataTask, NSURL, + }, +}; +use objc2::{extern_class, extern_methods, Cases, Codes, UserInfo}; + +mod ns_url_error_reasons { + use super::*; + + #[objc(typed_extensible_enum, type = isize)] + #[derive(Clone, Debug)] + pub enum BackgroundTaskCancelledReason { + UserForceQuitApplication = 0, + BackgroundUpdatesDisabled = 1, + InsufficientSystemResources = 2, + } + impl BackgroundTaskCancelledReason { + #[allow(unused)] + pub(super) fn from_number(number: &NSNumber) -> Self { + Self(number.as_isize()) + } + } + + #[objc(typed_extensible_enum, type = isize)] + #[derive(Clone, Debug)] + pub enum NetworkUnavailableReason { + Cellular = 0, + Expensive = 1, + Constrained = 2, + } + impl NetworkUnavailableReason { + #[allow(unused)] + pub(super) fn from_number(number: &NSNumber) -> Self { + Self(number.as_isize()) + } + } +} + +#[objc(error_enum, + mod = ns_url_error, + domain = unsafe { NSURLErrorDomain }, + user_info = [ + // NOTE: Here we specify (typed) getters for the associated `userInfo` dict. + { key = NSURLErrorFailingURLErrorKey, type = Id }, + { key = NSURLErrorFailingURLStringErrorKey, type = Id }, + { key = NSURLErrorBackgroundTaskCancelledReasonKey, type = ns_url_error_reasons::BackgroundTaskCancelledReason }, + { key = NSURLErrorNetworkUnavailableReasonKey, type = ns_url_error_reasons::NetworkUnavailableReason }, + ] +)] +pub enum NSURLError { + Unknown = -1, + Cancelled = -999, + BadURL = -1000, + TimedOut = -1001, + UnsupportedURL = -1002, + CannotFindHost = -1003, + CannotConnectToHost = -1004, + NetworkConnectionLost = -1005, + DNSLookupFailed = -1006, + HTTPTooManyRedirects = -1007, + ResourceUnavailable = -1008, + NotConnectedToInternet = -1009, + RedirectToNonExistentLocation = -1010, + BadServerResponse = -1011, + UserCancelledAuthentication = -1012, + UserAuthenticationRequired = -1013, + ZeroByteResource = -1014, + CannotDecodeRawData = -1015, + CannotDecodeContentData = -1016, + CannotParseResponse = -1017, + AppTransportSecurityRequiresSecureConnection = -1022, + FileDoesNotExist = -1100, + FileIsDirectory = -1101, + NoPermissionsToReadFile = -1102, + DataLengthExceedsMaximum = -1103, + FileOutsideSafeArea = -1104, + SecureConnectionFailed = -1200, + ServerCertificateHasBadDate = -1201, + ServerCertificateUntrusted = -1202, + ServerCertificateHasUnknownRoot = -1203, + ServerCertificateNotYetValid = -1204, + ClientCertificateRejected = -1205, + ClientCertificateRequired = -1206, + CannotLoadFromNetwork = -2000, + CannotCreateFile = -3000, + CannotOpenFile = -3001, + CannotCloseFile = -3002, + CannotWriteToFile = -3003, + CannotRemoveFile = -3004, + CannotMoveFile = -3005, + DownloadDecodingFailedMidStream = -3006, + DownloadDecodingFailedToComplete = -3007, + InternationalRoamingOff = -1018, + CallIsActive = -1019, + DataNotAllowed = -1020, + RequestBodyStreamExhausted = -1021, + BackgroundSessionRequiresSharedContainer = -995, + BackgroundSessionInUseByAnotherProcess = -996, + BackgroundSessionWasDisconnected = -997, +} + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + #[cfg(feature = "Foundation_NSURLSession")] + pub struct NSURLSession; + + #[cfg(feature = "Foundation_NSURLSession")] + unsafe impl ClassType for NSURLSession { + type Super = NSObject; + } +); + +extern_methods!( + /// NSURLSessionAsynchronousConvenience + #[cfg(feature = "Foundation_NSURLSession")] + unsafe impl NSURLSession { + #[method_id(@__retain_semantics Other sharedSession)] + pub unsafe fn sharedSession() -> Id; + + #[cfg(all( + feature = "Foundation_NSData", + feature = "Foundation_NSError", + feature = "Foundation_NSURL", + feature = "Foundation_NSURLResponse", + feature = "Foundation_NSURLSessionDataTask" + ))] + #[method_id(@__retain_semantics Other dataTaskWithURL:completionHandler:)] + pub unsafe fn dataTaskWithURL_completionHandlerRefined( + &self, + url: &NSURL, + // NOTE: we refine `NSError` to `NSURLError` here + completion_handler: &block2::Block< + (*mut NSData, *mut NSURLResponse, *mut NSURLError), + (), + >, + ) -> Id; + } +); + +fn example_catch() { + let (tx, rx) = std::sync::mpsc::channel::>(); + std::thread::spawn(move || { + let session = unsafe { NSURLSession::sharedSession() }; + let task = { + let url_string = ns_string!("foo://www.google.com"); + let url = unsafe { NSURL::URLWithString(url_string) }.expect("URL should parse"); + let completion_handler = { + // NOTE: here we use a refined error type (via `ns_error_enum`) for the completion handler + let block = ConcreteBlock::new(move |_data, _response, error: *mut NSURLError| { + // Get the refined error + let error = unsafe { error.as_ref() }.expect("error should be present"); + // Verify that the refined error has the expected domain + assert_eq!(&*error.domain(), unsafe { NSURLErrorDomain }); + // Use the refined error to pattern match on the error codes + #[allow(clippy::single_match)] + match error.code().cases() { + Some(code) => match code { + Cases::::UnsupportedURL => { + // NOTE: here we use the generated `user_info` getter to check the failing URL + let failing_url_string = + error.failing_url_string().expect("value should exist"); + assert_eq!(&*failing_url_string, url_string); + + // NOTE: we can also pattern match on the typed `user_info` data + #[allow(unused)] + let UserInfo:: { + failing_url, + failing_url_string, + background_task_cancelled_reason, + network_unavailable_reason, + } = error.user_info(); + let failing_url_string = + error.failing_url_string().expect("value should exist"); + assert_eq!(&*failing_url_string, url_string); + + // Send back the error code + tx.send(Some(error.code())).expect("channel should send"); + } + // Rust Analyzer can actually fill in all these cases but we only care about the above case for this example + _ => {} + }, + None => { + println!("unknown code from error: {:#?}", error.as_super()); + } + } + }); + block.copy() + }; + unsafe { session.dataTaskWithURL_completionHandlerRefined(&url, &completion_handler) } + }; + unsafe { task.resume() }; + }); + // Wait for the completion handler to finish with a possible error code + let code = rx.recv().expect("channel should receive"); + // Verify that the error code is the one we expected + assert!(code == Some(Codes::::UnsupportedURL)); +} + +fn example_throw() { + use ns_url_error_reasons::BackgroundTaskCancelledReason; + let reason = Some(BackgroundTaskCancelledReason::BackgroundUpdatesDisabled); + let code = Codes::::UnsupportedURL; + let user_info = { + UserInfo:: { + background_task_cancelled_reason: reason.clone(), + ..Default::default() + } + }; + // Construct an error with `code` and `user_info` + let error = NSURLError::new_with_user_info(code, user_info); + // Confirm that the `user_info` we get back contains the data we expect + let UserInfo:: { + background_task_cancelled_reason, + .. + } = error.user_info(); + assert_eq!(reason, background_task_cancelled_reason); +} + +fn main() { + example_catch(); + example_throw(); +} diff --git a/crates/icrate/tests/proc_macros.rs b/crates/icrate/tests/proc_macros.rs new file mode 100644 index 000000000..0537e6490 --- /dev/null +++ b/crates/icrate/tests/proc_macros.rs @@ -0,0 +1,32 @@ +#![cfg(all( + feature = "unstable-proc-macros", + feature = "Foundation_NSDictionary", + feature = "Foundation_NSError", + feature = "Foundation_NSString" +))] +use icrate::ns_string; +use objc2::{objc, Cases, ClassType, Codes}; + +#[test] +fn ns_error_enum() { + #[objc(error_enum, domain = ns_string!("MyError"))] + enum MyError { + Code0 = 1000, + Code1 = -1000, + Code2 = 1100, + Code3 = 2000, + } + let code = Codes::::Code1; + let error = MyError::new(code); + assert_eq!(&*error.as_super().domain(), ns_string!("MyError")); + let did_match = match error.code().cases() { + Some(some) => match some { + Cases::::Code0 => false, + Cases::::Code1 => true, + Cases::::Code2 => false, + Cases::::Code3 => false, + }, + None => false, + }; + assert!(did_match); +} 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..e61fc4a18 --- /dev/null +++ b/crates/objc2-proc-macros/src/enumeration.rs @@ -0,0 +1,855 @@ +use heck::ToSnakeCase; +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens, TokenStreamExt}; +use syn::{ + parse::{Parse, ParseStream, Parser}, + punctuated::Punctuated, + spanned::Spanned, +}; + +// TODO: derive options +// TODO: naming options for generated constants/variants/helper mods +// TODO: impls for encoding machinery + +mod macro_args { + pub(super) mod keyword { + syn::custom_keyword!(closed_enum); + syn::custom_keyword!(domain); + syn::custom_keyword!(error_enum); + syn::custom_keyword!(from); + syn::custom_keyword!(into); + syn::custom_keyword!(key); + syn::custom_keyword!(name); + syn::custom_keyword!(options); + syn::custom_keyword!(repr); + syn::custom_keyword!(typed_enum); + syn::custom_keyword!(typed_extensible_enum); + syn::custom_keyword!(user_info); + } +} + +struct EmptyPunctuation; + +impl Parse for EmptyPunctuation { + fn parse(_input: ParseStream<'_>) -> syn::Result { + Ok(Self) + } +} + +impl ToTokens for EmptyPunctuation { + fn to_tokens(&self, _tokens: &mut TokenStream) {} +} + +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]), + }) + } +} + +#[repr(transparent)] +struct Repr(syn::Ident); + +static REPRS: &[&'static str] = &[ + "usize", "u8", "u16", "u32", "u64", "isize", "i8", "i16", "i32", "i64", +]; + +#[inline] +fn ident_is_repr(ident: &syn::Ident) -> bool { + REPRS.contains(&ident.to_string().as_str()) +} + +impl Parse for Repr { + fn parse(input: ParseStream<'_>) -> syn::Result { + let ident = input.parse::()?; + if ident_is_repr(&ident) { + Ok(Self(ident)) + } else { + let span = ident.span(); + let message = "#[objc]: `repr` must be one of: `usize`, `u8`, `u16`, `u32`, `u64`, `isize`, `i8`, `i16`, `i32`, `i64`"; + Err(syn::Error::new(span, message)) + } + } +} + +enum MacroArgs { + Options { + #[allow(unused)] + options_token: macro_args::keyword::options, + repr: KeyVal, + }, + Enum { + #[allow(unused)] + closed_enum_token: macro_args::keyword::closed_enum, + repr: KeyVal, + }, + ErrorEnum { + #[allow(unused)] + error_enum_token: macro_args::keyword::error_enum, + module: Option>, + import: Option>, + domain: KeyVal, + user_info: Option, + }, + OpenEnum { + #[allow(unused)] + enum_token: syn::Token![enum], + repr: KeyVal, + }, + TypedEnum { + #[allow(unused)] + typed_enum_token: macro_args::keyword::typed_enum, + r#type: KeyVal, + }, + OpenTypedEnum { + #[allow(unused)] + typed_extensible_enum_token: macro_args::keyword::typed_extensible_enum, + r#type: 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()?, + r#type: input.parse()?, + }); + } + if lookahead.peek(macro_args::keyword::typed_extensible_enum) { + return Ok(MacroArgs::OpenTypedEnum { + typed_extensible_enum_token: input.parse()?, + r#type: input.parse()?, + }); + } + if lookahead.peek(macro_args::keyword::error_enum) { + let error_enum_token = input.parse()?; + let module = if input.peek(syn::Token![,]) && input.peek2(syn::Token![mod]) { + Some(input.parse()?) + } else { + None + }; + let import = if input.peek(syn::Token![,]) && input.peek2(syn::Token![use]) { + Some(input.parse()?) + } else { + None + }; + let domain = input.parse()?; + let user_info = + if input.peek(syn::Token![,]) && input.peek2(macro_args::keyword::user_info) { + Some(input.parse()?) + } else { + None + }; + return Ok(MacroArgs::ErrorEnum { + error_enum_token, + module, + import, + domain, + user_info, + }); + } + + 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 { + punctuation: P, + key: K, + separator: S, + val: V, +} + +impl Parse for KeyVal { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(KeyVal { + punctuation: input.parse()?, + key: input.parse()?, + separator: 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 args = Parser::parse2(MacroArgs::parse, attr)?; + + match args { + MacroArgs::Options { repr, .. } => ns_options(item_enum, repr.val.0), + MacroArgs::Enum { repr, .. } => ns_enum(item_enum, repr.val.0, Closed), + MacroArgs::OpenEnum { repr, .. } => ns_enum(item_enum, repr.val, Open), + MacroArgs::TypedEnum { r#type: repr, .. } => { + ns_typed_enum(item_enum, repr.val, Closed.into()) + } + MacroArgs::OpenTypedEnum { r#type: repr, .. } => { + ns_typed_enum(item_enum, repr.val, Open.into()) + } + MacroArgs::ErrorEnum { + module, + import, + domain, + user_info, + .. + } => ns_error_enum( + item_enum, + module.map(|kv| kv.val), + import.map(|kv| kv.val), + domain.val, + user_info, + ), + } +} + +fn ns_options(item_enum: syn::ItemEnum, repr: syn::Ident) -> 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 : #repr { + #options + } + })) +} + +fn ns_enum( + item_enum: syn::ItemEnum, + repr: syn::Ident, + extensibility: Extensibility, +) -> syn::Result { + let ty = &item_enum.ident; + let constructor = format_ident!("new_{}", repr); + Ok(quote!( + #[derive(objc2::num_enum::IntoPrimitive)] + #[repr(#repr)] + #extensibility + #item_enum + + #[cfg(all(feature = "Foundation", feature = "Foundation_NSNumber"))] + impl From<#ty> for objc2::rc::Id { + fn from(value: #ty) -> Self { + icrate::Foundation::NSNumber::#constructor(value.into()) + } + } + )) +} + +struct TypedEnumConfig { + emit_struct_in_module: bool, + extensibility: Extensibility, + mod_ident: Option, + mod_items: TokenStream, +} + +impl From for TypedEnumConfig { + fn from(extensibility: Extensibility) -> Self { + let emit_struct_in_module = false; + let mod_ident = None; + let mod_items = TokenStream::new(); + Self { + emit_struct_in_module, + extensibility, + mod_ident, + mod_items, + } + } +} + +fn ns_typed_enum( + item_enum: syn::ItemEnum, + repr: syn::Type, + config: TypedEnumConfig, +) -> syn::Result { + let syn::ItemEnum { + // FIXME: What to do with attrs? Probably we should filter `cfg` and `derive` and duplicate + // those, then only place the rest on the struct. + attrs, + vis, + ident: struct_ident, + variants, + .. + } = item_enum; + let TypedEnumConfig { + emit_struct_in_module, + extensibility, + mod_ident, + mod_items, + } = config; + + let mod_ident = mod_ident.unwrap_or_else(|| { + let struct_ident = struct_ident.to_string(); + let string = struct_ident.to_snake_case(); + format_ident!("r#{}", string) + }); + let cases_ident = if emit_struct_in_module { + quote!(Cases) + } else { + quote!(#mod_ident::Cases) + }; + + let (into_enum_ty, into_enum_case_default) = match extensibility { + Extensibility::Closed => ( + quote!(#cases_ident), + quote!(_wrapper => unreachable!("unexpected case in (closed) typed enum"),), + ), + Extensibility::Open => ( + quote!(core::option::Option<#cases_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(); + + let (struct_vis, struct_field_vis) = if emit_struct_in_module { + (quote!(pub), quote!(pub(super))) + } else { + (quote!(#vis), quote!()) + }; + + // 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!( + pub const #variant_ident: Self = Self(#expr); + )); + enum_variants.append_all(quote!( + #variant_ident, + )); + into_cases.append_all(quote!( + &Self::#variant_ident => #cases_ident::#variant_ident.into(), + )); + from_cases.append_all(quote!( + #cases_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)); + } + } + + let mut impl_into_nsnumber = TokenStream::new(); + let mut impl_try_from_nsnumber = TokenStream::new(); + + if let syn::Type::Path(tp) = &repr { + if let Some(repr) = tp.path.get_ident() { + if ident_is_repr(repr) { + let struct_ident_string = struct_ident.to_string(); + let repr = repr.to_string(); + let constructor = format_ident!("new_{}", repr); + let destructor = format_ident!("as_{}", repr); + + impl_into_nsnumber.append_all(quote!( + #[cfg(all(feature = "Foundation", feature = "Foundation_NSNumber"))] + impl From<#struct_ident> for objc2::rc::Id { + #[inline] + fn from(value: #struct_ident) -> Self { + icrate::Foundation::NSNumber::#constructor(value.0.into()) + } + } + // NOTE: used for automatic conversion for insersion into `userInfo` dictionaries + #[cfg(feature = "Foundation")] + impl core::ops::Deref for #struct_ident { + type Target = icrate::Foundation::NSObject; + fn deref(&self) -> &Self::Target { + let num = icrate::Foundation::NSNumber::#constructor(self.0.into()); + let obj = unsafe { objc2::rc::Id::cast::(num) }; + let obj = objc2::rc::Id::autorelease_return(obj); + let obj = unsafe { obj.as_ref() }.expect("pointer is non-null"); + obj + } + } + )); + + impl_try_from_nsnumber.append_all(quote!( + #[cfg(all(feature = "Foundation", feature = "Foundation_NSNumber"))] + impl TryFrom<&icrate::Foundation::NSNumber> for #struct_ident { + type Error = Box; + fn try_from(number: &icrate::Foundation::NSNumber) -> Result { + use objc2::Encoding::*; + let encoding = number.encoding(); + let equivalent = match #repr { + "usize" => encoding == ULong + || (cfg!(target_pointer_width = "32") && encoding == UInt) + || (cfg!(target_pointer_width = "64") && encoding == ULongLong), + "isize" => encoding == Long + || (cfg!(target_pointer_width = "32") && encoding == Int) + || (cfg!(target_pointer_width = "64") && encoding == LongLong), + "u8" => encoding == UChar, + "u16" => encoding == UShort, + "u32" => encoding == UInt + || (cfg!(target_pointer_width = "32") && encoding == ULong), + "u64" => encoding == ULongLong + || (cfg!(target_pointer_width = "64") && encoding == ULong), + "i8" => encoding == Char, + "i16" => encoding == Short, + "i32" => encoding == Int + || (cfg!(target_pointer_width = "32") && encoding == Long), + "i64" => encoding == LongLong + || (cfg!(target_pointer_width = "64") && encoding == Long), + _ => false, + }; + if equivalent { + let number = number.#destructor(); + let value = Self(number); + if value.cases().is_some() { + Ok(value) + } else { + Err(format!( + "NSNumber value `{}` does not match any of enum values for `{}`", + number, + #struct_ident_string, + ).into()) + } + } else { + Err(format!( + "NSNumber encoding `{}` is not equivalent to `{}` repr `{}`", + encoding, + #struct_ident_string, + #repr, + ).into()) + } + } + } + impl TryFrom> for #struct_ident { + type Error = Box; + #[inline] + fn try_from(number: Id) -> Result { + (&*number).try_into() + } + } + )); + } + } + } + + let struct_and_impls = quote!( + #(#attrs)* + #[derive(Eq, PartialEq)] + #[repr(transparent)] + #struct_vis struct #struct_ident(#struct_field_vis #repr); + 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) -> &#repr { + &self.0 + } + #[inline] + pub fn take(self) -> #repr { + self.0 + } + } + impl From<#cases_ident> for #struct_ident { + fn from(case: #cases_ident) -> Self { + match case { + #from_cases + } + } + } + impl core::convert::From<#struct_ident> for #repr { + #[inline] + fn from(wrapper: #struct_ident) -> Self { + wrapper.take() + } + } + impl From<#cases_ident> for #repr { + #[inline] + fn from(case: #cases_ident) -> Self { + Self::from(#struct_ident::from(case)) + } + } + impl objc2::TypedEnum for #struct_ident { + type Cases = #cases_ident; + } + #impl_into_nsnumber + #impl_try_from_nsnumber + ); + + let (struct_outside_mod, struct_inside_mod) = if emit_struct_in_module { + (quote!(), struct_and_impls) + } else { + (struct_and_impls, quote!()) + }; + + Ok(quote!( + #struct_outside_mod + + #[allow(non_snake_case)] + #vis mod #mod_ident { + #struct_inside_mod + + #[allow(non_camel_case_types)] + #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] + #extensibility + pub enum Cases { + #enum_variants + } + + #mod_items + } + )) +} + +struct UserInfoField { + #[allow(unused)] + brace_token: syn::token::Brace, + key: KeyVal, + r#type: KeyVal, + name: Option>, + from: Option>, + into: Option>, +} + +impl Parse for UserInfoField { + fn parse(input: ParseStream<'_>) -> syn::Result { + let content; + let brace_token = syn::braced!(content in input); + let key = content.parse()?; + let r#type = content.parse()?; + let name = if content.peek(syn::Token![,]) && content.peek2(macro_args::keyword::name) { + Some(content.parse()?) + } else { + None + }; + let from = if content.peek(syn::Token![,]) && content.peek2(macro_args::keyword::from) { + Some(content.parse()?) + } else { + None + }; + let into = if content.peek(syn::Token![,]) && content.peek2(macro_args::keyword::into) { + Some(content.parse()?) + } else { + None + }; + Ok(UserInfoField { + brace_token, + key, + r#type, + name, + from, + into, + }) + } +} + +struct UserInfoSpec { + #[allow(unused)] + bracket_token: syn::token::Bracket, + fields: Punctuated, +} + +impl Parse for UserInfoSpec { + fn parse(input: ParseStream<'_>) -> syn::Result { + let content; + let bracket_token = syn::bracketed!(content in input); + let fields = content.parse_terminated(UserInfoField::parse)?; + Ok(UserInfoSpec { + bracket_token, + fields, + }) + } +} + +type UserInfo = KeyVal; + +// TODO: add `impl TryFrom for ...` which checks the error domain +fn ns_error_enum( + mut item_enum: syn::ItemEnum, + module: Option, + import: Option, + domain: syn::Expr, + user_info: Option, +) -> syn::Result { + let syn::ItemEnum { vis, .. } = &item_enum; + + let error_ident_string = item_enum.ident.to_string(); + let error_ident_string = error_ident_string.trim_end_matches("Code"); + let error_ident = format_ident!("r#{}", error_ident_string); + let mod_ident = + module.unwrap_or_else(|| format_ident!("r#{}", error_ident_string.to_snake_case())); + + let mut tokens = TokenStream::new(); + let mut user_info_field_decls = TokenStream::new(); + let mut user_info_field_intos = TokenStream::new(); + let mut user_info_field_froms = TokenStream::new(); + let mut user_info_getters = TokenStream::new(); + let mut mod_items = TokenStream::new(); + + if let Some(KeyVal { + val: UserInfoSpec { fields, .. }, + .. + }) = user_info + { + for UserInfoField { + key: KeyVal { val: key, .. }, + r#type: KeyVal { val: ty, .. }, + name, + from, + into, + .. + } in fields.iter() + { + let ident = name.as_ref().map(|kv| kv.val.clone()).unwrap_or_else(|| { + let name = &key.to_string(); + let name = name.strip_prefix(error_ident_string).unwrap_or(name); + let name = name.strip_suffix("Key").unwrap_or(name); + let name = name.strip_suffix("Error").unwrap_or(name); + let name = name.to_snake_case(); + format_ident!("r#{}", name) + }); + + let into = if let Some(KeyVal { val: into, .. }) = into { + quote!((#into)) + } else { + quote!() + }; + user_info_field_decls.append_all(quote!( + pub #ident: Option<#ty>, + )); + user_info_field_intos.append_all(quote!( + if let Some(value) = #into(user_info.#ident) { + unsafe { dict.setValue_forKey(Some(&*value), #key) }; + } + )); + user_info_field_froms.append_all(quote!( + user_info.#ident = self.#ident(); + )); + + let from = if let Some(KeyVal { val: from, .. }) = from { + quote!((#from)) + } else { + quote!((|n: Option<_>| n.map(TryInto::try_into).and_then(Result::ok))) + }; + user_info_getters.append_all(quote!( + #[inline] + fn #ident(&self) -> Option<#ty> { + let user_info = self.as_super().userInfo(); + let value = unsafe { + user_info.valueForKey(#key) + }.map(|inner| { + unsafe { objc2::rc::Id::cast(inner) } + }); + #from(value) + } + )); + } + } + + // emit the NSError machinery + tokens.append_all(quote!( + #[cfg(all(feature = "Foundation", feature = "Foundation_NSError", feature = "Foundation_NSString"))] + objc2::declare_class!( + #vis struct #error_ident {} + + unsafe impl ClassType for #error_ident { + type Super = icrate::Foundation::NSError; + const NAME: &'static str = #error_ident_string; + } + ); + + // NOTE: We could include the error codes as constants here, in addition to, or instead of + // on, the error code wrapper struct. Downsides to this would be that (1) we would have to + // duplicate all of the functionality of the `ns_typed_enum` here (more maintenance burden), + // or (2) perferm a second pass through the `item_enum` and only do some additional + // reprocessing (but this also means duplicated constants in both the error subclass struct + // and error code wrapper struct). + // + // Ideally, when (or if) inherent associated types land, we can just include a type + // referring to the codes and not worry about it. + #[cfg(all(feature = "Foundation", feature = "Foundation_NSError", feature = "Foundation_NSString"))] + impl #error_ident { + #[inline] + pub fn domain() -> &'static icrate::Foundation::NSErrorDomain { + #domain + } + #[inline] + pub fn code(&self) -> #mod_ident::Codes { + #mod_ident::Codes(self.as_super().code()) + } + #[inline] + pub fn localized_description(&self) -> objc2::rc::Id { + self.as_super().localizedDescription() + } + #[inline] + pub fn user_info(&self) -> #mod_ident::UserInfo { + let mut user_info = #mod_ident::UserInfo::default(); + #user_info_field_froms + user_info + } + #[inline] + pub fn new(code: #mod_ident::Codes) -> objc2::rc::Id { + let code = code.0; + let domain = Self::domain(); + let error = unsafe { + icrate::Foundation::NSError::new(code, domain) + }; + unsafe { objc2::rc::Id::cast(error) } + } + #[cfg(all(feature = "Foundation_NSDictionary", feature = "Foundation_NSMutableDictionary"))] + #[inline] + fn new_with_user_info( + code: #mod_ident::Codes, + user_info: #mod_ident::UserInfo, + ) -> objc2::rc::Id { + let domain = Self::domain(); + let code = code.0; + let dict = Self::user_info_into_dictionary(user_info); + let error = unsafe { + icrate::Foundation::NSError::errorWithDomain_code_userInfo(domain, code, Some(&*dict)) + }; + unsafe { objc2::rc::Id::cast(error) } + } + #[doc(hidden)] + #[inline] + fn user_info_into_dictionary( + user_info: #mod_ident::UserInfo, + ) -> objc2::rc::Id> { + let dict = icrate::Foundation::NSMutableDictionary::::new(); + #user_info_field_intos + objc2::rc::Id::into_shared(dict) + } + #user_info_getters + } + #[cfg(all(feature = "Foundation", feature = "Foundation_NSError", feature = "Foundation_NSString"))] + impl objc2::ErrorEnum for #error_ident { + type Codes = #mod_ident::Codes; + type UserInfo = #mod_ident::UserInfo; + } + #[cfg(all(feature = "Foundation", feature = "Foundation_NSError", feature = "Foundation_NSString"))] + impl objc2::TypedEnum for #error_ident { + type Cases = #mod_ident::Cases; + } + )); + + mod_items.append_all({ + let import = if let Some(import) = import { + if let syn::UseTree::Group(group) = import { + let trees = group.items.iter(); + quote!(#(use #trees;)*) + } else { + quote!(use #import;) + } + } else { + quote!() + }; + quote!( + #[allow(unused_imports)] + use super::*; + #[allow(unused_imports)] + use objc2::{rc::Id, runtime::Object}; + #[cfg(feature = "Foundation")] + #[allow(unused_imports)] + use icrate::Foundation::*; + #import + #[derive(Default)] + pub struct UserInfo { + #user_info_field_decls + } + ) + }); + + // emit the typed_enum machinery for the error codes + item_enum.ident = format_ident!("Codes"); + let ty = syn::parse_str::("isize")?; + let config = TypedEnumConfig { + emit_struct_in_module: true, + extensibility: Extensibility::Open, + mod_ident: Some(mod_ident), + mod_items, + }; + let error_codes = ns_typed_enum(item_enum, ty, config)?; + tokens.append_all(error_codes); + + Ok(tokens) +} 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..b1eaf12aa --- /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::Enum(item_enum) => crate::enumeration::item_enum(attr, item_enum), + ItemObjC::Fn(item_fn) => crate::function::item_fn(attr, item_fn), + ItemObjC::Impl(item_impl) => crate::implementation::item_impl(attr, item_impl), + ItemObjC::Static(item_static) => crate::constant::item_static(attr, item_static), + ItemObjC::Struct(item_struct) => crate::interface::item_struct(attr, item_struct), + ItemObjC::Trait(item_trait) => crate::protocol::item_trait(attr, item_trait), + ItemObjC::Type(item_type) => crate::interface::item_type(attr, item_type), + } +} + +pub(crate) enum ItemObjC { + Enum(syn::ItemEnum), + Fn(syn::ItemFn), + Impl(syn::ItemImpl), + Static(self::ItemStatic), + Struct(syn::ItemStruct), + Trait(syn::ItemTrait), + Type(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::Enum(item_enum)); + } + if lookahead.peek(syn::Token![static]) { + let item_static = parse_rest_of_static(input, attrs, vis)?; + return Ok(ItemObjC::Static(item_static)); + } + if lookahead.peek(syn::Token![struct]) { + let item_struct = parse_rest_of_struct(input, attrs, vis)?; + return Ok(ItemObjC::Struct(item_struct)); + } + if lookahead.peek(syn::Token![type]) { + let item_type = parse_rest_of_type(input, attrs, vis)?; + return Ok(ItemObjC::Type(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::Fn(item_fn)); + } + if lookahead.peek(syn::Token![impl]) { + let item_impl = parse_rest_of_impl(input, attrs, defaultness, unsafety)?; + return Ok(ItemObjC::Impl(item_impl)); + } + if lookahead.peek(syn::Token![trait]) { + let item_trait = parse_rest_of_trait(input, attrs, vis, unsafety, auto_token)?; + return Ok(ItemObjC::Trait(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()?); + } + 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)?; + 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::take(&mut last.attrs); + 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..dd9ec6023 100644 --- a/crates/objc2/src/__macro_helpers.rs +++ b/crates/objc2/src/__macro_helpers.rs @@ -630,6 +630,36 @@ impl ClassProtocolMethodsBuilder<'_, '_> { } } +// 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::Code` directly +#[cfg(feature = "objc2-proc-macros")] +/// Helper for proc-macro for NS_ERROR_ENUM. +/// This allows us to write `::Code` for pattern matching. +pub trait ErrorEnum { + type Codes; + type UserInfo; +} + +#[cfg(feature = "objc2-proc-macros")] +pub type Codes = ::Codes; + +#[cfg(feature = "objc2-proc-macros")] +pub type UserInfo = ::UserInfo; + +// 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 +#[cfg(feature = "objc2-proc-macros")] +/// Helper for proc-macro for NS_TYPED_ENUM, NS_TYPED_EXTENSIBLE_ENUM. +/// This allows us to write `::Cases::Variant` for pattern matching. +pub trait TypedEnum { + type Cases; +} + +#[cfg(feature = "objc2-proc-macros")] +pub type Cases = ::Cases; + #[cfg(test)] mod tests { use super::*; diff --git a/crates/objc2/src/declare.rs b/crates/objc2/src/declare.rs index d563eac30..1c8bc36d6 100644 --- a/crates/objc2/src/declare.rs +++ b/crates/objc2/src/declare.rs @@ -445,6 +445,49 @@ impl ClassBuilder { assert!(success.as_bool(), "Failed to add method {sel:?}"); } + /// Adds a method with the given name and implementation. + /// + /// + /// # Safety + /// + /// The caller must ensure that the types match those that are expected + /// when the method is invoked from Objective-C. + 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..1422233f9 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::{Cases, Codes, ErrorEnum, TypedEnum, UserInfo}; +#[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..740783355 --- /dev/null +++ b/crates/objc2/tests/proc_macros.rs @@ -0,0 +1,321 @@ +#![cfg(feature = "objc2-proc-macros")] + +use objc2::{objc, Cases}; + +#[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 = usize)] + 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 = usize)] + 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 = usize)] + 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 = usize)] + 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 = usize)] + 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 = usize)] + 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; + // without macro + let did_match = match value.cases() { + ::Cases::Var0 => false, + ::Cases::Var1 => false, + ::Cases::Var2 => true, + ::Cases::Var3 => false, + }; + assert!(did_match); + // with macro + let did_match = match value.cases() { + Cases::::Var0 => false, + Cases::::Var1 => false, + Cases::::Var2 => true, + Cases::::Var3 => false, + }; + assert!(did_match); +} + +#[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: &str = "foo"; +}