Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix enums and typedefs in framework crates #310

Open
madsmtm opened this issue Dec 23, 2022 · 9 comments
Open

Fix enums and typedefs in framework crates #310

madsmtm opened this issue Dec 23, 2022 · 9 comments
Labels
A-framework Affects the framework crates and the translator for them I-unsound A soundness hole

Comments

@madsmtm
Copy link
Owner

madsmtm commented Dec 23, 2022

Right now we're just using type aliases - instead, we should generate newtypes with the appropriate traits and constants. See also how Swift does it.

EDIT: I think the output should be something like this (depending on the naming scheme, so until that the variant/method names might be longer):

  • NS_OPTIONS (example: NSSortOptions):

    bitflags! {
        #[repr(transparent)]
        #[derive(Default)]
        pub struct NSSortOptions: NSUInteger {
            #[doc(alias = "NSSortConcurrent")]
            const CONCURRENT = 1 << 0;
            #[doc(alias = "NSSortStable")]
            const STABLE = 1 << 4;
        }
    }
    // Or perhaps we do something custom instead of `bitflags`?
    // Not sure if we want all the traits and helper functions it exposes?
    
    unsafe impl Encode for NSSortOptions { ... }
    unsafe impl RefEncode for NSSortOptions { ... }
  • NS_ENUM (example: NSDecodingFailurePolicy):

    // Cannot be a Rust `enum`, since that assumes that the enum is exhaustive
    // ABI-wise, even with `#[non_exhaustive]`.
    //
    // See https://play.rust-lang.org/?version=stable&mode=release&edition=2021&gist=3a19fcb4267d6fdb0d26b0c9defd946a
    #[repr(transparent)]
    #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
    pub struct NSDecodingFailurePolicy(NSInteger);
    
    unsafe impl Encode for NSDecodingFailurePolicy { ... }
    unsafe impl RefEncode for NSDecodingFailurePolicy { ... }
    
    impl NSDecodingFailurePolicy {
        pub fn as_raw(self) -> NSInteger { ... }
        pub fn from_raw(raw: NSInteger) -> Option<Self> { ... }
        pub unsafe fn from_raw_unchecked(raw: NSInteger) -> Self { ... }
    
        #[doc(alias = "NSDecodingFailurePolicyRaiseException")]
        pub const RAISE_EXCEPTION: Self = Self(0 as NSInteger);
        #[doc(alias = "NSDecodingFailurePolicySetErrorAndReturn")]
        pub const SET_ERROR_AND_RETURN: Self = Self(1 as NSInteger);
    }
    
    impl fmt::Debug for NSDecodingFailurePolicy { ... }
    
    // Same as `as_raw`
    impl From<NSDecodingFailurePolicy> for NSInteger { ... }
    // Same as `from_raw`
    impl TryFrom<NSInteger> for NSDecodingFailurePolicy { ... }
  • NS_CLOSED_ENUM (example: NSComparisonResult):

    #[repr(isize)]
    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
    enum NSComparisonResult {
        NSOrderedAscending = -1,
        NSOrderedSame = 0,
        NSOrderedDescending = 1,
    }
    
    unsafe impl Encode for NSComparisonResult { ... }
    unsafe impl RefEncode for NSComparisonResult { ... }
  • NS_TYPED_ENUM (example: NSStringEncodingDetectionOptionsKey):

    #[repr(C)]
    #[derive(Debug, PartialEq, Eq, Hash)]
    pub struct NSStringEncodingDetectionOptionsKey(NSString);
    
    unsafe impl RefEncode for NSStringEncodingDetectionOptionsKey { ... }
    unsafe impl Message for NSStringEncodingDetectionOptionsKey { ... }
    
    impl NSStringEncodingDetectionOptionsKey {
        pub fn as_raw(&self) -> &NSString { ... }
        pub fn into_raw(this: Id<Self>) -> Id<NSString> { ... }
        pub unsafe fn from_raw_unchecked(raw: &NSString) -> &Self { ... }
        pub unsafe fn from_raw_id_unchecked(raw: Id<NSString>) -> Id<Self> { ... }
    }
    
    // Delegates to `NSString`
    impl fmt::Display for NSStringEncodingDetectionOptionsKey { ... }
    
    // + AsRef, Borrow and From implementations
    
    // Constants
    impl NSStringEncodingDetectionOptionsKey {
        #[doc(alias = "NSStringEncodingDetectionSuggestedEncodingsKey")]
        pub fn suggested_encodings_key() -> &'static Self { ... }
    }
    impl NSStringEncodingDetectionOptionsKey {
        #[doc(alias = "NSStringEncodingDetectionDisallowedEncodingsKey")]
        pub fn disallowed_encodings_key() -> &'static Self { ... }
    }
    // ...
  • NS_TYPED_EXTENSIBLE_ENUM (example: NSExceptionName):

    #[repr(C)]
    #[derive(Debug, PartialEq, Eq, Hash)]
    pub struct NSExceptionName(pub(crate) NSString);
    
    unsafe impl RefEncode for NSExceptionName { ... }
    unsafe impl Message for NSExceptionName { ... }
    
    impl NSExceptionName {
        pub fn as_raw(&self) -> &NSString { ... }
        pub fn into_raw(this: Id<Self>) -> Id<NSString> { ... }
        pub fn from_raw(raw: &NSString) -> &Self { ... }
        pub fn from_raw_id(raw: Id<NSString>) -> Id<Self> { ... }
    }
    
    // Delegates to `NSString`
    impl fmt::Display for NSExceptionName { ... }
    
    // + AsRef, Borrow and From implementations
    
    // Constants (may be spread across frameworks)
    impl NSExceptionName {
        #[doc(alias = "NSGenericException")]
        pub fn generic_exception() -> &'static Self { ... }
    }
    impl NSExceptionName {
        #[doc(alias = "NSRangeException")]
        pub fn range_exception() -> &'static Self { ... }
    }
    // ...
  • NS_ERROR_ENUM with typedef (example: MTLIOError + MTLIOErrorDomain):

    todo!()
  • NS_ERROR_ENUM without typedef (example: NSURLErrorDomain + related URL errors):

    todo!()
  • Anonymous enum (example: Memory Allocation Options):

    const NSScannedOption: NSUInteger = 1 << 0;
    const NSCollectorDisabledOption: NSUInteger = 1 << 1;
    
    // Note: A bit unsure of the type here, maybe we just want to always use `NSInteger` like Swift does?
@madsmtm madsmtm added A-framework Affects the framework crates and the translator for them I-unsound A soundness hole labels Dec 23, 2022
@madsmtm madsmtm added this to the icrate v0.1.0 milestone Jan 27, 2023
@silvanshade
Copy link
Contributor

silvanshade commented Feb 20, 2023

I believe the implementation I've been working on in #425 does a reasonably good job now of handling enums at least.

