From 554d293f9d07877a27ebf398b65071ba2a51bef9 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 | 124 ++ crates/icrate/Cargo.toml | 24 + crates/icrate/examples/browser.rs | 1 + crates/icrate/examples/ns_error_enum.rs | 251 ++++ crates/icrate/tests/enumerations.rs | 388 ++++++ crates/icrate/tests/proc_macros.rs | 309 +++++ crates/objc2-proc-macros/Cargo.toml | 7 + crates/objc2-proc-macros/src/common.rs | 64 + crates/objc2-proc-macros/src/declare.rs | 106 ++ crates/objc2-proc-macros/src/enumeration.rs | 1236 +++++++++++++++++ .../objc2-proc-macros/src/implementation.rs | 51 + crates/objc2-proc-macros/src/interface.rs | 368 +++++ .../src/interface/external.rs | 337 +++++ .../src/interface/internal.rs | 37 + crates/objc2-proc-macros/src/lib.rs | 129 ++ crates/objc2-proc-macros/src/protocol.rs | 45 + crates/objc2/Cargo.toml | 5 +- crates/objc2/src/__macro_helpers/mod.rs | 30 + crates/objc2/src/declare/mod.rs | 43 + crates/objc2/src/export.rs | 48 + crates/objc2/src/lib.rs | 16 +- crates/objc2/src/macros/declare_class.rs | 2 +- crates/objc2/src/rc/id.rs | 2 +- 24 files changed, 3621 insertions(+), 6 deletions(-) create mode 100644 crates/icrate/examples/ns_error_enum.rs create mode 100644 crates/icrate/tests/enumerations.rs create mode 100644 crates/icrate/tests/proc_macros.rs create mode 100644 crates/objc2-proc-macros/src/common.rs create mode 100644 crates/objc2-proc-macros/src/declare.rs create mode 100644 crates/objc2-proc-macros/src/enumeration.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/interface/external.rs create mode 100644 crates/objc2-proc-macros/src/interface/internal.rs create mode 100644 crates/objc2-proc-macros/src/protocol.rs create mode 100644 crates/objc2/src/export.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..4911e037d 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" @@ -231,6 +259,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 +278,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 +310,10 @@ dependencies = [ name = "objc2" version = "0.3.0-beta.5" dependencies = [ + "bitflags", "iai", "malloc_buf", + "num_enum", "objc-sys", "objc2-encode", "objc2-proc-macros", @@ -266,6 +326,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 +358,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 +655,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 +757,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 43fb215cb..5339e2261 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/browser.rs b/crates/icrate/examples/browser.rs index d127b5127..83340568e 100644 --- a/crates/icrate/examples/browser.rs +++ b/crates/icrate/examples/browser.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "unstable-example-browser")] #![deny(unsafe_op_in_unsafe_fn)] use icrate::{ diff --git a/crates/icrate/examples/ns_error_enum.rs b/crates/icrate/examples/ns_error_enum.rs new file mode 100644 index 000000000..32ff608e1 --- /dev/null +++ b/crates/icrate/examples/ns_error_enum.rs @@ -0,0 +1,251 @@ +#![allow(non_snake_case)] + +use block2::ConcreteBlock; +use icrate::{ + ns_string, + objc2::{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::*; + + #[objc2::typed_extensible_enum(type = isize)] // or #[objc(typed_extensible_enum, type = isize)] + 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()) + } + } + + #[objc2::typed_extensible_enum(type = isize)] // or #[objc(typed_extensible_enum, type = isize)] + 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()) + } + } +} + +#[objc2::error_enum( // or #[objc(error_enum, + domain = unsafe { NSURLErrorDomain }, + // NOTE: Here we specify (typed) getters for the associated `userInfo` dict. + user_info = [ + /// # SAFETY (type) + /// https://developer.apple.com/documentation/foundation/nsurlerrorfailingurlerrorkey?language=objc + { key = NSURLErrorFailingURLErrorKey, unsafe type = Id }, + /// # SAFETY (type) + /// https://developer.apple.com/documentation/foundation/nsurlerrorfailingurlstringerrorkey?language=objc + { key = NSURLErrorFailingURLStringErrorKey, unsafe type = Id }, + /// # SAFETY (type) + /// https://developer.apple.com/documentation/foundation/nsurlerrorbackgroundtaskcancelledreasonkey?language=objc + { key = NSURLErrorBackgroundTaskCancelledReasonKey, unsafe type = ns_url_error_reasons::BackgroundTaskCancelledReason }, + /// # SAFETY (type) + /// https://developer.apple.com/documentation/foundation/nsurlerrornetworkunavailablereasonkey?language=objc' + /// + /// # SAFETY (from) + /// 1. Casting dictionary `Object` to `NSNumber` is safe (type) + /// 2. `NSNumber` can safely be converted to `NetworkUnavailableReason` + /// + /// # SAFETY (into) + /// `NetworkUnavailableReason` can safely be converted to `NSObject` via deref (through + /// converting to `NSNumber` first). This happens implicitly in the `&*arg` that + /// occurs during the call to insert the value in the dictionary. + { + key = NSURLErrorNetworkUnavailableReasonKey, + unsafe type = ns_url_error_reasons::NetworkUnavailableReason, + unsafe from = |n: Option>| n.map(TryInto::try_into).and_then(Result::ok), + unsafe into = std::convert::identity, + // NOTE: `from` and `into` aren't necessary in this case since they can be computed + // automatically but this is what they look like for demonstration purposes. + }, + ] +)] +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::::UNSUPPORTED_URL)); +} + +fn example_throw() { + use ns_url_error_reasons::BackgroundTaskCancelledReason; + let reason = Some(BackgroundTaskCancelledReason::BACKGROUND_UPDATES_DISABLED); + let code = Codes::::UNSUPPORTED_URL; + 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/enumerations.rs b/crates/icrate/tests/enumerations.rs new file mode 100644 index 000000000..df52968e8 --- /dev/null +++ b/crates/icrate/tests/enumerations.rs @@ -0,0 +1,388 @@ +#![cfg(all( + feature = "unstable-proc-macros", + feature = "Foundation_NSDictionary", + feature = "Foundation_NSError", + feature = "Foundation_NSMutableDictionary", + feature = "Foundation_NSNumber", + feature = "Foundation_NSString", + feature = "Foundation", +))] + +#[test] +fn ns_options() { + #[objc2::options(repr = usize)] + enum NSSortOptions { + NSSortConcurrent = 1 << 0, + NSSortStable = 1 << 4, + } +} +/* +objc2::bitflags::bitflags! { + #[repr(transparent)]#[derive(core::default::Default)]struct NSSortOptions:usize { + #[doc(alias = "NSSortConcurrent")]const CONCURRENT = 1<<0; + #[doc(alias = "NSSortStable")]const STABLE = 1<<4; + } +} +unsafe impl objc2::Encode for NSSortOptions { + const ENCODING: objc2::Encoding = ::ENCODING; +} +unsafe impl objc2::RefEncode for NSSortOptions { + const ENCODING_REF: objc2::Encoding = ::ENCODING_REF; +} +*/ + +#[test] +fn ns_enum() { + #[objc2::r#enum(repr = isize)] + enum NSDecodingFailurePolicy { + NSDecodingFailurePolicyRaiseException, + NSDecodingFailurePolicySetErrorAndReturn, + } +} +/* +#[derive( + core::marker::Copy, + core::clone::Clone, + core::cmp::Ord, + core::cmp::PartialOrd, + core::hash::Hash, + core::cmp::Eq, + core::cmp::PartialEq, +)] +#[repr(transparent)] +struct NSDecodingFailurePolicy(isize); + +unsafe impl objc2::Encode for NSDecodingFailurePolicy { + const ENCODING: objc2::Encoding = ::ENCODING; +} +unsafe impl objc2::RefEncode for NSDecodingFailurePolicy { + const ENCODING_REF: objc2::Encoding = ::ENCODING_REF; +} +impl NSDecodingFailurePolicy { + #[doc(alias = "NSDecodingFailurePolicyRaiseException")] + pub const RAISE_EXCEPTION: Self = Self(NSDecodingFailurePolicyCases::RaiseException as isize); + #[doc(alias = "NSDecodingFailurePolicySetErrorAndReturn")] + pub const SET_ERROR_AND_RETURN: Self = + Self(NSDecodingFailurePolicyCases::SetErrorAndReturn as isize); + pub fn cases(&self) -> core::option::Option { + match self { + &Self::RAISE_EXCEPTION => NSDecodingFailurePolicyCases::RaiseException.into(), + &Self::SET_ERROR_AND_RETURN => NSDecodingFailurePolicyCases::SetErrorAndReturn.into(), + _ => core::option::Option::None, + } + } + #[inline] + pub fn peek(&self) -> &isize { + &self.0 + } + #[inline] + pub fn take(self) -> isize { + self.0 + } + #[inline] + pub fn as_raw(self) -> isize { + self.take() + } + #[inline] + pub fn from_raw(raw: isize) -> Option { + raw.try_into().ok() + } + #[inline] + pub fn from_raw_unchecked(raw: isize) -> Self { + Self(raw) + } +} +impl core::fmt::Debug for NSDecodingFailurePolicy { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + if let Some(cases) = self.cases() { + core::fmt::Debug::fmt(&cases, f) + } else { + f.debug_tuple("NSDecodingFailurePolicy") + .field(&self.0) + .finish() + } + } +} +impl core::convert::From for isize { + #[inline] + fn from(wrapper: NSDecodingFailurePolicy) -> Self { + wrapper.take() + } +} +impl core::convert::TryFrom for NSDecodingFailurePolicy { + type Error = &'static str; + fn try_from(raw: isize) -> Result { + match Self(raw) { + value @ Self::RAISE_EXCEPTION => Ok(value), + value @ Self::SET_ERROR_AND_RETURN => Ok(value), + _ => Err("Value does not match any of the enum values"), + } + } +} +#[cfg(all(feature = "Foundation", feature = "Foundation_NSNumber"))] +impl core::convert::From for objc2::rc::Id { + #[inline] + fn from(value: NSDecodingFailurePolicy) -> Self { + icrate::Foundation::NSNumber::new_isize(value.0.into()) + } +} +#[cfg(all(feature = "Foundation", feature = "Foundation_NSNumber"))] +impl core::convert::TryFrom<&icrate::Foundation::NSNumber> for NSDecodingFailurePolicy { + type Error = &'static str; + fn try_from(number: &icrate::Foundation::NSNumber) -> Result { + use objc2::Encoding::*; + let encoding = number.encoding(); + let equivalent = match "isize" { + "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 { + number.as_isize().try_into() + } else { + Err("NSNumber encoding is not equivalent to error repr") + } + } +} +#[cfg(all(feature = "Foundation", feature = "Foundation_NSNumber"))] +impl core::convert::TryFrom> + for NSDecodingFailurePolicy +{ + type Error = &'static str; + #[inline] + fn try_from( + number: objc2::rc::Id, + ) -> Result { + (&*number).try_into() + } +} +#[cfg(feature = "Foundation")] +impl core::ops::Deref for NSDecodingFailurePolicy { + type Target = icrate::Foundation::NSObject; + fn deref(&self) -> &Self::Target { + let num = icrate::Foundation::NSNumber::new_isize(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 objc2::TypedEnum for NSDecodingFailurePolicy { + type Cases = NSDecodingFailurePolicyCases; +} +#[derive( + core::marker::Copy, + core::clone::Clone, + core::fmt::Debug, + core::cmp::Eq, + core::cmp::PartialEq, + core::cmp::Ord, + core::cmp::PartialOrd, + core::hash::Hash, +)] +#[non_exhaustive] +pub enum NSDecodingFailurePolicyCases { + #[doc(alias = "NSDecodingFailurePolicyRaiseException")] + RaiseException, + #[doc(alias = "NSDecodingFailurePolicySetErrorAndReturn")] + SetErrorAndReturn, +} +*/ + +#[test] +fn ns_typed_enum() { + #[objc2::typed_enum(type = &'static icrate::Foundation::NSStringEncodingDetectionOptionsKey)] + enum NSStringEncodingDetectionOptionsKey { + NSStringEncodingDetectionSuggestedEncodingsKey = + unsafe { icrate::Foundation::NSStringEncodingDetectionSuggestedEncodingsKey }, + NSStringEncodingDetectionDisallowedEncodingsKey = + unsafe { icrate::Foundation::NSStringEncodingDetectionDisallowedEncodingsKey }, + NSStringEncodingDetectionUseOnlySuggestedEncodingsKey = + unsafe { icrate::Foundation::NSStringEncodingDetectionUseOnlySuggestedEncodingsKey }, + NSStringEncodingDetectionAllowLossyKey = + unsafe { icrate::Foundation::NSStringEncodingDetectionAllowLossyKey }, + NSStringEncodingDetectionFromWindowsKey = + unsafe { icrate::Foundation::NSStringEncodingDetectionFromWindowsKey }, + NSStringEncodingDetectionLossySubstitutionKey = + unsafe { icrate::Foundation::NSStringEncodingDetectionLossySubstitutionKey }, + NSStringEncodingDetectionLikelyLanguageKey = + unsafe { icrate::Foundation::NSStringEncodingDetectionLikelyLanguageKey }, + } +} +/* +#[derive(core::hash::Hash, core::cmp::Eq, core::cmp::PartialEq)] +#[repr(C)] +struct NSStringEncodingDetectionOptionsKey( + &'static icrate::Foundation::NSStringEncodingDetectionOptionsKey, +); + +unsafe impl objc2::Encode for NSStringEncodingDetectionOptionsKey { + const ENCODING: objc2::Encoding = + <&'static icrate::Foundation::NSStringEncodingDetectionOptionsKey>::ENCODING; +} +unsafe impl objc2::RefEncode for NSStringEncodingDetectionOptionsKey { + const ENCODING_REF: objc2::Encoding = + <&'static icrate::Foundation::NSStringEncodingDetectionOptionsKey>::ENCODING_REF; +} +impl NSStringEncodingDetectionOptionsKey { + #[doc(alias = "NSStringEncodingDetectionSuggestedEncodingsKey")] + pub fn suggested_encodings() -> Self { + Self(unsafe { icrate::Foundation::NSStringEncodingDetectionSuggestedEncodingsKey }) + } + #[doc(alias = "NSStringEncodingDetectionDisallowedEncodingsKey")] + pub fn disallowed_encodings() -> Self { + Self(unsafe { icrate::Foundation::NSStringEncodingDetectionDisallowedEncodingsKey }) + } + #[doc(alias = "NSStringEncodingDetectionUseOnlySuggestedEncodingsKey")] + pub fn use_only_suggested_encodings() -> Self { + Self(unsafe { icrate::Foundation::NSStringEncodingDetectionUseOnlySuggestedEncodingsKey }) + } + #[doc(alias = "NSStringEncodingDetectionAllowLossyKey")] + pub fn allow_lossy() -> Self { + Self(unsafe { icrate::Foundation::NSStringEncodingDetectionAllowLossyKey }) + } + #[doc(alias = "NSStringEncodingDetectionFromWindowsKey")] + pub fn from_windows() -> Self { + Self(unsafe { icrate::Foundation::NSStringEncodingDetectionFromWindowsKey }) + } + #[doc(alias = "NSStringEncodingDetectionLossySubstitutionKey")] + pub fn lossy_substitution() -> Self { + Self(unsafe { icrate::Foundation::NSStringEncodingDetectionLossySubstitutionKey }) + } + #[doc(alias = "NSStringEncodingDetectionLikelyLanguageKey")] + pub fn likely_language() -> Self { + Self(unsafe { icrate::Foundation::NSStringEncodingDetectionLikelyLanguageKey }) + } + pub fn cases(&self) -> NSStringEncodingDetectionOptionsKeyCases { + match self.peek() { + &value if value == Self::suggested_encodings().take() => { + NSStringEncodingDetectionOptionsKeyCases::SuggestedEncodings.into() + } + &value if value == Self::disallowed_encodings().take() => { + NSStringEncodingDetectionOptionsKeyCases::DisallowedEncodings.into() + } + &value if value == Self::use_only_suggested_encodings().take() => { + NSStringEncodingDetectionOptionsKeyCases::UseOnlySuggestedEncodings.into() + } + &value if value == Self::allow_lossy().take() => { + NSStringEncodingDetectionOptionsKeyCases::AllowLossy.into() + } + &value if value == Self::from_windows().take() => { + NSStringEncodingDetectionOptionsKeyCases::FromWindows.into() + } + &value if value == Self::lossy_substitution().take() => { + NSStringEncodingDetectionOptionsKeyCases::LossySubstitution.into() + } + &value if value == Self::likely_language().take() => { + NSStringEncodingDetectionOptionsKeyCases::LikelyLanguage.into() + } + _wrapper => core::unreachable!("unexpected case in (closed) typed enum"), + } + } + #[inline] + pub fn peek(&self) -> &&'static icrate::Foundation::NSStringEncodingDetectionOptionsKey { + &self.0 + } + #[inline] + pub fn take(self) -> &'static icrate::Foundation::NSStringEncodingDetectionOptionsKey { + self.0 + } + #[inline] + pub fn as_raw(self) -> &'static icrate::Foundation::NSStringEncodingDetectionOptionsKey { + self.take() + } + #[inline] + pub fn from_raw( + raw: &'static icrate::Foundation::NSStringEncodingDetectionOptionsKey, + ) -> Option { + raw.try_into().ok() + } + #[inline] + pub fn from_raw_unchecked( + raw: &'static icrate::Foundation::NSStringEncodingDetectionOptionsKey, + ) -> Self { + Self(raw) + } +} +impl core::fmt::Debug for NSStringEncodingDetectionOptionsKey { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Debug::fmt(&self.cases(), f) + } +} +impl core::convert::From + for &'static icrate::Foundation::NSStringEncodingDetectionOptionsKey +{ + #[inline] + fn from(wrapper: NSStringEncodingDetectionOptionsKey) -> Self { + wrapper.take() + } +} +impl core::convert::TryFrom<&'static icrate::Foundation::NSStringEncodingDetectionOptionsKey> + for NSStringEncodingDetectionOptionsKey +{ + type Error = &'static str; + fn try_from( + raw: &'static icrate::Foundation::NSStringEncodingDetectionOptionsKey, + ) -> Result { + match raw { + value if value == Self::suggested_encodings().take() => Ok(Self(value)), + value if value == Self::disallowed_encodings().take() => Ok(Self(value)), + value if value == Self::use_only_suggested_encodings().take() => Ok(Self(value)), + value if value == Self::allow_lossy().take() => Ok(Self(value)), + value if value == Self::from_windows().take() => Ok(Self(value)), + value if value == Self::lossy_substitution().take() => Ok(Self(value)), + value if value == Self::likely_language().take() => Ok(Self(value)), + _ => Err("Value does not match any of the enum values"), + } + } +} +impl objc2::TypedEnum for NSStringEncodingDetectionOptionsKey { + type Cases = NSStringEncodingDetectionOptionsKeyCases; +} +#[derive( + core::marker::Copy, + core::clone::Clone, + core::fmt::Debug, + core::cmp::Eq, + core::cmp::PartialEq, + core::cmp::Ord, + core::cmp::PartialOrd, + core::hash::Hash, +)] +pub enum NSStringEncodingDetectionOptionsKeyCases { + #[doc(alias = "NSStringEncodingDetectionSuggestedEncodingsKey")] + SuggestedEncodings, + #[doc(alias = "NSStringEncodingDetectionDisallowedEncodingsKey")] + DisallowedEncodings, + #[doc(alias = "NSStringEncodingDetectionUseOnlySuggestedEncodingsKey")] + UseOnlySuggestedEncodings, + #[doc(alias = "NSStringEncodingDetectionAllowLossyKey")] + AllowLossy, + #[doc(alias = "NSStringEncodingDetectionFromWindowsKey")] + FromWindows, + #[doc(alias = "NSStringEncodingDetectionLossySubstitutionKey")] + LossySubstitution, + #[doc(alias = "NSStringEncodingDetectionLikelyLanguageKey")] + LikelyLanguage, +} +*/ diff --git a/crates/icrate/tests/proc_macros.rs b/crates/icrate/tests/proc_macros.rs new file mode 100644 index 000000000..56f71ef55 --- /dev/null +++ b/crates/icrate/tests/proc_macros.rs @@ -0,0 +1,309 @@ +#![cfg(all( + feature = "unstable-proc-macros", + feature = "AppKit", + feature = "AppKit_NSEvent", + feature = "AppKit_NSFont", + feature = "AppKit_NSView", + feature = "Foundation_NSCoder", + feature = "Foundation_NSDictionary", + feature = "Foundation_NSError", + feature = "Foundation_NSFormatter", + feature = "Foundation_NSMutableDictionary", + feature = "Foundation_NSNumber", + feature = "Foundation_NSString", + feature = "Foundation", +))] + +#[allow(unused_imports)] +use icrate::{AppKit::NSView, Foundation::NSObject}; + +#[test] +fn interface_struct() { + #![allow(dead_code)] + #[objc2::interface(super = NSObject, inherits = [])] + mod __ { + struct C {} // locally defined class + } +} + +#[test] +fn interface_type() { + use icrate::{ + AppKit::{ + NSControlSize, NSEvent, NSEventMask, NSFont, NSLineBreakMode, NSResponder, + NSTextAlignment, NSWritingDirection, + }, + Foundation::{ + NSAttributedString, NSCoder, NSFormatter, NSInteger, NSRect, NSSize, NSString, + }, + }; + use objc2::{ + rc::{Allocated, Id}, + runtime::{Object, Sel}, + }; + use std::ffi::{c_double, c_float, c_int}; + + #[objc2::interface( + unsafe super = NSView, + unsafe inherits = [ + #[cfg(feature = "AppKit_NSResponder")] + NSResponder, + NSObject, + ] + )] + extern "Objective-C" { + // NOTE: a single `type` is allowed (and required) per interface-extern block (so `self` is never ambiguous) + #[cfg(feature = "AppKit_NSControl")] + #[derive(Debug, PartialEq, Eq, Hash)] + pub type NSControl; + + #[objc2::method(sel = "initWithFrame:", managed = "Init")] + pub unsafe fn initWithFrame(this: Option>, frame_rect: NSRect) -> Id; + + #[cfg(feature = "Foundation_NSCoder")] + #[objc2::method(sel = "initWithCoder:", managed = "Init")] + pub unsafe fn initWithCoder( + this: Option>, + coder: &NSCoder, + ) -> Option>; + + #[objc2::method(sel = "target", managed = "Other")] + pub unsafe fn target(&self) -> Option>; + + #[objc2::method(sel = "setTarget:")] + pub unsafe fn setTarget(&self, target: Option<&Object>); + + #[objc2::method(sel = "action")] + pub unsafe fn action(&self) -> Option; + + #[objc2::method(sel = "setAction:")] + pub unsafe fn setAction(&self, action: Option); + + #[objc2::method(sel = "tag")] + pub unsafe fn tag(&self) -> NSInteger; + + #[objc2::method(sel = "setTag:")] + pub unsafe fn setTag(&self, tag: NSInteger); + + #[objc2::method(sel = "ignoresMultiClick")] + pub unsafe fn ignoresMultiClick(&self) -> bool; + + #[objc2::method(sel = "setIgnoresMultiClick:")] + pub unsafe fn setIgnoresMultiClick(&self, ignores_multi_click: bool); + + #[objc2::method(sel = "isContinuous")] + pub unsafe fn isContinuous(&self) -> bool; + + #[objc2::method(sel = "setContinuous:")] + pub unsafe fn setContinuous(&self, continuous: bool); + + #[objc2::method(sel = "isEnabled")] + pub unsafe fn isEnabled(&self) -> bool; + + #[objc2::method(sel = "setEnabled:")] + pub unsafe fn setEnabled(&self, enabled: bool); + + #[objc2::method(sel = "refusesFirstResponder")] + pub unsafe fn refusesFirstResponder(&self) -> bool; + + #[objc2::method(sel = "setRefusesFirstResponder:")] + pub unsafe fn setRefusesFirstResponder(&self, refuses_first_responder: bool); + + #[objc2::method(sel = "isHighlighted")] + pub unsafe fn isHighlighted(&self) -> bool; + + #[objc2::method(sel = "setHighlighted:")] + pub unsafe fn setHighlighted(&self, highlighted: bool); + + #[objc2::method(sel = "controlSize")] + pub unsafe fn controlSize(&self) -> NSControlSize; + + #[objc2::method(sel = "setControlSize:")] + pub unsafe fn setControlSize(&self, control_size: NSControlSize); + + #[cfg(feature = "Foundation_NSFormatter")] + #[objc2::method(sel = "formatter", managed = "Other")] + pub unsafe fn formatter(&self) -> Option>; + + #[cfg(feature = "Foundation_NSFormatter")] + #[objc2::method(sel = "setFormatter:")] + pub unsafe fn setFormatter(&self, formatter: Option<&NSFormatter>); + + #[objc2::method(sel = "objectValue", managed = "Other")] + pub unsafe fn objectValue(&self) -> Option>; + + #[objc2::method(sel = "setObjectValue:")] + pub unsafe fn setObjectValue(&self, object_value: Option<&Object>); + + #[cfg(feature = "Foundation_NSString")] + #[objc2::method(sel = "stringValue", managed = "Other")] + pub unsafe fn stringValue(&self) -> Id; + + #[cfg(feature = "Foundation_NSString")] + #[objc2::method(sel = "setStringValue:")] + pub unsafe fn setStringValue(&self, string_value: &NSString); + + #[cfg(feature = "Foundation_NSAttributedString")] + #[objc2::method(sel = "attributedStringValue", managed = "Other")] + pub unsafe fn attributedStringValue(&self) -> Id; + + #[cfg(feature = "Foundation_NSAttributedString")] + #[objc2::method(sel = "setAttributedStringValue:")] + pub unsafe fn setAttributedStringValue(&self, attributed_string_value: &NSAttributedString); + + #[objc2::method(sel = "intValue")] + pub unsafe fn intValue(&self) -> c_int; + + #[objc2::method(sel = "setIntValue:")] + pub unsafe fn setIntValue(&self, int_value: c_int); + + #[objc2::method(sel = "integerValue")] + pub unsafe fn integerValue(&self) -> NSInteger; + + #[objc2::method(sel = "setIntegerValue:")] + pub unsafe fn setIntegerValue(&self, integer_value: NSInteger); + + #[objc2::method(sel = "floatValue")] + pub unsafe fn floatValue(&self) -> c_float; + + #[objc2::method(sel = "setFloatValue:")] + pub unsafe fn setFloatValue(&self, float_value: c_float); + + #[objc2::method(sel = "doubleValue")] + pub unsafe fn doubleValue(&self) -> c_double; + + #[objc2::method(sel = "setDoubleValue:")] + pub unsafe fn setDoubleValue(&self, double_value: c_double); + + #[objc2::method(sel = "sizeThatFits:")] + pub unsafe fn sizeThatFits(&self, size: NSSize) -> NSSize; + + #[objc2::method(sel = "sizeToFit")] + pub unsafe fn sizeToFit(&self); + + #[objc2::method(sel = "sendActionOn:")] + pub unsafe fn sendActionOn(&self, mask: NSEventMask) -> NSInteger; + + #[objc2::method(sel = "sendAction:to:")] + pub unsafe fn sendAction_to(&self, action: Option, target: Option<&Object>) -> bool; + + #[objc2::method(sel = "takeIntValueFrom:")] + pub unsafe fn takeIntValueFrom(&self, sender: Option<&Object>); + + #[objc2::method(sel = "takeFloatValueFrom:")] + pub unsafe fn takeFloatValueFrom(&self, sender: Option<&Object>); + + #[objc2::method(sel = "takeDoubleValueFrom:")] + pub unsafe fn takeDoubleValueFrom(&self, sender: Option<&Object>); + + #[objc2::method(sel = "takeStringValueFrom:")] + pub unsafe fn takeStringValueFrom(&self, sender: Option<&Object>); + + #[objc2::method(sel = "takeObjectValueFrom:")] + pub unsafe fn takeObjectValueFrom(&self, sender: Option<&Object>); + + #[objc2::method(sel = "takeIntegerValueFrom:")] + pub unsafe fn takeIntegerValueFrom(&self, sender: Option<&Object>); + + #[cfg(feature = "AppKit_NSEvent")] + #[objc2::method(sel = "mouseDown:")] + pub unsafe fn mouseDown(&self, event: &NSEvent); + + #[objc2::method(sel = "performClick:")] + pub unsafe fn performClick(&self, sender: Option<&Object>); + + #[cfg(feature = "AppKit_NSFont")] + #[objc2::method(sel = "font", managed = "Other")] + pub unsafe fn font(&self) -> Option>; + + #[cfg(feature = "AppKit_NSFont")] + #[objc2::method(sel = "setFont:")] + pub unsafe fn setFont(&self, font: Option<&NSFont>); + + #[objc2::method(sel = "usesSingleLineMode")] + pub unsafe fn usesSingleLineMode(&self) -> bool; + + #[objc2::method(sel = "setUsesSingleLineMode:")] + pub unsafe fn setUsesSingleLineMode(&self, uses_single_line_mode: bool); + + #[objc2::method(sel = "lineBreakMode")] + pub unsafe fn lineBreakMode(&self) -> NSLineBreakMode; + + #[objc2::method(sel = "setLineBreakMode:")] + pub unsafe fn setLineBreakMode(&self, line_break_mode: NSLineBreakMode); + + #[objc2::method(sel = "alignment")] + pub unsafe fn alignment(&self) -> NSTextAlignment; + + #[objc2::method(sel = "setAlignment:")] + pub unsafe fn setAlignment(&self, alignment: NSTextAlignment); + + #[objc2::method(sel = "baseWritingDirection")] + pub unsafe fn baseWritingDirection(&self) -> NSWritingDirection; + + #[objc2::method(sel = "setBaseWritingDirection:")] + pub unsafe fn setBaseWritingDirection(&self, base_writing_direction: NSWritingDirection); + + #[objc2::method(sel = "allowsExpansionToolTips")] + pub unsafe fn allowsExpansionToolTips(&self) -> bool; + + #[objc2::method(sel = "setAllowsExpansionToolTips:")] + pub unsafe fn setAllowsExpansionToolTips(&self, allows_expansion_tool_tips: bool); + + #[objc2::method(sel = "expansionFrameWithFrame:")] + pub unsafe fn expansionFrameWithFrame(&self, content_frame: NSRect) -> NSRect; + + #[objc2::method(sel = "drawWithExpansionFrame:inView:")] + pub unsafe fn drawWithExpansionFrame_inView(&self, content_frame: NSRect, view: &NSView); + } +} + +#[test] +fn implementation() { + #[allow(dead_code)] + #[objc2::interface(super = NSObject)] + mod __ { + struct C {} + + impl C {} + } +} + +#[test] +fn ns_enum() { + #[objc2::r#enum(repr = isize)] + enum NSDecodingFailurePolicy { + NSDecodingFailurePolicyRaiseException, + NSDecodingFailurePolicySetErrorAndReturn, + } +} + +#[test] +fn ns_options() { + #[objc2::options(repr = usize)] + enum NSSortOptions { + NSSortConcurrent = 1 << 0, + NSSortStable = 1 << 4, + } +} + +#[test] +fn ns_typed_enum() { + #[objc2::typed_enum(type = &'static icrate::Foundation::NSStringEncodingDetectionOptionsKey)] + enum NSStringEncodingDetectionOptionsKey { + NSStringEncodingDetectionSuggestedEncodingsKey = + unsafe { icrate::Foundation::NSStringEncodingDetectionSuggestedEncodingsKey }, + NSStringEncodingDetectionDisallowedEncodingsKey = + unsafe { icrate::Foundation::NSStringEncodingDetectionDisallowedEncodingsKey }, + NSStringEncodingDetectionUseOnlySuggestedEncodingsKey = + unsafe { icrate::Foundation::NSStringEncodingDetectionUseOnlySuggestedEncodingsKey }, + NSStringEncodingDetectionAllowLossyKey = + unsafe { icrate::Foundation::NSStringEncodingDetectionAllowLossyKey }, + NSStringEncodingDetectionFromWindowsKey = + unsafe { icrate::Foundation::NSStringEncodingDetectionFromWindowsKey }, + NSStringEncodingDetectionLossySubstitutionKey = + unsafe { icrate::Foundation::NSStringEncodingDetectionLossySubstitutionKey }, + NSStringEncodingDetectionLikelyLanguageKey = + unsafe { icrate::Foundation::NSStringEncodingDetectionLikelyLanguageKey }, + } +} diff --git a/crates/objc2-proc-macros/Cargo.toml b/crates/objc2-proc-macros/Cargo.toml index 6ca19ff8e..13446a166 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", "extra-traits"] } diff --git a/crates/objc2-proc-macros/src/common.rs b/crates/objc2-proc-macros/src/common.rs new file mode 100644 index 000000000..e517502c4 --- /dev/null +++ b/crates/objc2-proc-macros/src/common.rs @@ -0,0 +1,64 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::parse::{Parse, ParseStream}; + +#[derive(Default)] +pub(crate) struct EmptyToken; + +impl Parse for EmptyToken { + fn parse(_input: ParseStream<'_>) -> syn::Result { + Ok(Self) + } +} + +impl ToTokens for EmptyToken { + fn to_tokens(&self, _tokens: &mut TokenStream) {} +} + +/// Helper type for parsing the UserInfo specification fields. +pub(crate) struct KeyVal { + #[allow(unused)] + pub(crate) punctuation: Punctuation, + #[allow(unused)] + pub(crate) unsafety: Unsafety, + #[allow(unused)] + pub(crate) key: Key, + #[allow(unused)] + pub(crate) separator: syn::Token![=], + pub(crate) val: Val, +} + +impl Parse for KeyVal +where + Key: Parse, + Val: Parse, + Punctuation: Parse, + Unsafety: Parse, +{ + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(KeyVal { + punctuation: input.parse()?, + unsafety: input.parse()?, + key: input.parse()?, + separator: input.parse()?, + val: input.parse()?, + }) + } +} + +pub(crate) fn check_abi(item_extern: &syn::ItemForeignMod) -> syn::Result<()> { + if item_extern + .abi + .name + .as_ref() + .map(|lit| lit.value()) + .as_deref() + != Some("Objective-C") + { + return Err(syn::Error::new_spanned( + &item_extern.abi, + r#"objc2: ABI must be "Objective-C""#, + )); + } + Ok(()) +} diff --git a/crates/objc2-proc-macros/src/declare.rs b/crates/objc2-proc-macros/src/declare.rs new file mode 100644 index 000000000..84a46dc42 --- /dev/null +++ b/crates/objc2-proc-macros/src/declare.rs @@ -0,0 +1,106 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::parse::{Parse, ParseStream}; + +pub(crate) struct MacroArgsRest; + +impl From for MacroArgs { + fn from(rest: MacroArgsRest) -> Self { + Self { + declare_token: Default::default(), + rest, + } + } +} + +impl Parse for MacroArgsRest { + fn parse(_input: ParseStream<'_>) -> syn::Result { + Ok(Self) + } +} + +pub(crate) struct MacroArgs { + #[allow(unused)] + declare_token: self::tokens::declare, + #[allow(unused)] + rest: MacroArgsRest, +} + +impl Parse for MacroArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + declare_token: input.parse()?, + rest: input.parse()?, + }) + } +} + +pub(crate) mod tokens { + syn::custom_keyword!(declare); +} + +pub(crate) fn item_declare( + _args: MacroArgs, + mut item_extern: syn::ItemForeignMod, +) -> syn::Result { + let mut tokens = TokenStream::new(); + for item in &mut item_extern.items { + match item { + syn::ForeignItem::Fn(item) => { + item_fn(&mut tokens, item)?; + } + syn::ForeignItem::Static(_) => { + item.to_tokens(&mut tokens); + } + _ => { + return Err(syn::Error::new_spanned( + item_extern, + "objc2: only `fn` and `static` items are allowed in `extern` for #[declare]", + )); + } + } + } + todo!() +} + +fn item_fn(tokens: &mut TokenStream, item: &mut syn::ForeignItemFn) -> syn::Result<()> { + if item.sig.receiver().is_some() { + return Err(syn::Error::new_spanned( + item, + "objc2: methods are only allowed in #[interface] `extern` blocks", + )); + } else if item.sig.unsafety.is_some() { + item_fn_unsafe(tokens, item); + } else { + item_fn_safe(tokens, item); + } + Ok(()) +} + +fn item_fn_safe(tokens: &mut TokenStream, item: &mut syn::ForeignItemFn) { + let syn::ForeignItemFn { + attrs, vis, sig, .. + } = &item; + 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!( + #[inline] + #(#attrs)* + #vis extern "C" #sig { + extern "C" { + #sig; + } + unsafe { + #ident(#(#args,)*) + } + } + )); +} + +fn item_fn_unsafe(tokens: &mut TokenStream, item: &mut syn::ForeignItemFn) { + item.sig.unsafety = None; + item.to_tokens(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..e3caf390d --- /dev/null +++ b/crates/objc2-proc-macros/src/enumeration.rs @@ -0,0 +1,1236 @@ +use crate::common::KeyVal; +use heck::{ToShoutySnakeCase, ToSnakeCase}; +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens, TokenStreamExt}; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, +}; + +pub(crate) struct EnumArgs { + repr: KeyVal, +} + +impl From for MacroArgs { + fn from(args: EnumArgs) -> Self { + Self::Enum { + closed_enum_token: Default::default(), + comma_token: Default::default(), + args, + } + } +} + +impl Parse for EnumArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + repr: input.parse()?, + }) + } +} + +pub(crate) struct OpenEnumArgs { + repr: KeyVal, +} + +impl From for MacroArgs { + fn from(args: OpenEnumArgs) -> Self { + Self::OpenEnum { + enum_token: Default::default(), + comma_token: Default::default(), + args, + } + } +} + +impl Parse for OpenEnumArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + repr: input.parse()?, + }) + } +} + +pub(crate) struct TypedEnumArgs { + r#type: KeyVal, +} + +impl From for MacroArgs { + fn from(args: TypedEnumArgs) -> Self { + Self::TypedEnum { + typed_enum_token: Default::default(), + comma_token: Default::default(), + args, + } + } +} + +impl Parse for TypedEnumArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + r#type: input.parse()?, + }) + } +} + +pub(crate) struct OpenTypedEnumArgs { + r#type: KeyVal, +} + +impl From for MacroArgs { + fn from(args: OpenTypedEnumArgs) -> Self { + Self::OpenTypedEnum { + typed_extensible_enum_token: Default::default(), + comma_token: Default::default(), + args, + } + } +} + +impl Parse for OpenTypedEnumArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + r#type: input.parse()?, + }) + } +} + +pub(crate) struct ErrorEnumArgs { + domain: KeyVal, + user_info: UserInfo, +} + +impl From for MacroArgs { + fn from(args: ErrorEnumArgs) -> Self { + Self::ErrorEnum { + error_enum_token: Default::default(), + comma_token: Default::default(), + args, + } + } +} + +impl Parse for ErrorEnumArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + domain: input.parse()?, + user_info: input.parse()?, + }) + } +} + +pub(crate) struct OptionsArgs { + repr: KeyVal, +} + +impl From for MacroArgs { + fn from(args: OptionsArgs) -> Self { + Self::Options { + options_token: Default::default(), + comma_token: Default::default(), + args, + } + } +} + +impl Parse for OptionsArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + repr: input.parse()?, + }) + } +} + +/// Representation of the sub-macro arguments. +pub(crate) enum MacroArgs { + /// Corresponds to `ns_closed_enum`. + Enum { + #[allow(unused)] + closed_enum_token: tokens::closed_enum, + #[allow(unused)] + comma_token: syn::Token![,], + args: EnumArgs, + }, + /// Corresponds to `ns_enum`. + OpenEnum { + #[allow(unused)] + enum_token: syn::Token![enum], + #[allow(unused)] + comma_token: syn::Token![,], + args: OpenEnumArgs, + }, + + /// Corresponds to `ns_typed_enum`. + TypedEnum { + #[allow(unused)] + typed_enum_token: tokens::typed_enum, + #[allow(unused)] + comma_token: syn::Token![,], + args: TypedEnumArgs, + }, + /// Corresponds to `ns_typed_extensible_enum`. + OpenTypedEnum { + #[allow(unused)] + typed_extensible_enum_token: tokens::typed_extensible_enum, + #[allow(unused)] + comma_token: syn::Token![,], + args: OpenTypedEnumArgs, + }, + /// Corresponds to `ns_error_enum`. + ErrorEnum { + #[allow(unused)] + error_enum_token: tokens::error_enum, + #[allow(unused)] + comma_token: syn::Token![,], + args: ErrorEnumArgs, + }, + /// Corresponds to `ns_options`. + Options { + #[allow(unused)] + options_token: tokens::options, + #[allow(unused)] + comma_token: syn::Token![,], + args: OptionsArgs, + }, +} + +impl Parse for MacroArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + let lookahead = input.lookahead1(); + + if lookahead.peek(tokens::closed_enum) { + return Ok(MacroArgs::Enum { + closed_enum_token: input.parse()?, + comma_token: input.parse()?, + args: input.parse()?, + }); + } + if lookahead.peek(syn::Token![enum]) { + return Ok(MacroArgs::OpenEnum { + enum_token: input.parse()?, + comma_token: input.parse()?, + args: input.parse()?, + }); + } + if lookahead.peek(tokens::error_enum) { + return Ok(MacroArgs::ErrorEnum { + error_enum_token: input.parse()?, + comma_token: input.parse()?, + args: input.parse()?, + }); + } + if lookahead.peek(tokens::options) { + return Ok(MacroArgs::Options { + options_token: input.parse()?, + comma_token: input.parse()?, + args: input.parse()?, + }); + } + if lookahead.peek(tokens::typed_enum) { + return Ok(MacroArgs::TypedEnum { + typed_enum_token: input.parse()?, + comma_token: input.parse()?, + args: input.parse()?, + }); + } + if lookahead.peek(tokens::typed_extensible_enum) { + return Ok(MacroArgs::OpenTypedEnum { + typed_extensible_enum_token: input.parse()?, + comma_token: input.parse()?, + args: input.parse()?, + }); + } + + let mut error = syn::Error::new( + input.span(), + "#[objc]: must specify type of enum as first argument", + ); + error.combine(lookahead.error()); + Err(error) + } +} + +/// Helper module for sub-macro specific definitions. +pub(crate) mod tokens { + 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); +} + +// Process the `item_enum` according to the macro args from `attr`. +pub(crate) fn item_enum(args: MacroArgs, item_enum: syn::ItemEnum) -> syn::Result { + use Extensibility::*; + + // Disallow generics + if !item_enum.generics.params.is_empty() { + return Err(syn::Error::new_spanned( + item_enum, + "#[objc]: enums with generics are not supported", + )); + } + + // Disallow where-clauses + if item_enum.generics.where_clause.is_some() { + return Err(syn::Error::new_spanned( + item_enum.generics.where_clause, + "#[objc]: enums with `where` clauses are not supported", + )); + } + + // Disallow variants with fields (named or record-like) + for variant in &item_enum.variants { + if !matches!(variant.fields, syn::Fields::Unit) { + return Err(syn::Error::new_spanned( + &variant.fields, + "#[objc]: variants with fields are not supported", + )); + } + } + + // Dispatch the enum generation logic according to which mode was specified + match args { + MacroArgs::Enum { args, .. } => ns_closed_enum(item_enum, args.repr.val.0), + MacroArgs::OpenEnum { args, .. } => ns_enum(item_enum, args.repr.val.0), + MacroArgs::TypedEnum { args, .. } => { + ns_typed_enum(item_enum, args.r#type.val, Closed.into()) + } + MacroArgs::OpenTypedEnum { args, .. } => { + ns_typed_enum(item_enum, args.r#type.val, Open.into()) + } + MacroArgs::ErrorEnum { args, .. } => { + ns_error_enum(item_enum, args.domain.val, args.user_info) + } + MacroArgs::Options { args, .. } => ns_options(item_enum, args.repr.val.0), + } +} + +/// The extensibility of the generated enumeration (e.g., `ns_typed_enum` vs `ns_typed_extensible_enum`). +#[derive(Default)] +enum Extensibility { + Closed, + #[default] + 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]), + }) + } +} + +/// Helper type for parsing and checking for valid repr types. +pub(crate) struct Repr(syn::Ident); + +pub(crate) static REPRS: &[&str] = &[ + "usize", "u8", "u16", "u32", "u64", "isize", "i8", "i16", "i32", "i64", +]; + +/// Check whether an identifier is a valid repr type. +#[inline] +fn ident_is_numeric(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_numeric(&ident) { + Ok(Self(ident)) + } else { + Err(syn::Error::new_spanned( + ident, + format!("#[objc]: `repr` must be one of: {:#?}", REPRS), + )) + } + } +} + +/// Process the `item_enum` as an `ns_option` enumeration. +fn ns_options(mut item_enum: syn::ItemEnum, repr: syn::Ident) -> syn::Result { + generate_and_rename(&mut item_enum)?; + + let syn::ItemEnum { + attrs, + vis, + ident: enum_ident, + variants, + .. + } = item_enum; + + let mut options = TokenStream::new(); + + // Process the individual enum variants. + for variant in &variants { + let attrs = &variant.attrs; + let variant_ident = format_ident!("{}", variant.ident.to_string().to_shouty_snake_case()); + + // Emit constants for the individual enum variants. + if let Some((_, expr)) = &variant.discriminant { + options.append_all(quote!( + #(#attrs)* + const #variant_ident = #expr; + )); + } else { + return Err(syn::Error::new_spanned( + variant, + "#[objc]: all options variants must have explicit values (` = `)", + )); + } + } + + // Emit the encoding impls. + let impl_encoding = quote!( + unsafe impl ::objc2::Encode for #enum_ident { + const ENCODING: ::objc2::Encoding = <#repr>::ENCODING; + } + unsafe impl ::objc2::RefEncode for #enum_ident { + const ENCODING_REF: ::objc2::Encoding = <#repr>::ENCODING_REF; + } + ); + + // Emit the final result. + Ok(quote!( + ::objc2::__private::bitflags::bitflags! { + #(#attrs)* + #[repr(transparent)] + #[derive(::objc2::__private::Default)] + #vis struct #enum_ident : #repr { + #options + } + } + #impl_encoding + )) +} + +/// Process the `item_enum` as an `ns_enum`. +fn ns_enum(item_enum: syn::ItemEnum, repr: syn::Ident) -> syn::Result { + let repr = syn::parse2(quote!(#repr))?; + let config = TypedEnumConfig::default(); + ns_typed_enum(item_enum, repr, config) +} + +/// Process the `item_enum` as an `ns_closed_enum` enumeration. +fn ns_closed_enum(mut item_enum: syn::ItemEnum, repr: syn::Ident) -> syn::Result { + generate_and_rename(&mut item_enum)?; + + let enum_ident = &item_enum.ident; + + // Emit the encoding impls. + let impl_encoding = quote!( + unsafe impl ::objc2::Encode for #enum_ident { + const ENCODING: ::objc2::Encoding = <#repr>::ENCODING; + } + unsafe impl ::objc2::RefEncode for #enum_ident { + const ENCODING_REF: ::objc2::Encoding = <#repr>::ENCODING_REF; + } + ); + + // Emit the final result. + Ok(quote!( + #[derive(::objc2::__private::num_enum::IntoPrimitive)] + #[repr(#repr)] + #item_enum + #impl_encoding + )) +} + +/// Helper type for additional configuration when generating typed enumerations. In particular, this +/// is used by `ns_error_enum` for generating a typed enumeration for the error codes, where the +/// associated struct is generated within a submodule. +#[derive(Default)] +struct TypedEnumConfig { + extensibility: Extensibility, + extra_items: TokenStream, +} + +impl From for TypedEnumConfig { + fn from(extensibility: Extensibility) -> Self { + let extra_items = TokenStream::new(); + Self { + extensibility, + extra_items, + } + } +} + +/// Process the `item_enum` as an `ns_typed_enum` or `ns_typed_extensible_enum` enumeration. +fn ns_typed_enum( + mut item_enum: syn::ItemEnum, + repr: syn::Type, + config: TypedEnumConfig, +) -> syn::Result { + generate_and_rename(&mut item_enum)?; + + let syn::ItemEnum { + attrs, + vis, + ident: struct_ident, + variants, + .. + } = item_enum; + let TypedEnumConfig { + extensibility, + extra_items, + } = &config; + + // Check whether the underlying type is a valid repr type. + let mut is_numeric = false; + if let syn::Type::Path(tp) = &repr { + if let Some(ident) = tp.path.get_ident() { + is_numeric = REPRS.contains(&ident.to_string().as_str()); + } + } + + // The identifier for the `Cases` helper type. + let cases_ident = format_ident!("{}Cases", struct_ident); + + // The type for mapping into `Cases` (i.e., whether to wrap the return in `Option` if the enum + // is extensible), along with the default case accordingly. + let (matches_into_cases_type, matches_into_cases_default) = match extensibility { + Extensibility::Closed => ( + quote!(#cases_ident), + quote!(_wrapper => ::objc2::__private::unreachable!("unexpected case in (closed) typed enum"),), + ), + Extensibility::Open => ( + quote!(::objc2::__private::Option<#cases_ident>), + quote!(_ => ::objc2::__private::None,), + ), + }; + let matches_try_from_repr_default = + quote!(_ => ::objc2::__private::Err("Value does not match any of the enum values")); + + let mut struct_constants = TokenStream::new(); + let mut enum_variants = TokenStream::new(); + let mut matches_into_cases = TokenStream::new(); + let mut matches_from_cases = TokenStream::new(); + let mut matches_try_from_repr = TokenStream::new(); + + let match_discriminant = if is_numeric { + quote!(self) + } else { + quote!(self.peek()) + }; + + let (struct_vis, struct_field_vis) = (quote!(#vis), quote!()); + + // Process the individual enum variants. + for mut variant in variants { + if is_numeric { + let doc_alias_attr = get_doc_alias(&variant); + let cases_variant_ident = &variant.ident; + let struct_variant_ident = + format_ident!("r#{}", variant.ident.to_string().to_shouty_snake_case()); + + // Emit the => `match` arm. + matches_into_cases.append_all(quote!( + &Self::#struct_variant_ident => #cases_ident::#cases_variant_ident.into(), + )); + // Emit the => `match` arm. + matches_from_cases.append_all(quote!( + #cases_ident::#cases_variant_ident => #struct_ident::#struct_variant_ident, + )); + matches_try_from_repr.append_all(quote!( + value @ Self::#struct_variant_ident => ::objc2::__private::Ok(value), + )); + // Emit constants for the individual enum variants. + struct_constants.append_all(quote!( + #doc_alias_attr + pub const #struct_variant_ident: Self = Self(#cases_ident::#cases_variant_ident as #repr); + )); + } else if let Some((_, expr)) = variant.discriminant.take() { + // NOTE: we `take` the discriminant above so it is removed in the output tokens + let doc_alias_attr = get_doc_alias(&variant); + let cases_variant_ident = &variant.ident; + let struct_variant_ident = + format_ident!("r#{}", variant.ident.to_string().to_snake_case()); + + // Emit the => `match` arm. + matches_into_cases.append_all(quote!( + &value if value == Self::#struct_variant_ident().take() => #cases_ident::#cases_variant_ident.into(), + )); + // Emit the => `match` arm. + matches_from_cases.append_all(quote!( + #cases_ident::#cases_variant_ident => #struct_ident::#struct_variant_ident(), + )); + matches_try_from_repr.append_all(quote!( + value if value == Self::#struct_variant_ident().take() => ::objc2::__private::Ok(Self(value)), + )); + // Emit constants for the individual enum variants. + struct_constants.append_all(quote!( + #doc_alias_attr + pub fn #struct_variant_ident() -> Self { + Self(#expr) + } + )); + } else { + return Err(syn::Error::new_spanned( + variant, + "#[objc]: all non-repr typed enum variants must have values (` = `)", + )); + } + + // Emit the enum variant. + enum_variants.append_all(quote!( + #variant, + )); + } + + let impl_encoding = quote!( + unsafe impl ::objc2::Encode for #struct_ident { + const ENCODING: ::objc2::Encoding = <#repr>::ENCODING; + } + unsafe impl ::objc2::RefEncode for #struct_ident { + const ENCODING_REF: ::objc2::Encoding = <#repr>::ENCODING_REF; + } + ); + let mut impl_into_nsnumber = TokenStream::new(); + let mut impl_deref_as_nsobject = TokenStream::new(); + let mut impl_try_from_repr = TokenStream::new(); + let mut impl_try_from_nsnumber = TokenStream::new(); + + // If the underlying type is a valid repr type, generate additional helper code. + if let syn::Type::Path(tp) = &repr { + if let Some(repr) = tp.path.get_ident() { + if ident_is_numeric(repr) { + let constructor = format_ident!("new_{}", repr); + let destructor = format_ident!("as_{}", repr); + + // Emit the `From: Id` impl. + impl_into_nsnumber.append_all(quote!( + #[cfg(all(feature = "Foundation", feature = "Foundation_NSNumber"))] + impl ::objc2::__private::From<#struct_ident> for ::objc2::rc::Id { + #[inline] + fn from(value: #struct_ident) -> Self { + icrate::Foundation::NSNumber::#constructor(value.0.into()) + } + } + )); + + // Emit the `Deref` impl. + impl_deref_as_nsobject.append_all(quote!( + // NOTE: used for automatic conversion for insersion into `userInfo` dictionaries + #[cfg(feature = "Foundation")] + impl ::objc2::__private::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 + } + } + )); + + // Emit the `TryFrom>` impl. + impl_try_from_nsnumber.append_all({ + let repr_string = repr.to_string(); + quote!( + #[cfg(all(feature = "Foundation", feature = "Foundation_NSNumber"))] + impl ::objc2::__private::TryFrom<&icrate::Foundation::NSNumber> for #struct_ident { + type Error = &'static ::objc2::__private::str; + fn try_from(number: &icrate::Foundation::NSNumber) -> ::objc2::__private::Result { + use ::objc2::Encoding::*; + let encoding = number.encoding(); + let equivalent = match #repr_string { + "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 { + number.#destructor().try_into() + } else { + ::objc2::__private::Err("NSNumber encoding is not equivalent to error repr") + } + } + } + #[cfg(all(feature = "Foundation", feature = "Foundation_NSNumber"))] + impl ::objc2::__private::TryFrom<::objc2::rc::Id> for #struct_ident { + type Error = &'static str; + #[inline] + fn try_from(number: ::objc2::rc::Id) -> ::objc2::__private::Result { + (&*number).try_into() + } + } + ) + }); + } + } + } + + impl_try_from_repr.append_all({ + let match_discriminant = if is_numeric { + quote!(Self(raw)) + } else { + quote!(raw) + }; + quote!( + impl ::objc2::__private::TryFrom<#repr> for #struct_ident { + type Error = &'static ::objc2::__private::str; + fn try_from(raw: #repr) -> ::objc2::__private::Result { + match #match_discriminant { + #matches_try_from_repr + #matches_try_from_repr_default + } + } + } + ) + }); + + let derives_for_repr = if is_numeric { + quote!(#[derive(::objc2::__private::Copy, ::objc2::__private::Clone, ::objc2::__private::Ord, ::objc2::__private::PartialOrd, ::objc2::__private::Hash)]) + } else { + quote!(#[derive(::objc2::__private::Hash)]) + }; + + // Emit the wrapper struct and related impls. + let struct_and_impls = { + let struct_ident_string = struct_ident.to_string(); + let debug_fmt_body = if matches!(config.extensibility, Extensibility::Open) { + quote!( + if let ::objc2::__private::Some(cases) = self.cases() { + ::objc2::__private::Debug::fmt(&cases, f) + } else { + f.debug_tuple(#struct_ident_string).field(&self.0).finish() + } + ) + } else { + quote!(::objc2::__private::Debug::fmt(&self.cases(), f)) + }; + quote!( + #(#attrs)* + #derives_for_repr + #[derive(::objc2::__private::Eq, ::objc2::__private::PartialEq)] + #[repr(transparent)] + #struct_vis struct #struct_ident(#struct_field_vis #repr); + #impl_encoding + impl #struct_ident { + #struct_constants + pub fn cases(&self) -> #matches_into_cases_type { + match #match_discriminant { + #matches_into_cases + #matches_into_cases_default + } + } + #[inline] + pub fn peek(&self) -> &#repr { + &self.0 + } + #[inline] + pub fn take(self) -> #repr { + self.0 + } + #[inline] + pub fn as_raw(self) -> #repr { + self.take() + } + #[inline] + pub fn from_raw(raw: #repr) -> Option { + raw.try_into().ok() + } + #[inline] + pub fn from_raw_unchecked(raw: #repr) -> Self { + Self(raw) + } + } + impl core::fmt::Debug for #struct_ident { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + #debug_fmt_body + } + } + impl core::convert::From<#struct_ident> for #repr { + #[inline] + fn from(wrapper: #struct_ident) -> Self { + wrapper.take() + } + } + #impl_try_from_repr + #impl_into_nsnumber + #impl_try_from_nsnumber + #impl_deref_as_nsobject + impl ::objc2::TypedEnum for #struct_ident { + type Cases = #cases_ident; + } + ) + }; + + // Emit the `Cases` helper enum. + let cases_enum = quote!( + #[derive(::objc2::__private::Copy, ::objc2::__private::Clone, ::objc2::__private::Debug, ::objc2::__private::Eq, ::objc2::__private::PartialEq, ::objc2::__private::Ord, ::objc2::__private::PartialOrd, ::objc2::__private::Hash)] + #extensibility + pub enum #cases_ident { + #enum_variants + } + ); + + Ok(quote!( + #struct_and_impls + #cases_enum + #extra_items + )) +} + +/// Representation of a UserInfo field specification. +struct UserInfoField { + attrs: Vec, + #[allow(unused)] + brace_token: syn::token::Brace, + key: KeyVal, + r#type: KeyVal, + name: Option>, + from: Option>, + into: Option>, + #[allow(unused)] + comma_token: Option, +} + +impl Parse for UserInfoField { + fn parse(input: ParseStream<'_>) -> syn::Result { + let attrs = input.call(syn::Attribute::parse_outer)?; + let content; + let brace_token = syn::braced!(content in input); + let key = content.parse()?; + let r#type = content.parse()?; + + let mut name = None; + let mut from = None; + let mut into = None; + let mut comma_token = None; + + // The arguments `name`, `from`, and `into` are optional but expected in order so we + // structure the parsing this way (using `lookahead`) for better error messages. + for _ in ["name", "from", "into"] { + let mut lookahead; + if content.peek(syn::Token![,]) { + let punctuation = content.parse()?; + lookahead = content.lookahead1(); + if content.is_empty() { + comma_token = Some(punctuation); + break; + } else if into.is_none() + && from.is_none() + && name.is_none() + && lookahead.peek(tokens::name) + { + name = Some(KeyVal { + punctuation, + unsafety: content.parse()?, + key: content.parse()?, + separator: content.parse()?, + val: content.parse()?, + }); + } else if lookahead.peek(syn::Token![unsafe]) { + let unsafety = content.parse()?; + lookahead = content.lookahead1(); + if into.is_none() && from.is_none() && lookahead.peek(tokens::from) { + from = Some(KeyVal { + punctuation, + unsafety, + key: content.parse()?, + separator: content.parse()?, + val: content.parse()?, + }); + } else if into.is_none() && lookahead.peek(tokens::into) { + into = Some(KeyVal { + punctuation, + unsafety, + key: content.parse()?, + separator: content.parse()?, + val: content.parse()?, + }); + comma_token = content.parse()?; + break; + } else { + return Err(lookahead.error()); + } + } else { + return Err(lookahead.error()); + } + } + } + + Ok(UserInfoField { + attrs, + brace_token, + key, + r#type, + name, + from, + into, + comma_token, + }) + } +} + +/// Representation of the UserInfo specification. +pub(crate) 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, + }) + } +} + +/// Representation of the UserInfo field argument. +pub(crate) struct UserInfo(Option>); + +impl Parse for UserInfo { + fn parse(input: ParseStream<'_>) -> syn::Result { + if input.peek(syn::Token![,]) && input.peek2(tokens::user_info) { + Ok(Self(Some(input.parse()?))) + } else { + Ok(Self(None)) + } + } +} + +/// Process the `item_enum` as an `ns_error_enum`. +fn ns_error_enum( + mut item_enum: syn::ItemEnum, + domain: syn::Expr, + user_info: UserInfo, +) -> syn::Result { + generate_and_rename(&mut item_enum)?; + + 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 codes_ident = format_ident!("r#{}Codes", error_ident); + let cases_ident = format_ident!("r#{}Cases", codes_ident); + let user_info_ident = format_ident!("r#{}UserInfo", error_ident); + + 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 extra_items = TokenStream::new(); + + if let UserInfo(Some(KeyVal { + val: UserInfoSpec { fields, .. }, + .. + })) = user_info + { + // Process the individual UserInfo specification fields. + for UserInfoField { + attrs, + key: KeyVal { val: key, .. }, + r#type: KeyVal { val: ty, .. }, + name, + from, + into, + .. + } in fields.iter() + { + // Generate the ident for `UserInfo` field. + let ident = name.as_ref().map(|kv| kv.val.clone()).unwrap_or_else(|| { + let name = &key.to_string(); + // Strip the common error name from the prefix. + let name = name.strip_prefix(error_ident_string).unwrap_or(name); + // Strip "Key" from the suffix. + let name = name.strip_suffix("Key").unwrap_or(name); + // Strip "Error" from the suffix. + let name = name.strip_suffix("Error").unwrap_or(name); + // Convert to snake_case. + let name = name.to_snake_case(); + // Convert to an identifier. + format_ident!("r#{}", name) + }); + + // Emit the `into` coercion helper. + let into = if let Some(KeyVal { val: into, .. }) = into { + quote!((#into)) + } else { + quote!() + }; + + // Emit the `UserInfo` field. + user_info_field_decls.append_all(quote!( + pub #ident: ::objc2::__private::Option<#ty>, + )); + + // Emit the `UserInfo` dictionary insertion logic for the field. + user_info_field_intos.append_all(quote!( + #[allow(unused_unsafe)] + if let ::objc2::__private::Some(value) = #into(user_info.#ident) { + unsafe { dict.setValue_forKey(::objc2::__private::Some(&*value), #key) }; + } + )); + + // Emit the `UserInfo` struct construction logic for the field. + user_info_field_froms.append_all(quote!( + user_info.#ident = self.#ident(); + )); + + // Emit the `from` coercion helper. + let from = if let Some(KeyVal { val: from, .. }) = from { + quote!((#from)) + } else { + quote!( + (|n: ::objc2::__private::Option<_>| n + .map(::objc2::__private::TryInto::try_into) + .and_then(::objc2::__private::Result::ok)) + ) + }; + + // Emit the `UserInfo` getter method. + user_info_getters.append_all(quote!( + #(#attrs)* + fn #ident(&self) -> ::objc2::__private::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 and define the newtype as an `NSError` subclass. + let error_class_declaration = 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 ::objc2::__private::str = #error_ident_string; + } + ); + ); + + // Emit the newtype methods. + let error_class_methods = quote!( + #[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) -> #codes_ident { + #codes_ident(self.as_super().code()) + } + #[inline] + pub fn localized_description(&self) -> ::objc2::rc::Id { + self.as_super().localizedDescription() + } + #[inline] + pub fn user_info(&self) -> #user_info_ident { + let mut user_info = #user_info_ident::default(); + #user_info_field_froms + user_info + } + #[inline] + pub fn new(code: #codes_ident) -> ::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) } + } + #[inline] + pub fn downcast(error: ::objc2::rc::Id) -> ::objc2::__private::Option<::objc2::rc::Id> { + if &*error.domain() == Self::domain() { + let error = unsafe { ::objc2::rc::Id::cast(error) }; + core::option::Option::Some(error) + } else { + core::option::Option::None + } + } + #[cfg(all(feature = "Foundation_NSDictionary", feature = "Foundation_NSMutableDictionary"))] + #[inline] + fn new_with_user_info( + code: #codes_ident, + user_info: #user_info_ident, + ) -> ::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: #user_info_ident, + ) -> ::objc2::rc::Id> { + let dict = icrate::Foundation::NSMutableDictionary::::new(); + #user_info_field_intos + ::objc2::rc::Id::into_shared(dict) + } + #user_info_getters + } + ); + + let error_helper_impls = quote!( + #[cfg(all(feature = "Foundation", feature = "Foundation_NSError", feature = "Foundation_NSString"))] + impl ::objc2::ErrorEnum for #error_ident { + type Codes = #codes_ident; + type UserInfo = #user_info_ident; + } + #[cfg(all(feature = "Foundation", feature = "Foundation_NSError", feature = "Foundation_NSString"))] + impl ::objc2::TypedEnum for #error_ident { + type Cases = #cases_ident; + } + ); + + // Emit addition helper module items for `ns_typed_enum` to place during code generation. + // Currently this consists of the `UserInfo` struct and relevant imports. + extra_items.append_all(quote!( + #[derive(::objc2::__private::Default)] + pub struct #user_info_ident { + #user_info_field_decls + } + )); + + // Call `ns_typed_enum` and emit the `Codes` and `Cases` machinery in the helper submodule. + let error_helper_module = { + item_enum.ident = codes_ident; + let ty = syn::parse_str::("isize")?; + let config = TypedEnumConfig { + extensibility: Extensibility::Open, + extra_items, + }; + ns_typed_enum(item_enum, ty, config)? + }; + + // Emit the final result. + Ok(quote!( + #error_class_declaration + #error_class_methods + #error_helper_impls + #error_helper_module + )) +} + +/// Process and rename the `item_enum` ident and variant idents according to naming conventions. +fn generate_and_rename(item_enum: &mut syn::ItemEnum) -> syn::Result<()> { + let mut variants = if item_enum.ident != "__anonymous__" { + // If name is not `__anonymous__`, include among the idents checked for (pre|suf)fixes. + Box::new(std::iter::once(&item_enum.ident)) as Box> + } else { + // Otherwise leave name out of the idents checked for (pre|suf)fixes. + Box::new(std::iter::empty()) as Box> + } + .chain(item_enum.variants.iter().map(|v| &v.ident)); + + if let Some(var) = variants.next().map(|ident| ident.to_string()) { + let mut prefix = var.as_str(); + let mut suffix = var.as_str(); + + for ident in variants { + // Find the longest common prefix among variant names. + for (i, (lhs, rhs)) in prefix.chars().zip(ident.to_string().chars()).enumerate() { + if lhs != rhs { + prefix = prefix.split_at(i).0; + break; + } + } + // Find the longest common suffix among variant names. + for (i, (lhs, rhs)) in suffix + .chars() + .rev() + .zip(ident.to_string().chars().rev()) + .enumerate() + { + if lhs != rhs { + suffix = suffix.split_at(suffix.len() - i).1; + break; + } + } + } + + // If the enum name was `__anonymous__`, use the common prefix as a name instead. + if item_enum.ident == "__anonymous__" { + if !prefix.is_empty() { + item_enum.ident = format_ident!("{}", prefix); + } else { + return Err(syn::Error::new_spanned( + &item_enum.ident, + "#[objc]: could not find a common variant prefix to generate a name from", + )); + } + } + + // Strip the common (pre|suf)fix from the variant names. + for var in item_enum.variants.iter_mut() { + let ident = var.ident.to_string(); + let candidate = &ident; + let candidate = candidate.strip_prefix(prefix).unwrap_or(candidate); + let candidate = candidate.strip_suffix(suffix).unwrap_or(candidate); + if candidate != ident { + var.attrs.push(syn::parse_quote!(#[doc(alias = #ident)])); + var.ident = format_ident!("{}", candidate); + } + } + } + Ok(()) +} + +#[inline] +fn find_doc_alias(variant: &syn::Variant) -> Option { + for (i, attr) in variant.attrs.iter().enumerate() { + if matches!(attr.style, syn::AttrStyle::Outer) && attr.path.is_ident("doc") { + return Some(i); + } + } + None +} + +#[inline] +fn get_doc_alias(variant: &syn::Variant) -> Option<&syn::Attribute> { + find_doc_alias(variant).and_then(|i| variant.attrs.get(i)) +} + +#[allow(unused)] +#[inline] +fn remove_doc_alias(variant: &mut syn::Variant) -> Option { + find_doc_alias(variant).map(|i| variant.attrs.remove(i)) +} diff --git a/crates/objc2-proc-macros/src/implementation.rs b/crates/objc2-proc-macros/src/implementation.rs new file mode 100644 index 000000000..cfd2c4a13 --- /dev/null +++ b/crates/objc2-proc-macros/src/implementation.rs @@ -0,0 +1,51 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::parse::{Parse, ParseStream}; + +pub(crate) struct MacroArgsRest; + +impl From for MacroArgs { + fn from(rest: MacroArgsRest) -> Self { + Self { + implementation_token: Default::default(), + rest, + } + } +} + +impl Parse for MacroArgsRest { + fn parse(_input: ParseStream<'_>) -> syn::Result { + Ok(Self) + } +} + +pub(crate) struct MacroArgs { + #[allow(unused)] + implementation_token: self::tokens::implementation, + #[allow(unused)] + rest: MacroArgsRest, +} + +impl Parse for MacroArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + implementation_token: input.parse()?, + rest: input.parse()?, + }) + } +} + +pub(crate) mod tokens { + syn::custom_keyword!(implementation); +} + +pub(crate) fn item_impl( + _args: MacroArgs, + mut item_impl: syn::ItemImpl, +) -> syn::Result { + // 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..145362912 --- /dev/null +++ b/crates/objc2-proc-macros/src/interface.rs @@ -0,0 +1,368 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, +}; + +use crate::{ + common::{EmptyToken, KeyVal}, + CfgAttributes, +}; + +pub(crate) mod external; +pub(crate) mod internal; + +pub(crate) struct ClassStruct { + attrs_all: TokenStream, + attrs_cfg: TokenStream, + vis: syn::Visibility, + ident: syn::Ident, + generics: TokenStream, + generics_split: (TokenStream, TokenStream, TokenStream), + fields: syn::Fields, +} + +impl From for ClassStruct { + fn from(item: syn::ItemStruct) -> Self { + let syn::ItemStruct { + attrs, + vis, + ident, + generics, + fields, + .. + } = item; + let attrs_cfg = CfgAttributes::from(&*attrs); + let generics_split = { + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + ( + quote!(#impl_generics), + quote!(#ty_generics), + quote!(#where_clause), + ) + }; + let generics = quote!(generics); + Self { + attrs_all: quote!(#(#attrs)*), + attrs_cfg: quote!(#(#attrs_cfg)*), + vis, + ident, + generics, + generics_split, + fields, + } + } +} + +impl From for ClassStruct { + fn from(item: syn::ForeignItemType) -> Self { + let syn::ForeignItemType { + attrs, vis, ident, .. + } = item; + let attrs_cfg = CfgAttributes::from(&*attrs); + Self { + attrs_all: quote!(#(#attrs)*), + attrs_cfg: quote!(#(#attrs_cfg)*), + vis, + ident, + generics: Default::default(), + generics_split: Default::default(), + fields: syn::Fields::Unit, + } + } +} + +pub(crate) struct Class { + attrs: TokenStream, + class: syn::Type, +} + +impl Parse for Class { + fn parse(input: ParseStream<'_>) -> syn::Result { + let attrs = input.call(syn::Attribute::parse_outer)?; + Ok(Self { + attrs: quote!(#(#attrs)*), + class: input.parse()?, + }) + } +} + +pub(crate) struct Inherits( + Option>, +); + +impl Parse for Inherits { + fn parse(input: ParseStream<'_>) -> syn::Result { + if input.peek(syn::Token![,]) { + Ok(Self(Some(input.parse()?))) + } else { + Ok(Self(None)) + } + } +} + +struct InheritsSpec { + #[allow(unused)] + bracket_token: syn::token::Bracket, + classes: Punctuated, +} + +impl Parse for InheritsSpec { + fn parse(input: ParseStream<'_>) -> syn::Result { + let content; + let bracket_token = syn::bracketed!(content in input); + let classes = content.parse_terminated(Class::parse)?; + Ok(Self { + bracket_token, + classes, + }) + } +} + +pub(crate) enum MacroArgsRest { + Intern(self::internal::MacroArgs), + Extern(self::external::MacroArgs), +} + +impl From for MacroArgs { + fn from(rest: MacroArgsRest) -> Self { + Self { + interface_token: Default::default(), + rest, + } + } +} + +impl Parse for MacroArgsRest { + fn parse(input: ParseStream<'_>) -> syn::Result { + let punctuation = input.parse()?; + if input.peek(syn::Token![unsafe]) { + let super_ = KeyVal { + punctuation, + unsafety: input.parse()?, + key: input.parse()?, + separator: input.parse()?, + val: input.parse()?, + }; + Ok(self::external::MacroArgs { + super_, + inherits: input.parse()?, + } + .into()) + } else { + let super_ = KeyVal { + punctuation, + unsafety: Default::default(), + key: input.parse()?, + separator: input.parse()?, + val: input.parse()?, + }; + Ok(self::internal::MacroArgs { + super_class: super_, + inherits: input.parse()?, + } + .into()) + } + } +} + +pub(crate) struct MacroArgs { + #[allow(unused)] + pub(crate) interface_token: self::tokens::interface, + #[allow(unused)] + pub(crate) rest: MacroArgsRest, +} + +impl Parse for MacroArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + interface_token: input.parse()?, + rest: input.parse()?, + }) + } +} + +pub(crate) mod tokens { + syn::custom_keyword!(interface); + syn::custom_keyword!(inherits); +} + +struct ClassEmitter { + this: ClassStruct, + super_: Class, + inherits: Option>, + methods: TokenStream, +} + +impl ClassEmitter { + fn class_this(&self, tokens: &mut TokenStream) { + let attrs_this_all = &self.this.attrs_all; + let attrs_super = &self.super_.attrs; + + let class_this = &self.this.ident; + let class_this_name = class_this.to_string(); + let class_super = &self.super_.class; + + let vis = &self.this.vis; + let fields = self.this.fields.iter(); + + let generics_this = &self.this.generics; + let (impl_generics, ty_generics, where_clause) = &self.this.generics_split; + + tokens.append_all(quote!( + #attrs_this_all + #attrs_super + #vis struct #class_this #generics_this { + #(#fields,)* + __objc2_super: #class_super, + } + unsafe impl #impl_generics ::objc2::ClassType for #class_this #ty_generics #where_clause { + type Super = #class_super; + const NAME: &'static ::objc2::__private::str = #class_this_name; + + #[inline] + fn class() -> &'static ::objc2::runtime::Class { + ::objc2::__class_inner!( + #class_this_name, + ::objc2::__hash_idents!(#class_this), + ) + } + + #[inline] + fn as_super(&self) -> &Self::Super { + &self.__objc2_super + } + + #[inline] + fn as_super_mut(&mut self) -> &mut Self::Super { + &mut self.__objc2_super + } + } + )); + } + + fn impls_this(&self, tokens: &mut TokenStream) { + let attrs_this_cfg = &self.this.attrs_cfg; + let class_this = &self.this.ident; + let methods = &self.methods; + tokens.append_all(quote!( + #attrs_this_cfg + impl #class_this { + #![allow(non_snake_case)] + #methods + } + )); + } + + fn impls_super(&self, tokens: &mut TokenStream) { + let attrs_this_cfg = &self.this.attrs_cfg; + let attrs_super = &self.super_.attrs; + let class_this = &self.this.ident; + let class_super = &self.super_.class; + + tokens.append_all(quote!( + #attrs_this_cfg + #attrs_super + unsafe impl ::objc2::RefEncode for #class_this { + const ENCODING_REF: ::objc2::Encoding = <#class_super as ::objc2::RefEncode>::ENCODING_REF; + } + + #attrs_this_cfg + unsafe impl ::objc2::Message for #class_this {} + + #attrs_this_cfg + #attrs_super + impl ::objc2::__private::Deref for #class_this { + type Target = #class_super; + #[inline] + fn deref(&self) -> &Self::Target { + &self.__objc2_super + } + } + + #attrs_this_cfg + #attrs_super + impl ::objc2::__private::DerefMut for #class_this { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.__objc2_super + } + } + + #attrs_this_cfg + impl ::objc2::__private::AsRef for #class_this { + #[inline] + fn as_ref(&self) -> &Self { + self + } + } + + #attrs_this_cfg + impl ::objc2::__private::AsMut for #class_this { + #[inline] + fn as_mut(&mut self) -> &mut Self { + self + } + } + )); + } + + fn impls_inherits(&self, tokens: &mut TokenStream) { + let attrs_cfg = &self.this.attrs_cfg; + let class_this = &self.this.ident; + if let Some(classes) = &self.inherits { + for that in classes.iter() { + let attrs_that = &that.attrs; + let class_that = &that.class; + + tokens.append_all(quote!( + #attrs_cfg + #attrs_that + impl ::objc2::__private::AsRef<#class_that> for #class_this { + #[inline] + fn as_ref(&self) -> &#class_that { + &*self + } + } + + #attrs_cfg + #attrs_that + impl ::objc2::__private::AsMut<#class_that> for #class_this { + #[inline] + fn as_mut(&mut self) -> &mut #class_that { + &mut *self + } + } + + #attrs_cfg + #attrs_that + impl ::objc2::__private::Borrow<#class_that> for #class_this { + #[inline] + fn borrow(&self) -> &#class_that { + &*self + } + } + + #attrs_cfg + #attrs_that + impl ::objc2::__private::BorrowMut<#class_that> for #class_this { + #[inline] + fn borrow_mut(&mut self) -> &mut #class_that { + &mut *self + } + } + )); + } + } + } +} + +impl ToTokens for ClassEmitter { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.class_this(tokens); + self.impls_super(tokens); + self.impls_inherits(tokens); + self.impls_this(tokens); + } +} diff --git a/crates/objc2-proc-macros/src/interface/external.rs b/crates/objc2-proc-macros/src/interface/external.rs new file mode 100644 index 000000000..fd4a63202 --- /dev/null +++ b/crates/objc2-proc-macros/src/interface/external.rs @@ -0,0 +1,337 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + parse::{Parse, ParseStream}, + spanned::Spanned, +}; + +use crate::common::{EmptyToken, KeyVal}; + +use super::ClassEmitter; + +pub(crate) struct MacroArgs { + pub(crate) super_: + KeyVal, + pub(crate) inherits: crate::interface::Inherits, +} + +impl From for crate::interface::MacroArgsRest { + fn from(args: MacroArgs) -> Self { + Self::Extern(args) + } +} + +impl Parse for MacroArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + let unsafety = input.parse()?; + let super_ = KeyVal { + punctuation: Default::default(), + unsafety, + key: input.parse()?, + separator: input.parse()?, + val: input.parse()?, + }; + Ok(Self { + super_, + inherits: input.parse()?, + }) + } +} + +struct ExternContext { + args: MacroArgs, + item_span: proc_macro2::Span, + class: Option, + methods: TokenStream, +} + +impl ExternContext { + fn new(args: MacroArgs, item_extern: &syn::ItemForeignMod) -> Self { + let item_span = item_extern.abi.span(); + Self { + args, + item_span, + class: Default::default(), + methods: Default::default(), + } + } +} + +impl TryFrom for crate::interface::ClassEmitter { + type Error = syn::Error; + + fn try_from(ctx: ExternContext) -> Result { + if let Some(item_type) = ctx.class { + Ok(Self { + this: crate::interface::ClassStruct::from(item_type), + super_: ctx.args.super_.val, + inherits: ctx.args.inherits.0.map(|kv| kv.val.classes), + methods: ctx.methods, + }) + } else { + Err(syn::Error::new( + ctx.item_span, + "objc2: no class `type` was found in `extern` block", + )) + } + } +} + +pub(crate) fn item_interface( + args: MacroArgs, + item_extern: syn::ItemForeignMod, +) -> syn::Result { + // Ensure ABI is "Objective-C" + crate::common::check_abi(&item_extern)?; + + let mut ctx = ExternContext::new(args, &item_extern); + + for item in item_extern.items { + match item { + syn::ForeignItem::Type(item_type) => { + if let Some(original) = ctx.class { + let mut error = syn::Error::new_spanned(item_type, "objc2: only a single class `type` is allowed per `extern` for #[interface]"); + error.combine(syn::Error::new_spanned( + original, + "objc2: original class `type` declared here", + )); + return Err(error); + } else { + ctx.class = Some(item_type); + } + } + syn::ForeignItem::Fn(item) => { + if ctx.class.is_none() { + return Err(syn::Error::new_spanned( + item, + "objc2: a class `type` must be declared before its methods", + )); + } else { + item_method(&mut ctx, item)?; + } + } + _ => { + item_other(&item)?; + } + } + } + + let emitter = ClassEmitter::try_from(ctx)?; + let mut tokens = TokenStream::new(); + emitter.to_tokens(&mut tokens); + tokens.append_all({ + let class_this = emitter.this.ident; + let message = format!("objc2: the struct `{}` is not zero-sized!", class_this); + quote!( + const _: () = { + if ::objc2::__private::size_of::<#class_this>() != 0 { + ::objc2::__private::panic!(#message); + } + }; + ) + }); + + Ok(tokens) +} + +fn item_method(ctx: &mut ExternContext, mut item: syn::ForeignItemFn) -> syn::Result<()> { + let spec = extract_method_specification(&mut item)?; + let syn::ForeignItemFn { + attrs, vis, sig, .. + } = &item; + + let alias = spec.doc_alias(&item.sig); + + let mut receiver = TokenStream::new(); + let mut args = TokenStream::new(); + + let mut args_iter = item.sig.inputs.iter(); + + match args_iter.next() { + Some(syn::FnArg::Receiver(_)) => { + receiver = quote!(self); + } + Some(syn::FnArg::Typed(syn::PatType { pat, .. })) => match &**pat { + syn::Pat::Ident(pi) if pi.ident == "self" || pi.ident == "this" => { + receiver = quote!(#pat); + } + _ => { + args.append_all(quote!(#pat,)); + } + }, + None => {} + } + for arg in args_iter { + match arg { + syn::FnArg::Typed(syn::PatType { pat, .. }) => { + args.append_all(quote!(#pat,)); + } + syn::FnArg::Receiver(_) => unreachable!("already consumed receiver"), + } + } + + let selector = &spec.selector; + + let message_sender = match spec.managed { + Some(RetainSemantics::Init) => quote!( + <::objc2::__macro_helpers::Init as ::objc2::__macro_helpers::MsgSendId<_, _>>::send_message_id + ), + Some(RetainSemantics::New) => quote!( + <::objc2::__macro_helpers::New as ::objc2::__macro_helpers::MsgSendId<_, _>>::send_message_id + ), + Some(RetainSemantics::Other) => quote!( + <::objc2::__macro_helpers::Other as ::objc2::__macro_helpers::MsgSendId<_, _>>::send_message_id + ), + Some(RetainSemantics::Unspecified) => quote!( + <::objc2::__macro_helpers::RetainSemantics<{ + ::objc2::__macro_helpers::retain_semantics(#selector) + }> as ::objc2::__macro_helpers::MsgSendId<_, _>>::send_message_id + ), + None => quote!(::objc2::MessageReceiver::send_message), + }; + + if receiver.is_empty() { + receiver = quote!(::class()); + } + + ctx.methods + .append_all(quote!( + #(#attrs)* + #(#[doc(alias = #alias)])* + #vis #sig { + #message_sender( + #receiver, + { + static __OBJC2_CACHED_SEL: ::objc2::__macro_helpers::CachedSel = ::objc2::__macro_helpers::CachedSel::new(); + unsafe { + __OBJC2_CACHED_SEL.get(#selector) + } + }, + (#args) + ) + } + )); + + Ok(()) +} + +enum RetainSemantics { + Init, + New, + Other, + Unspecified, +} + +struct MethodSpecification { + selector: String, + managed: Option, +} + +impl MethodSpecification { + pub(crate) fn doc_alias(&self, sig: &syn::Signature) -> impl Iterator + '_ { + let alias = &self.selector[..self.selector.len() - 1]; + if sig.ident != alias { + Some(alias) + } else { + None + } + .into_iter() + } +} + +fn extract_method_specification(item: &mut syn::ForeignItemFn) -> syn::Result { + for i in 0..item.attrs.len() { + let attr = &item.attrs[i]; + if matches!(attr.style, syn::AttrStyle::Outer) { + if let syn::Meta::List(syn::MetaList { path, nested, .. }) = attr.parse_meta()? { + let mut segments = path.segments.iter(); + if let (Some(objc2), Some(managed)) = (segments.next(), segments.next()) { + if attr.path.leading_colon.is_none() + && objc2.ident == "objc2" + && managed.ident == "method" + && segments.next().is_none() + { + let mut metas = nested.iter(); + if let Some(syn::NestedMeta::Meta(syn::Meta::NameValue( + syn::MetaNameValue { path, lit, .. }, + ))) = metas.next() + { + if path.get_ident().map(|i| i.to_string()).as_deref() == Some("sel") { + if let syn::Lit::Str(selector) = lit { + let managed = match metas.next() { + Some(syn::NestedMeta::Meta(syn::Meta::Path(path))) + if path.is_ident("managed") => + { + Some(RetainSemantics::Unspecified) + } + Some(syn::NestedMeta::Meta(syn::Meta::NameValue( + syn::MetaNameValue { path, lit, .. }, + ))) if path.is_ident("managed") => { + let mut result = None; + if let syn::Lit::Str(str) = lit { + match str.value().as_str() { + "New" => { + result = Some(RetainSemantics::New); + } + "Init" => { + result = Some(RetainSemantics::Init); + } + "Other" => { + result = Some(RetainSemantics::Other); + } + _ => {} + } + } + if result.is_none() { + return Err(syn::Error::new_spanned( + lit, + r#"objc2: unexpected argument for `managed` (expected "New", "Init", or "Other")"#, + )); + } + result + } + Some(meta) => { + return Err(syn::Error::new_spanned( + meta, + "objc2: unexpected argument (expected `managed`)", + )); + } + None => None, + }; + item.attrs.remove(i); + return Ok(MethodSpecification { + selector: { + let mut val = selector.value(); + val.push('\0'); + val + }, + managed, + }); + } + return Err(syn::Error::new_spanned( + lit, + "objc2: expected selector name as a string literal", + )); + } + return Err(syn::Error::new_spanned( + path, + "objc2: unexpected argument (expected `sel`)", + )); + } + } + } + } + } + } + Err(syn::Error::new_spanned( + item, + "objc2: methods must have an `#[objc2::method]` attribute", + )) +} + +fn item_other(item: &syn::ForeignItem) -> syn::Result<()> { + Err(syn::Error::new( + item.span(), + "objc2: only `type` and `fn` items are allowed in `extern` for #[interface]", + )) +} diff --git a/crates/objc2-proc-macros/src/interface/internal.rs b/crates/objc2-proc-macros/src/interface/internal.rs new file mode 100644 index 000000000..186e7536c --- /dev/null +++ b/crates/objc2-proc-macros/src/interface/internal.rs @@ -0,0 +1,37 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::parse::{Parse, ParseStream}; + +use crate::common::KeyVal; + +pub(crate) struct MacroArgs { + #[allow(unused)] + pub(super) super_class: KeyVal, + #[allow(unused)] + pub(super) inherits: crate::interface::Inherits, +} + +impl From for crate::interface::MacroArgsRest { + fn from(args: MacroArgs) -> Self { + Self::Intern(args) + } +} + +impl Parse for MacroArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + super_class: input.parse()?, + inherits: input.parse()?, + }) + } +} + +pub(crate) fn item_interface(_args: MacroArgs, item_mod: syn::ItemMod) -> syn::Result { + if item_mod.ident != "__" { + return Err(syn::Error::new_spanned( + item_mod, + "objc2: interface `mod` items must be named `__`", + )); + } + Ok(item_mod.to_token_stream()) +} diff --git a/crates/objc2-proc-macros/src/lib.rs b/crates/objc2-proc-macros/src/lib.rs index 1c905f20f..3e5384311 100644 --- a/crates/objc2-proc-macros/src/lib.rs +++ b/crates/objc2-proc-macros/src/lib.rs @@ -23,6 +23,14 @@ use proc_macro::Ident; use proc_macro::Literal; use proc_macro::TokenStream; use proc_macro::TokenTree; +use syn::parse::Parse; + +mod common; +mod declare; +mod enumeration; +mod implementation; +mod interface; +mod protocol; /// Extract all identifiers in the given tokenstream. fn get_idents(input: TokenStream) -> impl Iterator { @@ -72,3 +80,124 @@ 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 interface(attr: TokenStream, item: TokenStream) -> TokenStream { + match (move || match syn::parse(attr)? { + crate::interface::MacroArgsRest::Intern(args) => { + let item_struct = syn::parse(item)?; + crate::interface::internal::item_interface(args, item_struct) + } + crate::interface::MacroArgsRest::Extern(args) => { + let item_type = syn::parse(item)?; + crate::interface::external::item_interface(args, item_type) + } + })() { + Ok(value) => value.into(), + Err(error) => error.to_compile_error().into(), + } +} + +#[allow(missing_docs)] +#[proc_macro_attribute] +pub fn implementation(attr: TokenStream, item: TokenStream) -> TokenStream { + match (move || { + let args = syn::parse::(attr)?.into(); + let item_impl = syn::parse(item)?; + crate::implementation::item_impl(args, item_impl) + })() { + Ok(value) => value.into(), + Err(error) => error.to_compile_error().into(), + } +} + +#[allow(missing_docs)] +#[proc_macro_attribute] +pub fn declare(attr: TokenStream, item: TokenStream) -> TokenStream { + match (move || { + let args = syn::parse::(attr)?.into(); + let item_extern = syn::parse(item)?; + crate::declare::item_declare(args, item_extern) + })() { + Ok(value) => value.into(), + Err(error) => error.to_compile_error().into(), + } +} + +#[inline] +fn dispatch_enum(attr: TokenStream, item: TokenStream) -> TokenStream +where + T: Parse, + T: Into, +{ + match (move || { + let args = syn::parse::(attr)?.into(); + let item_enum = syn::parse(item)?; + crate::enumeration::item_enum(args, item_enum) + })() { + Ok(value) => value.into(), + Err(error) => error.to_compile_error().into(), + } +} + +#[allow(missing_docs)] +#[proc_macro_attribute] +pub fn options(attr: TokenStream, item: TokenStream) -> TokenStream { + dispatch_enum::(attr, item) +} + +#[allow(missing_docs)] +#[proc_macro_attribute] +pub fn r#enum(attr: TokenStream, item: TokenStream) -> TokenStream { + dispatch_enum::(attr, item) +} + +#[allow(missing_docs)] +#[proc_macro_attribute] +pub fn closed_enum(attr: TokenStream, item: TokenStream) -> TokenStream { + dispatch_enum::(attr, item) +} + +#[allow(missing_docs)] +#[proc_macro_attribute] +pub fn typed_enum(attr: TokenStream, item: TokenStream) -> TokenStream { + dispatch_enum::(attr, item) +} + +#[allow(missing_docs)] +#[proc_macro_attribute] +pub fn typed_extensible_enum(attr: TokenStream, item: TokenStream) -> TokenStream { + dispatch_enum::(attr, item) +} + +#[allow(missing_docs)] +#[proc_macro_attribute] +pub fn error_enum(attr: TokenStream, item: TokenStream) -> TokenStream { + dispatch_enum::(attr, item) +} + +#[derive(Clone, Copy, Default)] +#[repr(transparent)] +struct CfgAttributes<'a> { + attrs: &'a [syn::Attribute], +} + +impl<'a> Iterator for CfgAttributes<'a> { + type Item = &'a syn::Attribute; + + #[inline] + fn next(&mut self) -> Option { + self.attrs.get(0).filter(|attr| { + self.attrs = &self.attrs[1..]; + attr.path.is_ident("cfg") + }) + } +} + +impl<'a> From<&'a [syn::Attribute]> for CfgAttributes<'a> { + #[inline] + fn from(attrs: &'a [syn::Attribute]) -> Self { + Self { attrs } + } +} diff --git a/crates/objc2-proc-macros/src/protocol.rs b/crates/objc2-proc-macros/src/protocol.rs new file mode 100644 index 000000000..84f12419f --- /dev/null +++ b/crates/objc2-proc-macros/src/protocol.rs @@ -0,0 +1,45 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::parse::{Parse, ParseStream}; + +pub(crate) struct MacroArgsRest; + +impl From for MacroArgs { + fn from(rest: MacroArgsRest) -> Self { + Self { + protocol_token: Default::default(), + rest, + } + } +} + +impl Parse for MacroArgsRest { + fn parse(_input: ParseStream<'_>) -> syn::Result { + Ok(Self) + } +} + +pub(crate) struct MacroArgs { + #[allow(unused)] + protocol_token: self::tokens::protocol, + #[allow(unused)] + rest: self::MacroArgsRest, +} + +impl Parse for MacroArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + protocol_token: input.parse()?, + rest: input.parse()?, + }) + } +} + +pub(crate) mod tokens { + syn::custom_keyword!(protocol); +} + +#[allow(unused)] +pub(crate) fn item_trait(_args: MacroArgs, item_trait: syn::ItemTrait) -> syn::Result { + Ok(item_trait.to_token_stream()) +} diff --git a/crates/objc2/Cargo.toml b/crates/objc2/Cargo.toml index 2bd7b7085..3ca754569 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", "num_enum"] # Currently not possible to turn off, put here for forwards compatibility. std = ["alloc", "objc2-encode/std", "objc-sys/std"] @@ -76,7 +77,9 @@ gnustep-2-1 = ["gnustep-2-0", "objc-sys/gnustep-2-1"] unstable-compiler-rt = ["apple"] [dependencies] +bitflags = { version = "1.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/mod.rs b/crates/objc2/src/__macro_helpers/mod.rs index 8601037f2..f07af51f7 100644 --- a/crates/objc2/src/__macro_helpers/mod.rs +++ b/crates/objc2/src/__macro_helpers/mod.rs @@ -629,6 +629,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/mod.rs b/crates/objc2/src/declare/mod.rs index d563eac30..1c8bc36d6 100644 --- a/crates/objc2/src/declare/mod.rs +++ b/crates/objc2/src/declare/mod.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/export.rs b/crates/objc2/src/export.rs new file mode 100644 index 000000000..9d86e62c7 --- /dev/null +++ b/crates/objc2/src/export.rs @@ -0,0 +1,48 @@ +#[cfg(feature = "objc2-proc-macros")] +extern crate self as objc2; + +#[cfg(feature = "objc2-proc-macros")] +#[cfg(not(feature = "std"))] +#[doc(hidden)] +pub use core::{ + borrow::{Borrow, BorrowMut}, + clone::Clone, + cmp::{Eq, Ord, PartialEq, PartialOrd}, + convert::{AsMut, AsRef, From, TryFrom}, + default::Default, + fmt::Debug, + hash::Hash, + marker::Copy, + mem::size_of, + ops::{Deref, DerefMut}, + option::Option::{self, None, Some}, + panic, + primitive::str, + result::Result::{self, Err, Ok}, + unreachable, +}; + +#[cfg(feature = "objc2-proc-macros")] +#[cfg(feature = "std")] +#[doc(hidden)] +pub use std::{ + borrow::{Borrow, BorrowMut}, + clone::Clone, + cmp::{Eq, Ord, PartialEq, PartialOrd}, + convert::{AsMut, AsRef, From, TryFrom}, + default::Default, + fmt::Debug, + hash::Hash, + marker::Copy, + mem::size_of, + ops::{Deref, DerefMut}, + option::Option::{self, None, Some}, + panic, + primitive::str, + result::Result::{self, Err, Ok}, + unreachable, +}; + +#[cfg(feature = "objc2-proc-macros")] +#[doc(hidden)] +pub use ::{bitflags, num_enum}; diff --git a/crates/objc2/src/lib.rs b/crates/objc2/src/lib.rs index 72f884ebc..fd95bddbc 100644 --- a/crates/objc2/src/lib.rs +++ b/crates/objc2/src/lib.rs @@ -148,7 +148,7 @@ //! peruse the documentation at will! //! //! [exc]: crate::exception -//! [declare]: crate::declare +//! [declare]: mod@crate::declare //! [rc]: crate::rc #![no_std] @@ -177,6 +177,11 @@ compile_error!("The `std` feature currently must be enabled."); extern crate alloc; extern crate std; +// Not public API. +#[doc(hidden)] +#[path = "export.rs"] +pub mod __private; + #[cfg(doctest)] #[doc = include_str!("../README.md")] extern "C" {} @@ -190,6 +195,15 @@ pub use self::encode::{Encode, Encoding, RefEncode}; pub use self::message::{Message, MessageArguments, MessageReceiver}; pub use self::protocol_type::ProtocolType; +#[cfg(feature = "objc2-proc-macros")] +pub use __macro_helpers::{Cases, Codes, ErrorEnum, TypedEnum, UserInfo}; + +#[cfg(feature = "objc2-proc-macros")] +pub use objc2_proc_macros::{ + closed_enum, declare, error_enum, implementation, interface, options, r#enum, typed_enum, + typed_extensible_enum, +}; + #[cfg(feature = "objc2-proc-macros")] #[doc(hidden)] pub use objc2_proc_macros::__hash_idents; diff --git a/crates/objc2/src/macros/declare_class.rs b/crates/objc2/src/macros/declare_class.rs index d4392a85c..b94251240 100644 --- a/crates/objc2/src/macros/declare_class.rs +++ b/crates/objc2/src/macros/declare_class.rs @@ -6,7 +6,7 @@ /// classes! /// /// [`extern_class!`]: crate::extern_class -/// [`declare`]: crate::declare +/// [`declare`]: mod@crate::declare /// /// /// # Specification diff --git a/crates/objc2/src/rc/id.rs b/crates/objc2/src/rc/id.rs index 25c1ef1b5..8d7840241 100644 --- a/crates/objc2/src/rc/id.rs +++ b/crates/objc2/src/rc/id.rs @@ -504,7 +504,7 @@ impl Id { /// optimization relies heavily on this function being tail called, so be /// careful to call this function at the end of your method. /// - /// [declare]: crate::declare + /// [declare]: mod@crate::declare /// [mmRules]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmRules.html /// ///