I generally tried to follow a similar approach for mapping things over to how the enums are translated for Swift. There are some caveats, for example due to the fact that we can't define an enum within a struct in Rust like you can in Swift, or not having impl for enum, or not (yet) having associated types for inherent impls.

The macros automatically handle renaming by stripping variant prefixes if they match the enum name:

#[objc(error_enum,
    mod = sn_error,
    domain = unsafe { SNErrorDomain }
)]
pub enum SNErrorCode {
    SNErrorCodeUnknownError = 1,
    SNErrorCodeOperationFailed,
    SNErrorCodeInvalidFormat,
    SNErrorCodeInvalidModel,
    SNErrorCodeInvalidFile,
}

This generates code like this fragment (along with a bunch of additional helper machinery):

struct SNError {} // inherits from `NSError`
mod sn_error {
    struct Codes(isize);
    impl Codes {
        const UnknownError: Self = Self(Cases::UnknownError as isize);
        const OperationFailed: Self = Self(Cases::OperationFailed as isize);
        const InvalidFormat: Self = Self(Cases::InvalidFormat as isize);
        const InvalidModel: Self = Self(Cases::InvalidModel as isize);
        const InvalidFile: Self = Self(Cases::InvalidFile as isize);
    }
    pub enum Cases {
        UnknownError = 1,
        OperationFailed,
        InvalidFormat,
        InvalidModel,
        InvalidFile,
    }
}

Also, if you use the name enum __anonymous__ { ... }, it will attempt to extract and generate the enum name from the longest common prefix of the variant names.

The most sophisticated example is icrate/examples/ns_error_enum.rs where I use the macro to create an error enum translation for NSURLError. (See the code snippet and macro expansion at the end for reference).

A couple things to note:

You can (optionally) specify the name of the helper module with mod. I think it might be reasonable to update the macro with a list of Apple system framework prefixes and special cases so this isn't needed (the only reason it's used here is for correct tokenization of NSURL).

You can also (optionally) specify refinements for the userInfo dictionary for the error. The syntax is { key = <ident>, type = <type>, from? = <expr>, into? = <expr> }. The key is expected to be an identifier in scope that resolves to a dictionary key. The type is the type of the associated field in the generated UserInfo struct (which is then wrapped in Option<...>). The optional from and into keys are used to convert the entry from the NSDictionary to the UserInfo field, and then back from the field into the entry.

Similar to Swift, getters are generated for the specified UserInfo fields, but you can also pattern match on the entire UserInfo struct (which uses the getters to populate) like so:

// `UserInfo<T>` is an alias for `<T as objc2::ErrorEnum>::UserInfo` (to avoid mentioning the helper module)
let UserInfo::<NSURLError> {
    failing_url,
    failing_url_string,
    background_task_cancelled_reason,
    network_unavailable_reason,
} = error.user_info();
let failing_url_string = error.failing_url_string();

The error declaration looks like this:

#[objc(error_enum,
    mod = ns_url_error,
    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<NSURL> },
        /// # SAFETY (type)
        /// https://developer.apple.com/documentation/foundation/nsurlerrorfailingurlstringerrorkey?language=objc
        { key = NSURLErrorFailingURLStringErrorKey, unsafe type = Id<NSString> },
        /// # 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<Id<NSNumber>>| 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,
}

which expands to the following:

// Recursive expansion of objc! macro
// ===================================

#[cfg(all(
    feature = "Foundation",
    feature = "Foundation_NSError",
    feature = "Foundation_NSString"
))]
objc2::declare_class!(
    pub struct NSURLError {}
    unsafe impl ClassType for NSURLError {
        type Super = icrate::Foundation::NSError;
        const NAME: &'static str = "NSURLError";
    }
);
#[cfg(all(
    feature = "Foundation",
    feature = "Foundation_NSError",
    feature = "Foundation_NSString"
))]
impl NSURLError {
    #[inline]
    pub fn domain() -> &'static icrate::Foundation::NSErrorDomain {
        unsafe { NSURLErrorDomain }
    }
    #[inline]
    pub fn code(&self) -> ns_url_error::Codes {
        ns_url_error::Codes(self.as_super().code())
    }
    #[inline]
    pub fn localized_description(&self) -> objc2::rc::Id<icrate::Foundation::NSString> {
        self.as_super().localizedDescription()
    }
    #[inline]
    pub fn user_info(&self) -> ns_url_error::UserInfo {
        let mut user_info = ns_url_error::UserInfo::default();
        user_info.failing_url = self.failing_url();
        user_info.failing_url_string = self.failing_url_string();
        user_info.background_task_cancelled_reason = self.background_task_cancelled_reason();
        user_info.network_unavailable_reason = self.network_unavailable_reason();
        user_info
    }
    #[inline]
    pub fn new(code: ns_url_error::Codes) -> objc2::rc::Id<Self, objc2::rc::Shared> {
        let code = code.0;
        let domain = Self::domain();
        let error = unsafe { icrate::Foundation::NSError::new(code, domain) };
        unsafe { objc2::rc::Id::cast(error) }
    }
    #[cfg(all(
        feature = "Foundation_NSDictionary",
        feature = "Foundation_NSMutableDictionary"
    ))]
    #[inline]
    fn new_with_user_info(
        code: ns_url_error::Codes,
        user_info: ns_url_error::UserInfo,
    ) -> objc2::rc::Id<Self, objc2::rc::Shared> {
        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: ns_url_error::UserInfo,
    ) -> objc2::rc::Id<
        icrate::Foundation::NSMutableDictionary<
            icrate::Foundation::NSErrorUserInfoKey,
            objc2::runtime::Object,
        >,
    > {
        let dict = icrate::Foundation::NSMutableDictionary::<
            icrate::Foundation::NSErrorUserInfoKey,
            objc2::runtime::Object,
        >::new();
        #[allow(unused_unsafe)]
        if let Some(value) = (user_info.failing_url) {
            unsafe { dict.setValue_forKey(Some(&*value), NSURLErrorFailingURLErrorKey) };
        }
        #[allow(unused_unsafe)]
        if let Some(value) = (user_info.failing_url_string) {
            unsafe { dict.setValue_forKey(Some(&*value), NSURLErrorFailingURLStringErrorKey) };
        }
        #[allow(unused_unsafe)]
        if let Some(value) = (user_info.background_task_cancelled_reason) {
            unsafe {
                dict.setValue_forKey(Some(&*value), NSURLErrorBackgroundTaskCancelledReasonKey)
            };
        }
        #[allow(unused_unsafe)]
        if let Some(value) = (std::convert::identity)(user_info.network_unavailable_reason) {
            unsafe { dict.setValue_forKey(Some(&*value), NSURLErrorNetworkUnavailableReasonKey) };
        }
        objc2::rc::Id::into_shared(dict)
    }
    #[doc = " # SAFETY (type)"]
    #[doc = " https://developer.apple.com/documentation/foundation/nsurlerrorfailingurlerrorkey?language=objc"]
    fn failing_url(&self) -> Option<Id<NSURL>> {
        let user_info = self.as_super().userInfo();
        let value = unsafe { user_info.valueForKey(NSURLErrorFailingURLErrorKey) }
            .map(|inner| unsafe { objc2::rc::Id::cast(inner) });
        #[allow(unused_unsafe)]
        (|n: Option<_>| n.map(TryInto::try_into).and_then(Result::ok))(value)
    }
    #[doc = " # SAFETY (type)"]
    #[doc = " https://developer.apple.com/documentation/foundation/nsurlerrorfailingurlstringerrorkey?language=objc"]
    fn failing_url_string(&self) -> Option<Id<NSString>> {
        let user_info = self.as_super().userInfo();
        let value = unsafe { user_info.valueForKey(NSURLErrorFailingURLStringErrorKey) }
            .map(|inner| unsafe { objc2::rc::Id::cast(inner) });
        #[allow(unused_unsafe)]
        (|n: Option<_>| n.map(TryInto::try_into).and_then(Result::ok))(value)
    }
    #[doc = " # SAFETY (type)"]
    #[doc = " https://developer.apple.com/documentation/foundation/nsurlerrorbackgroundtaskcancelledreasonkey?language=objc"]
    fn background_task_cancelled_reason(
        &self,
    ) -> Option<ns_url_error_reasons::BackgroundTaskCancelledReason> {
        let user_info = self.as_super().userInfo();
        let value = unsafe { user_info.valueForKey(NSURLErrorBackgroundTaskCancelledReasonKey) }
            .map(|inner| unsafe { objc2::rc::Id::cast(inner) });
        #[allow(unused_unsafe)]
        (|n: Option<_>| n.map(TryInto::try_into).and_then(Result::ok))(value)
    }
    #[doc = " # SAFETY (type)"]
    #[doc = " https://developer.apple.com/documentation/foundation/nsurlerrornetworkunavailablereasonkey?language=objc\'"]
    #[doc = " "]
    #[doc = " # SAFETY (from)"]
    #[doc = " 1. Casting dictionary `Object` to `NSNumber` is safe (type)"]
    #[doc = " 2. `NSNumber` can safely be converted to `NetworkUnavailableReason`"]
    #[doc = ""]
    #[doc = " # SAFETY (into)"]
    #[doc = " `NetworkUnavailableReason` can safely be converted to `NSObject` via deref (through"]
    #[doc = " converting to `NSNumber` first). This happens implicitly in the `&*arg` that"]
    #[doc = " occurs during the call to insert the value in the dictionary."]
    fn network_unavailable_reason(&self) -> Option<ns_url_error_reasons::NetworkUnavailableReason> {
        let user_info = self.as_super().userInfo();
        let value = unsafe { user_info.valueForKey(NSURLErrorNetworkUnavailableReasonKey) }
            .map(|inner| unsafe { objc2::rc::Id::cast(inner) });
        #[allow(unused_unsafe)]
        (|n: Option<Id<NSNumber>>| n.map(TryInto::try_into).and_then(Result::ok))(value)
    }
}
#[cfg(all(
    feature = "Foundation",
    feature = "Foundation_NSError",
    feature = "Foundation_NSString"
))]
impl objc2::ErrorEnum for NSURLError {
    type Codes = ns_url_error::Codes;
    type UserInfo = ns_url_error::UserInfo;
}
#[cfg(all(
    feature = "Foundation",
    feature = "Foundation_NSError",
    feature = "Foundation_NSString"
))]
impl objc2::TypedEnum for NSURLError {
    type Cases = ns_url_error::Cases;
}
#[allow(non_snake_case)]
pub mod ns_url_error {
    #[derive(Eq, PartialEq)]
    #[repr(transparent)]
    pub struct Codes(pub(super) isize);

    impl Codes {
        #![allow(non_upper_case_globals)]
        pub const Unknown: Self = Self(Cases::Unknown as isize);
        pub const Cancelled: Self = Self(Cases::Cancelled as isize);
        pub const BadURL: Self = Self(Cases::BadURL as isize);
        pub const TimedOut: Self = Self(Cases::TimedOut as isize);
        pub const UnsupportedURL: Self = Self(Cases::UnsupportedURL as isize);
        pub const CannotFindHost: Self = Self(Cases::CannotFindHost as isize);
        pub const CannotConnectToHost: Self = Self(Cases::CannotConnectToHost as isize);
        pub const NetworkConnectionLost: Self = Self(Cases::NetworkConnectionLost as isize);
        pub const DNSLookupFailed: Self = Self(Cases::DNSLookupFailed as isize);
        pub const HTTPTooManyRedirects: Self = Self(Cases::HTTPTooManyRedirects as isize);
        pub const ResourceUnavailable: Self = Self(Cases::ResourceUnavailable as isize);
        pub const NotConnectedToInternet: Self = Self(Cases::NotConnectedToInternet as isize);
        pub const RedirectToNonExistentLocation: Self =
            Self(Cases::RedirectToNonExistentLocation as isize);
        pub const BadServerResponse: Self = Self(Cases::BadServerResponse as isize);
        pub const UserCancelledAuthentication: Self =
            Self(Cases::UserCancelledAuthentication as isize);
        pub const UserAuthenticationRequired: Self =
            Self(Cases::UserAuthenticationRequired as isize);
        pub const ZeroByteResource: Self = Self(Cases::ZeroByteResource as isize);
        pub const CannotDecodeRawData: Self = Self(Cases::CannotDecodeRawData as isize);
        pub const CannotDecodeContentData: Self = Self(Cases::CannotDecodeContentData as isize);
        pub const CannotParseResponse: Self = Self(Cases::CannotParseResponse as isize);
        pub const AppTransportSecurityRequiresSecureConnection: Self =
            Self(Cases::AppTransportSecurityRequiresSecureConnection as isize);
        pub const FileDoesNotExist: Self = Self(Cases::FileDoesNotExist as isize);
        pub const FileIsDirectory: Self = Self(Cases::FileIsDirectory as isize);
        pub const NoPermissionsToReadFile: Self = Self(Cases::NoPermissionsToReadFile as isize);
        pub const DataLengthExceedsMaximum: Self = Self(Cases::DataLengthExceedsMaximum as isize);
        pub const FileOutsideSafeArea: Self = Self(Cases::FileOutsideSafeArea as isize);
        pub const SecureConnectionFailed: Self = Self(Cases::SecureConnectionFailed as isize);
        pub const ServerCertificateHasBadDate: Self =
            Self(Cases::ServerCertificateHasBadDate as isize);
        pub const ServerCertificateUntrusted: Self =
            Self(Cases::ServerCertificateUntrusted as isize);
        pub const ServerCertificateHasUnknownRoot: Self =
            Self(Cases::ServerCertificateHasUnknownRoot as isize);
        pub const ServerCertificateNotYetValid: Self =
            Self(Cases::ServerCertificateNotYetValid as isize);
        pub const ClientCertificateRejected: Self = Self(Cases::ClientCertificateRejected as isize);
        pub const ClientCertificateRequired: Self = Self(Cases::ClientCertificateRequired as isize);
        pub const CannotLoadFromNetwork: Self = Self(Cases::CannotLoadFromNetwork as isize);
        pub const CannotCreateFile: Self = Self(Cases::CannotCreateFile as isize);
        pub const CannotOpenFile: Self = Self(Cases::CannotOpenFile as isize);
        pub const CannotCloseFile: Self = Self(Cases::CannotCloseFile as isize);
        pub const CannotWriteToFile: Self = Self(Cases::CannotWriteToFile as isize);
        pub const CannotRemoveFile: Self = Self(Cases::CannotRemoveFile as isize);
        pub const CannotMoveFile: Self = Self(Cases::CannotMoveFile as isize);
        pub const DownloadDecodingFailedMidStream: Self =
            Self(Cases::DownloadDecodingFailedMidStream as isize);
        pub const DownloadDecodingFailedToComplete: Self =
            Self(Cases::DownloadDecodingFailedToComplete as isize);
        pub const InternationalRoamingOff: Self = Self(Cases::InternationalRoamingOff as isize);
        pub const CallIsActive: Self = Self(Cases::CallIsActive as isize);
        pub const DataNotAllowed: Self = Self(Cases::DataNotAllowed as isize);
        pub const RequestBodyStreamExhausted: Self =
            Self(Cases::RequestBodyStreamExhausted as isize);
        pub const BackgroundSessionRequiresSharedContainer: Self =
            Self(Cases::BackgroundSessionRequiresSharedContainer as isize);
        pub const BackgroundSessionInUseByAnotherProcess: Self =
            Self(Cases::BackgroundSessionInUseByAnotherProcess as isize);
        pub const BackgroundSessionWasDisconnected: Self =
            Self(Cases::BackgroundSessionWasDisconnected as isize);
        pub fn cases(&self) -> core::option::Option<Cases> {
            match self {
                &Self::Unknown => Cases::Unknown.into(),
                &Self::Cancelled => Cases::Cancelled.into(),
                &Self::BadURL => Cases::BadURL.into(),
                &Self::TimedOut => Cases::TimedOut.into(),
                &Self::UnsupportedURL => Cases::UnsupportedURL.into(),
                &Self::CannotFindHost => Cases::CannotFindHost.into(),
                &Self::CannotConnectToHost => Cases::CannotConnectToHost.into(),
                &Self::NetworkConnectionLost => Cases::NetworkConnectionLost.into(),
                &Self::DNSLookupFailed => Cases::DNSLookupFailed.into(),
                &Self::HTTPTooManyRedirects => Cases::HTTPTooManyRedirects.into(),
                &Self::ResourceUnavailable => Cases::ResourceUnavailable.into(),
                &Self::NotConnectedToInternet => Cases::NotConnectedToInternet.into(),
                &Self::RedirectToNonExistentLocation => Cases::RedirectToNonExistentLocation.into(),
                &Self::BadServerResponse => Cases::BadServerResponse.into(),
                &Self::UserCancelledAuthentication => Cases::UserCancelledAuthentication.into(),
                &Self::UserAuthenticationRequired => Cases::UserAuthenticationRequired.into(),
                &Self::ZeroByteResource => Cases::ZeroByteResource.into(),
                &Self::CannotDecodeRawData => Cases::CannotDecodeRawData.into(),
                &Self::CannotDecodeContentData => Cases::CannotDecodeContentData.into(),
                &Self::CannotParseResponse => Cases::CannotParseResponse.into(),
                &Self::AppTransportSecurityRequiresSecureConnection => {
                    Cases::AppTransportSecurityRequiresSecureConnection.into()
                }
                &Self::FileDoesNotExist => Cases::FileDoesNotExist.into(),
                &Self::FileIsDirectory => Cases::FileIsDirectory.into(),
                &Self::NoPermissionsToReadFile => Cases::NoPermissionsToReadFile.into(),
                &Self::DataLengthExceedsMaximum => Cases::DataLengthExceedsMaximum.into(),
                &Self::FileOutsideSafeArea => Cases::FileOutsideSafeArea.into(),
                &Self::SecureConnectionFailed => Cases::SecureConnectionFailed.into(),
                &Self::ServerCertificateHasBadDate => Cases::ServerCertificateHasBadDate.into(),
                &Self::ServerCertificateUntrusted => Cases::ServerCertificateUntrusted.into(),
                &Self::ServerCertificateHasUnknownRoot => {
                    Cases::ServerCertificateHasUnknownRoot.into()
                }
                &Self::ServerCertificateNotYetValid => Cases::ServerCertificateNotYetValid.into(),
                &Self::ClientCertificateRejected => Cases::ClientCertificateRejected.into(),
                &Self::ClientCertificateRequired => Cases::ClientCertificateRequired.into(),
                &Self::CannotLoadFromNetwork => Cases::CannotLoadFromNetwork.into(),
                &Self::CannotCreateFile => Cases::CannotCreateFile.into(),
                &Self::CannotOpenFile => Cases::CannotOpenFile.into(),
                &Self::CannotCloseFile => Cases::CannotCloseFile.into(),
                &Self::CannotWriteToFile => Cases::CannotWriteToFile.into(),
                &Self::CannotRemoveFile => Cases::CannotRemoveFile.into(),
                &Self::CannotMoveFile => Cases::CannotMoveFile.into(),
                &Self::DownloadDecodingFailedMidStream => {
                    Cases::DownloadDecodingFailedMidStream.into()
                }
                &Self::DownloadDecodingFailedToComplete => {
                    Cases::DownloadDecodingFailedToComplete.into()
                }
                &Self::InternationalRoamingOff => Cases::InternationalRoamingOff.into(),
                &Self::CallIsActive => Cases::CallIsActive.into(),
                &Self::DataNotAllowed => Cases::DataNotAllowed.into(),
                &Self::RequestBodyStreamExhausted => Cases::RequestBodyStreamExhausted.into(),
                &Self::BackgroundSessionRequiresSharedContainer => {
                    Cases::BackgroundSessionRequiresSharedContainer.into()
                }
                &Self::BackgroundSessionInUseByAnotherProcess => {
                    Cases::BackgroundSessionInUseByAnotherProcess.into()
                }
                &Self::BackgroundSessionWasDisconnected => {
                    Cases::BackgroundSessionWasDisconnected.into()
                }
                _ => None,
            }
        }
        #[inline]
        pub fn peek(&self) -> &isize {
            &self.0
        }
        #[inline]
        pub fn take(self) -> isize {
            self.0
        }
    }
    impl From<Cases> for Codes {
        fn from(case: Cases) -> Self {
            match case {
                Cases::Unknown => Codes::Unknown,
                Cases::Cancelled => Codes::Cancelled,
                Cases::BadURL => Codes::BadURL,
                Cases::TimedOut => Codes::TimedOut,
                Cases::UnsupportedURL => Codes::UnsupportedURL,
                Cases::CannotFindHost => Codes::CannotFindHost,
                Cases::CannotConnectToHost => Codes::CannotConnectToHost,
                Cases::NetworkConnectionLost => Codes::NetworkConnectionLost,
                Cases::DNSLookupFailed => Codes::DNSLookupFailed,
                Cases::HTTPTooManyRedirects => Codes::HTTPTooManyRedirects,
                Cases::ResourceUnavailable => Codes::ResourceUnavailable,
                Cases::NotConnectedToInternet => Codes::NotConnectedToInternet,
                Cases::RedirectToNonExistentLocation => Codes::RedirectToNonExistentLocation,
                Cases::BadServerResponse => Codes::BadServerResponse,
                Cases::UserCancelledAuthentication => Codes::UserCancelledAuthentication,
                Cases::UserAuthenticationRequired => Codes::UserAuthenticationRequired,
                Cases::ZeroByteResource => Codes::ZeroByteResource,
                Cases::CannotDecodeRawData => Codes::CannotDecodeRawData,
                Cases::CannotDecodeContentData => Codes::CannotDecodeContentData,
                Cases::CannotParseResponse => Codes::CannotParseResponse,
                Cases::AppTransportSecurityRequiresSecureConnection => {
                    Codes::AppTransportSecurityRequiresSecureConnection
                }
                Cases::FileDoesNotExist => Codes::FileDoesNotExist,
                Cases::FileIsDirectory => Codes::FileIsDirectory,
                Cases::NoPermissionsToReadFile => Codes::NoPermissionsToReadFile,
                Cases::DataLengthExceedsMaximum => Codes::DataLengthExceedsMaximum,
                Cases::FileOutsideSafeArea => Codes::FileOutsideSafeArea,
                Cases::SecureConnectionFailed => Codes::SecureConnectionFailed,
                Cases::ServerCertificateHasBadDate => Codes::ServerCertificateHasBadDate,
                Cases::ServerCertificateUntrusted => Codes::ServerCertificateUntrusted,
                Cases::ServerCertificateHasUnknownRoot => Codes::ServerCertificateHasUnknownRoot,
                Cases::ServerCertificateNotYetValid => Codes::ServerCertificateNotYetValid,
                Cases::ClientCertificateRejected => Codes::ClientCertificateRejected,
                Cases::ClientCertificateRequired => Codes::ClientCertificateRequired,
                Cases::CannotLoadFromNetwork => Codes::CannotLoadFromNetwork,
                Cases::CannotCreateFile => Codes::CannotCreateFile,
                Cases::CannotOpenFile => Codes::CannotOpenFile,
                Cases::CannotCloseFile => Codes::CannotCloseFile,
                Cases::CannotWriteToFile => Codes::CannotWriteToFile,
                Cases::CannotRemoveFile => Codes::CannotRemoveFile,
                Cases::CannotMoveFile => Codes::CannotMoveFile,
                Cases::DownloadDecodingFailedMidStream => Codes::DownloadDecodingFailedMidStream,
                Cases::DownloadDecodingFailedToComplete => Codes::DownloadDecodingFailedToComplete,
                Cases::InternationalRoamingOff => Codes::InternationalRoamingOff,
                Cases::CallIsActive => Codes::CallIsActive,
                Cases::DataNotAllowed => Codes::DataNotAllowed,
                Cases::RequestBodyStreamExhausted => Codes::RequestBodyStreamExhausted,
                Cases::BackgroundSessionRequiresSharedContainer => {
                    Codes::BackgroundSessionRequiresSharedContainer
                }
                Cases::BackgroundSessionInUseByAnotherProcess => {
                    Codes::BackgroundSessionInUseByAnotherProcess
                }
                Cases::BackgroundSessionWasDisconnected => Codes::BackgroundSessionWasDisconnected,
            }
        }
    }
    impl core::convert::From<Codes> for isize {
        #[inline]
        fn from(wrapper: Codes) -> Self {
            wrapper.take()
        }
    }
    impl From<Cases> for isize {
        #[inline]
        fn from(case: Cases) -> Self {
            Self::from(Codes::from(case))
        }
    }
    impl objc2::TypedEnum for Codes {
        type Cases = Cases;
    }
    #[cfg(all(feature = "Foundation", feature = "Foundation_NSNumber"))]
    impl From<Codes> for objc2::rc::Id<icrate::Foundation::NSNumber> {
        #[inline]
        fn from(value: Codes) -> Self {
            icrate::Foundation::NSNumber::new_isize(value.0.into())
        }
    }
    #[cfg(feature = "Foundation")]
    impl core::ops::Deref for Codes {
        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::<icrate::Foundation::NSObject>(num) };
            let obj = objc2::rc::Id::autorelease_return(obj);
            let obj = unsafe { obj.as_ref() }.expect("pointer is non-null");
            obj
        }
    }
    #[cfg(all(feature = "Foundation", feature = "Foundation_NSNumber"))]
    impl TryFrom<&icrate::Foundation::NSNumber> for Codes {
        type Error = &'static str;
        fn try_from(number: &icrate::Foundation::NSNumber) -> Result<Self, Self::Error> {
            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 {
                let number = number.as_isize();
                let value = Self(number);
                if value.cases().is_some() {
                    Ok(value)
                } else {
                    Err("NSNumber value does not match any of the enum values")
                }
            } else {
                Err("NSNumber encoding is not equivalent to error repr")
            }
        }
    }
    impl<O: objc2::rc::Ownership> TryFrom<objc2::rc::Id<icrate::Foundation::NSNumber, O>> for Codes {
        type Error = &'static str;
        #[inline]
        fn try_from(number: Id<icrate::Foundation::NSNumber, O>) -> Result<Self, Self::Error> {
            (&*number).try_into()
        }
    }
    #[allow(non_camel_case_types)]
    #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
    #[non_exhaustive]
    pub enum Cases {
        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,
    }
    #[allow(unused_imports)]
    use super::*;
    #[cfg(feature = "Foundation")]
    #[allow(unused_imports)]
    use icrate::Foundation::*;
    #[allow(unused_imports)]
    use objc2::{rc::Id, runtime::Object};
    #[derive(Default)]
    pub struct UserInfo {
        pub failing_url: Option<Id<NSURL>>,
        pub failing_url_string: Option<Id<NSString>>,
        pub background_task_cancelled_reason:
            Option<ns_url_error_reasons::BackgroundTaskCancelledReason>,
        pub network_unavailable_reason: Option<ns_url_error_reasons::NetworkUnavailableReason>,
    }
}

@madsmtm
Copy link
Owner Author

madsmtm commented Feb 22, 2023

I think it's important to separate syntax of the macro from the desired output. To this end, I've edited the top-level comment with what I think the desired output should be, then that part is perhaps easier to discuss.
For example, I disagree that we want NSNumber conversions, I think it's better to just have raw primitive conversions, and then the user can do number.as_isize() themselves if they need to.


I can see you've spent a lot of time on translation of error enums - I'm quite uncertain about those myself, but I think you've taken a big step in the right direction. Comments on that:

  • Cases feels a bit redundant, couldn't we have just Codes? The user can't do an exhaustive match anyway.

  • I like the idea of allowing easier access to user info keys, but I think it should rather be a zero-cost solution like this (e.g. instead of fetching everything into a struct, we allow the user to only fetch the keys that are relevant to them):

    impl NSURLError {
        pub fn userInfo() -> UserInfo { ... }
    }
    
    struct UserInfo(NSDictionary<NSErrorUserInfoKey, Object>);
    // impl RefEncode + Message + Deref + ...
    
    impl UserInfo {
        pub fn failing_url() -> Option<Id<NSURL>> { ... }
        pub fn failing_url_string() -> Option<Id<NSString>> { ... }
        // Or perhaps just returns `Option<Id<NSNumber>>`
        pub fn background_task_cancelled_reason() -> Option<BackgroundTaskCancelledReason> { ... }
        // Or perhaps just returns `Option<Id<NSNumber>>`
        pub fn network_unavailable_reason() -> Option<Id<NetworkUnavailableReason>> { ... }
    }

    Though I recognize that that doesn't lend itself as easily to the new_with_user_info pattern :/

  • I we should consider getting rid of the module, and just have NSURLError, NSURLErrorCode and NSURLErrorUserInfo.


Regarding macro syntax, I think it's basically irrelevant since I don't think it'll really be necessary to expose the error enum macro to users? In any case, I think I'd like to split the error and the userInfo stuff into two different macros, it seems like they don't need to be in the same macro?

@silvanshade
Copy link
Contributor

silvanshade commented Feb 22, 2023

For example, I disagree that we want NSNumber conversions, I think it's better to just have raw primitive conversions, and then the user can do number.as_isize() themselves if they need to.

Without the NSNumber conversions, we cannot automatically generate the UserInfo machinery without requiring that the user write from and into for every instance where the field is some generated enum type.

That's the only reason they are there really.

We could easily make them optional as well, for cases where they wouldn't be needed.

I can see you've spent a lot of time on translation of error enums - I'm quite uncertain about those myself, but I think you've taken a big step in the right direction.

I mostly followed the design and rationale that was laid out here.

Personally, I think we really should have something like this if the intention is truly to allow Rust (through this crate ecosystem) to be an alternative to Swift in terms of ergonomics and feature parity (as far as that is possible).

Did you have another alternative in mind perhaps?

  • Cases feels a bit redundant, couldn't we have just Codes? The user can't do an exhaustive match anyway.

The main motivation for Cases is to provide a better pattern matching experience through various Rust tooling. In particular, I heavily use the Rust Analyzer feature for automatically filling in match arms, and this feature wouldn't work if we only had Codes.

The experience of just pressing a key combination and getting all of the error cases filled in versus looking them up individually in the docs (or even just completing each one manually) makes a fairly big difference in usability from my perspective.

  • I like the idea of allowing easier access to user info keys, but I think it should rather be a zero-cost solution like this (e.g. instead of fetching everything into a struct, we allow the user to only fetch the keys that are relevant to them):

We have both actually. I generate individual getters first, then the code that fetches the entire dictionary is just implemented in terms of that:

pub fn user_info(&self) -> ns_url_error::UserInfo {
    let mut user_info = ns_url_error::UserInfo::default();
    user_info.failing_url = self.failing_url();
    user_info.failing_url_string = self.failing_url_string();
    user_info.background_task_cancelled_reason = self.background_task_cancelled_reason();
    user_info.network_unavailable_reason = self.network_unavailable_reason();
    user_info
}
  • I we should consider getting rid of the module, and just have NSURLError, NSURLErrorCode and NSURLErrorUserInfo.

I think that would probably be fine. Part of the rationale behind the use of the submodules was to allow for fewer imports (by relying on the Cases, Codes, and UserInfo helper types being in scope already). I'm not sure how much of a difference that will make on average though.

Regarding macro syntax, I think it's basically irrelevant since I don't think it'll really be necessary to expose the error enum macro to users?

I'm not sure actually. Do people use ns_error_enum in 3rd party Objective-C frameworks? I guess the other question is whether we intend to support that use case in general. I've always kind of assumed we would but I don't know how the translator would work for other frameworks.

In any case, I think I'd like to split the error and the userInfo stuff into two different macros, it seems like they don't need to be in the same macro?

It would be possible to split the userInfo support code generation into a separate macro invocation.

The way the code is implemented, they are not deeply intertwined. Mostly the ns_error_enum macro is a combination of ns_typed_extensible_enum and userInfo generation. I just felt at the time that it was the most logical place for that information (following the general design of other attribute proc-macros). But maybe there is a better alternative.

@silvanshade
Copy link
Contributor

I think it's important to separate syntax of the macro from the desired output. To this end, I've edited the top-level comment with what I think the desired output should be, then that part is perhaps easier to discuss.

Thanks for the examples. That gives a good point of reference. I'll comment on the individual samples versus what is currently implemented.

  • NS_OPTIONS

I am currently using the bitflags! macro like you suggest. I haven't the objc2 helper traits yet.

With regard to whether we want all the additional helper traits from bitflags, I would personally opt to use what they provide unless we find that it impacts compilation time too severely.

I think the maintenance burden of having to maintain our own reimplementation is probably not worth it. But that's just my personal take.

  • NS_ENUM

I am currently translating this as an enum, but was worried about the exact issue you mention (which I hadn't tested yet).

I think the best option is just to handle this case the same as NS_TYPED_EXTENSIBLE_ENUM, specialized for numeric types. This is basically what I am doing for the NS_ERROR_ENUM case.

  • NS_CLOSED_ENUM

I am currently translating this how you suggest but haven't added the objc2 helper traits yet.

  • NS_TYPED_ENUM, NS_TYPED_EXTENSIBLE_ENUM

The representation you propose for both of these is different to how I am currently generating them.

For starters I am using #[repr(transparent)] instead of #[repr(C)], with the intention to facilitate transmutes. Is there a need for #[repr(C)] instead for some reason?

The other part that is different is that I am (or was) using const instead of fn for defining constants. But I realized from your example that that is problematic in the case where the variants refer to statics (since constants can't refer to statics).

So I can change that part. But that also brings me back to the issue with pattern matching, and why I think helper Cases enum is important for ergonomics, especially even more so because we can't use const here anymore, so we can't even do partial matches.

Generating Cases is more complicated in for the NS_TYPED_EXTENSIBLE_ENUM scenario but there are ways around it. This would be another instance where linkme would be useful, but we could also handle the situation manually, or with some sort of collation done during the header translation.

  • NS_ERROR_ENUM with typedef vs. without typedef

I'm not sure there is an important distinction between these cases, other than being an artifact of the conventions used for writing the headers at various points in time.

Sometimes the headers are not using the most appropriate definitions or contain other issues. For instance, sometimes they are not using the NSErrorDomain type (see here) for seemingly no particular reason. (In this case, it causes us problems because we translate the result with an option).

I think we should be looking at what the Swift analogue is for such cases and use that as a point of reference for our desired result.

In that case, MTLIOError and URLError are defined similarly.

@silvanshade
Copy link
Contributor

silvanshade commented Feb 22, 2023

I've started adding some of the stuff you mentioned in your original post that was missing from the macro translation. I've also added a new test file icrate/tests/enumerations.rs which uses your specific examples alongside the generated output.

And by the way, how are you thinking of handling the AsRef, Borrow, and similar impls? I'm not sure it makes sense to provide those by default since some cases may not support it. I mean, at least so far all the instances of NS_TYPED_ENUM I see us using in icrate are for NSString, but at least in the examples Apple provides for the macros they use some other types.

We could make deriving those optional through some additional arguments perhaps.

In any case, this is how the NS_OPTIONS, NS_ENUM, and NS_TYPED_ENUM examples look now:

NS_OPTIONS(definition)

#[objc(options, repr = usize)]
enum NSSortOptions {
    NSSortConcurrent = 1 << 0,
    NSSortStable = 1 << 4,
}

NS_OPTIONS(generated)

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 = <usize>::ENCODING;
}
unsafe impl objc2::RefEncode for NSSortOptions {
    const ENCODING_REF: objc2::Encoding = <usize>::ENCODING_REF;
}

NS_ENUM(definition)

#[objc(enum, repr = isize)]
enum NSDecodingFailurePolicy {
    NSDecodingFailurePolicyRaiseException,
    NSDecodingFailurePolicySetErrorAndReturn,
}

NS_ENUM(generated)

#[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 = <isize>::ENCODING;
}
unsafe impl objc2::RefEncode for NSDecodingFailurePolicy {
    const ENCODING_REF: objc2::Encoding = <isize>::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<NSDecodingFailurePolicyCases> {
        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<Self> {
        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<NSDecodingFailurePolicy> for isize {
    #[inline]
    fn from(wrapper: NSDecodingFailurePolicy) -> Self {
        wrapper.take()
    }
}
impl core::convert::TryFrom<isize> for NSDecodingFailurePolicy {
    type Error = &'static str;
    fn try_from(raw: isize) -> Result<Self, Self::Error> {
        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<NSDecodingFailurePolicy> for objc2::rc::Id<icrate::Foundation::NSNumber> {
    #[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<Self, Self::Error> {
        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<O: objc2::rc::Ownership> core::convert::TryFrom<objc2::rc::Id<icrate::Foundation::NSNumber, O>>
    for NSDecodingFailurePolicy
{
    type Error = &'static str;
    #[inline]
    fn try_from(
        number: objc2::rc::Id<icrate::Foundation::NSNumber, O>,
    ) -> Result<Self, Self::Error> {
        (&*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::<icrate::Foundation::NSObject>(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,
}

NS_TYPED_ENUM(definition)

#[objc(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 },
}

NS_TYPED_ENUM(generated)

#[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<Self> {
        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<NSStringEncodingDetectionOptionsKey>
    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<Self, Self::Error> {
        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,
}

@madsmtm
Copy link
Owner Author

madsmtm commented Feb 23, 2023

I can see you've spent a lot of time on translation of error enums - I'm quite uncertain about those myself, but I think you've taken a big step in the right direction.

I mostly followed the design and rationale that was laid out here.

Personally, I think we really should have something like this if the intention is truly to allow Rust (through this crate ecosystem) to be an alternative to Swift in terms of ergonomics and feature parity (as far as that is possible).

Did you have another alternative in mind perhaps?

Hmm, perhaps I sounded dismissive there? I agree that we should have something like this!

The main motivation for Cases is to provide a better pattern matching experience through various Rust tooling.

Hmm, I never considered that, I agree that would be quite nice to have! My problems with it is:

  • I think we should provide one "recommended" way of dealing with enums; e.g. I think it's overkill and confusing for the user if we have two types pr. enum. (Also the reason I'm using objc2::rc::Id and objc2::rc::Allocated instead of e.g. NSObjectRef<'_>, NSObjectOwned and NSObjectAllocated).
  • I think people will want to use Cases, which in turn will lead to a lot of unwrap-ing of the Option.
  • Using Cases as it is currently is fundamentally a pit-fall, since it doesn't allow a round-trip - e.g. if you wanted to create a new NSEvent from an old one, using NSEventTypeCases would be wrong.

I think it would be possible to do something like this:

#[non_exhaustive]
enum NSFoo {
    A = 1,
    B = 2,
    #[doc(hidden)]
    __Unknown(usize),
}

impl EncodeConvertArgument for NSFoo { ... }
impl EncodeConvertResult for NSFoo { ... }

Which would allow taking and returning NSFoo from functions (but not in other places like blocks). But then again, that increases the size of the enum, making it no longer zero-cost!
What we really need is for Rust to provide a way to specify niches for the unknown variant.

  • I like the idea of allowing easier access to user info keys, but I think it should rather be a zero-cost solution like this (e.g. instead of fetching everything into a struct, we allow the user to only fetch the keys that are relevant to them):

We have both actually. I generate individual getters first, then the code that fetches the entire dictionary is just implemented in terms of that:

Yeah, but that calls the userInfo selector multiple times (e.g. first inside failing_url, next inside failing_url_string, ...).

I don't know how the translator would work for other frameworks.

I haven't planned for header-translator to ever be public, since I just haven't really seen a use-case for 3rd party Objective-C frameworks? Like, the vast majority of Objective-C code is Apple's, and I suspect the remaining frameworks implement something that is already done better by other Rust crates?
Though I did just now find awesome iOS, so perhaps there are things there that people will want to use? Are there libraries/frameworks that you want to use?

It would be possible to split the userInfo support code generation into a separate macro invocation.

I think they should be separate, mostly because it seems like the error stuff we can autogenerate, while the userInfo stuff we can't?

I am using #[repr(transparent)] instead of #[repr(C)], with the intention to facilitate transmutes. Is there a need for #[repr(C)] instead for some reason?

No, they act exactly the same for structs with one field - the reason I wrote #[repr(C)] was because in the past you couldn't use #[repr(transparent] on zero-sized types (and the reference still documents this as well).

NS_TYPED_ENUM and the issue with pattern matching

Hmm, yeah, performance is a bit less of a concern here, since we're dealing with string comparisons which are comparatively slow anyhow. What you've done seems fairly optimal, though I still dislike that we're emitting two types :/

NS_ERROR_ENUM with typedef vs. without typedef

I think I was mostly thinking of it in terms of what we can autogenerate, and what can't - e.g. URLError is created using a manual Swift wrapper.

AsRef, Borrow, and similar for NS_TYPED_ENUM

I think it's worth it to special-case NSString for these, since it's so common.

@silvanshade
Copy link
Contributor

Hmm, perhaps I sounded dismissive there? I agree that we should have something like this!

Great! And by the way, I do agree there are a number of issues with the current implementation. I'm just hoping that there's a good idea in here somewhere that we can figure out eventually.

Yeah, but that calls the userInfo selector multiple times (e.g. first inside failing_url, next inside failing_url_string, ...).

Okay, I understand what you mean better now.

We could instead specialize the user_info getter during code-generation to avoid multiple calls to the underlying selector and then advise users that if they intend to use multiple fields it would possibly be more efficient to get the whole UserInfo that way versus using individual getters.

That seems like a fairly reasonable compromise I think? (versus tending toward a more complex design)

  • I think we should provide one "recommended" way of dealing with enums; e.g. I think it's overkill and confusing for the user if we have two types pr. enum. (Also the reason I'm using objc2::rc::Id and objc2::rc::Allocated instead of e.g. NSObjectRef<'_>, NSObjectOwned and NSObjectAllocated).

That is a fair point and I understand the comparison. I'm not entirely sure I agree that it's confusing to the same degree though.

Memory management and FFI-related code are difficult due to the complex and very situational semantics involved. The cognitive surface area with regard to API design is also generally much larger.

In comparison, the user doesn't have to understand too much about the struct-wrapping-values and enum-representing-match-cases pairing. The interface is limited: there's only a single method (.cases()) and you really only use it in a single situation, when pattern matching.

  • Using Cases as it is currently is fundamentally a pit-fall, since it doesn't allow a round-trip - e.g. if you wanted to create a new NSEvent from an old one, using NSEventTypeCases would be wrong.

I'm not sure I would expect the user to try to use Cases this way.

Consider the following code snippet:

fn generate_event(event: &NSEvent) -> Option<Id<NSEvent, Shared>> {
    match event.type().cases() {
        Some(NSEventTypeCases::LeftMouseDown) => {
            NSEvent::otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2(
                type: NSEventType::application_defined(),
                ...
            )
        }
        Some(NSEventTypeCases::KeyDown) => {
            ...
        }
        _ => {
            None
        }
    }
}

I suppose there might be a situation where you really want to generate the NSEventType value from the NSEventTypeCases, and it's possible to define a impl From<NSEventTypeCases> for NSEventType for that scenario. I originally included that in the output.

But the more I thought about it, the less likely it seemed that someone would really want to use that in practice, since any time you get the Cases (by calling .cases()), you should already have a value for the respective newtype wrapper at hand.

Maybe there is an example of that you had in mind?

  • I think people will want to use Cases, which in turn will lead to a lot of unwrap-ing of the Option.

Yeah, this one is something I've had concerns about also.

I did consider an alternate design that returned a Result instead of an Option, so that the user could more often use ? to unwrap (or use .cases().ok() if they want the Option), but that complicates the design. But maybe it's worth considering that more seriously?

On the other hand, the user doesn't have to unwrap, in the sense of explicitly writing a separate branch.

It's still possible to write this, which isn't much more verbose:

match value.cases() {
    Some(EnumCases::Var0) => ...,
    Some(EnumCases::Var1) => ...,
    _ => ...,
}

In comparison, this is what it would look like without .cases():

match value {
    _ if value == Enum::var0() => ...,
    _ if value == Enum::var1() => ...,
    _ => ...,
}

This seems more verbose and you also lose the exhaustivity checking and tooling support for filling in the cases.

However, despite advocating for Cases, I'm also not entirely convinced that it's a good idea to include. I just find it slightly bothersome to be able to get part of the way toward replicating what Swift is able to do for these situations, but missing the pattern matching, since pattern matching is a big part of what makes Rust so nice to use.

For the sake of moving this forward, I could disable (or make opt-in), the Cases generation functionality.

Like, the vast majority of Objective-C code is Apple's, and I suspect the remaining frameworks implement something that is already done better by other Rust crates?

That's a good point and I think you are probably right in general.

The one case that I had in mind where it might be relevant beyond this project is for commercial users of the crate. I could certainly imagine a developer with a large amount of code written in Objective-C that might be able to make use of header-translator for their company-specific proprietary libraries.

Are there libraries/frameworks that you want to use?

Not specifically. There are some interesting frameworks like ResearchKit that would be nice to translate but nothing I would probably use for my own projects at this point.

No, they act exactly the same for structs with one field - the reason I wrote #[repr(C)] was because in the past you couldn't use #[repr(transparent] on zero-sized types (and the reference still documents this as well).

Ah, okay, good to know.

I think I was mostly thinking of it in terms of what we can autogenerate, and what can't - e.g. URLError is created using a manual Swift wrapper.

Makes sense. I guess I was imagining that we would probably need to do this already for a lot of the error enums in order to have nice interfaces. But it is important to consider how much of it we could autogenerate for sure.

Relatedly:

I think they should be separate, mostly because it seems like the error stuff we can autogenerate, while the userInfo stuff we can't?

Okay, that makes sense also. I already started working on providing an alternative interface where that macro is separate so it will probably be in the PR branch soon.

I think it's worth it to special-case NSString for these, since it's so common.

Okay, that's doable.

Anyway, thanks for the feedback! You definitely raised a number of good points.

@madsmtm
Copy link
Owner Author

madsmtm commented May 18, 2024

Update on this:

This means that nowadays, the "generate_event" example you gave would look like this:

fn generate_event(event: &NSEvent) -> Option<Id<NSEvent, Shared>> {
    match event.type() {
        NSEventType::LeftMouseDown => {
            NSEvent::otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2(
                type: NSEventType::application_defined(),
                ...
            )
        }
        NSEventType::KeyDown => {
            ...
        }
        _ => {
            None
        }
    }
}

Your IDE would not autofill the cases, doing so is still blocked on better niche support in Rust.

@madsmtm
Copy link
Owner Author

madsmtm commented Jun 13, 2024

Linking a bit of discussion on non-exhaustive C-like enums on Zulip recently, see this topic.

@madsmtm madsmtm changed the title Fix enums and typedefs in icrate Fix enums and typedefs in framework crates Nov 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-framework Affects the framework crates and the translator for them I-unsound A soundness hole
Projects
None yet
Development

No branches or pull requests

2 participants