diff --git a/crates/header-translator/src/availability.rs b/crates/header-translator/src/availability.rs index c37d32092..0a2ec1a80 100644 --- a/crates/header-translator/src/availability.rs +++ b/crates/header-translator/src/availability.rs @@ -139,11 +139,10 @@ impl Availability { _swift, } } -} -impl fmt::Display for Availability { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &self.deprecated { + pub fn is_deprecated(&self) -> bool { + !matches!( + self.deprecated, Versions { ios: None, ios_app_extension: None, @@ -153,7 +152,15 @@ impl fmt::Display for Availability { watchos: None, tvos: None, visionos: None, - } => { + } + ) + } +} + +impl fmt::Display for Availability { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.deprecated { + _ if !self.is_deprecated() => { // Not deprecated } Versions { .. } => { diff --git a/crates/header-translator/src/cache.rs b/crates/header-translator/src/cache.rs index 225cd281d..b13f74cc0 100644 --- a/crates/header-translator/src/cache.rs +++ b/crates/header-translator/src/cache.rs @@ -78,7 +78,10 @@ impl<'a> Cache<'a> { let mut names = BTreeMap::<(ItemIdentifier, String), &mut Method>::new(); for stmt in file.stmts.iter_mut() { match stmt { - Stmt::ClassMethods { + Stmt::ExternMethods { + cls: id, methods, .. + } + | Stmt::ExternCategory { cls: id, methods, .. } | Stmt::ProtocolDecl { id, methods, .. } => { @@ -114,7 +117,10 @@ impl<'a> Cache<'a> { // Add `mainthreadonly` to relevant methods for stmt in file.stmts.iter_mut() { match stmt { - Stmt::ClassMethods { + Stmt::ExternMethods { + cls: id, methods, .. + } + | Stmt::ExternCategory { cls: id, methods, .. } | Stmt::ProtocolDecl { id, methods, .. } => { diff --git a/crates/header-translator/src/method.rs b/crates/header-translator/src/method.rs index 4941ceabd..1c8556b2a 100644 --- a/crates/header-translator/src/method.rs +++ b/crates/header-translator/src/method.rs @@ -245,16 +245,16 @@ impl MemoryManagement { pub struct Method { pub selector: String, pub fn_name: String, - availability: Availability, + pub availability: Availability, features: Features, pub is_class: bool, - is_optional_protocol: bool, + is_optional: bool, memory_management: MemoryManagement, pub(crate) arguments: Vec<(String, Ty)>, pub result_type: Ty, safe: bool, mutating: bool, - is_protocol: bool, + is_pub: bool, // Thread-safe, even on main-thread only (@MainActor/@UIActor) classes non_isolated: bool, pub(crate) mainthreadonly: bool, @@ -333,7 +333,7 @@ impl<'tu> PartialMethod<'tu> { self, data: MethodData, parent_is_mutable: bool, - parent_is_protocol: bool, + is_pub: bool, implied_features: impl IntoIterator, context: &Context<'_>, ) -> Option<(bool, Method)> { @@ -479,7 +479,7 @@ impl<'tu> PartialMethod<'tu> { availability, features, is_class, - is_optional_protocol: entity.is_objc_optional(), + is_optional: entity.is_objc_optional(), memory_management, arguments, result_type, @@ -488,7 +488,7 @@ impl<'tu> PartialMethod<'tu> { // since immutable methods are usually either declared on an // immutable subclass, or as a property. mutating: data.mutating.unwrap_or(parent_is_mutable), - is_protocol: parent_is_protocol, + is_pub, non_isolated: modifiers.non_isolated, mainthreadonly: modifiers.mainthreadonly, }, @@ -513,7 +513,7 @@ impl PartialProperty<'_> { getter_data: MethodData, setter_data: Option, parent_is_mutable: bool, - parent_is_protocol: bool, + is_pub: bool, implied_features: impl IntoIterator + Clone, context: &Context<'_>, ) -> (Option, Option) { @@ -566,7 +566,7 @@ impl PartialProperty<'_> { availability: availability.clone(), features: features.clone(), is_class, - is_optional_protocol: entity.is_objc_optional(), + is_optional: entity.is_objc_optional(), memory_management, arguments: Vec::new(), result_type: ty, @@ -574,7 +574,7 @@ impl PartialProperty<'_> { // Getters are usually not mutable, even if the class itself // is, so let's default to immutable. mutating: getter_data.mutating.unwrap_or(false), - is_protocol: parent_is_protocol, + is_pub, non_isolated: modifiers.non_isolated, mainthreadonly: modifiers.mainthreadonly, }) @@ -608,14 +608,14 @@ impl PartialProperty<'_> { availability, features, is_class, - is_optional_protocol: entity.is_objc_optional(), + is_optional: entity.is_objc_optional(), memory_management, arguments: vec![(name, ty)], result_type: Ty::VOID_RESULT, safe: !setter_data.unsafe_, // Setters are usually mutable if the class itself is. mutating: setter_data.mutating.unwrap_or(parent_is_mutable), - is_protocol: parent_is_protocol, + is_pub, non_isolated: modifiers.non_isolated, mainthreadonly: modifiers.mainthreadonly, }) @@ -659,7 +659,7 @@ impl fmt::Display for Method { write!(f, "{}", self.features.cfg_gate_ln())?; write!(f, "{}", self.availability)?; - if self.is_optional_protocol { + if self.is_optional { writeln!(f, " #[optional]")?; } @@ -683,7 +683,7 @@ impl fmt::Display for Method { // write!(f, " ")?; - if !self.is_protocol { + if self.is_pub { write!(f, "pub ")?; } diff --git a/crates/header-translator/src/stmt.rs b/crates/header-translator/src/stmt.rs index de6d20118..364b36d21 100644 --- a/crates/header-translator/src/stmt.rs +++ b/crates/header-translator/src/stmt.rs @@ -169,7 +169,7 @@ fn parse_methods( entity: &Entity<'_>, get_data: impl Fn(&str) -> MethodData, is_mutable: bool, - is_protocol: bool, + is_pub: bool, implied_features: &[ItemIdentifier], context: &Context<'_>, ) -> (Vec, Vec) { @@ -188,7 +188,7 @@ fn parse_methods( if !properties.remove(&(partial.is_class, partial.selector.clone())) { let data = get_data(&partial.selector); if let Some((designated_initializer, method)) = - partial.parse(data, is_mutable, is_protocol, implied_features, context) + partial.parse(data, is_mutable, is_pub, implied_features, context) { if designated_initializer { designated_initializers.push(method.selector.clone()); @@ -214,7 +214,7 @@ fn parse_methods( getter_data, setter_data, is_mutable, - is_protocol, + is_pub, implied_features, context, ); @@ -349,15 +349,6 @@ impl fmt::Display for Mutability { } } -#[derive(Debug, Clone, PartialEq)] -pub enum MethodSource { - Class, - Superclass(ItemIdentifier), - /// Some categories don't have a name (e.g. on `NSClipView`). - Category(ItemIdentifier>), - SuperclassCategory(ItemIdentifier, ItemIdentifier>), -} - #[derive(Debug, Clone, PartialEq)] pub enum Stmt { /// @interface name: superclass @@ -377,11 +368,22 @@ pub enum Stmt { /// @interface class_name (category_name) /// -> /// extern_methods! - ClassMethods { - source: MethodSource, + ExternMethods { availability: Availability, cls: ItemIdentifier, + source_superclass: Option, cls_generics: Vec, + category_name: Option, + methods: Vec, + }, + /// @interface class_name (category_name) + /// -> + /// extern_category! + ExternCategory { + id: ItemIdentifier, + actual_name: Option, + availability: Availability, + cls: ItemIdentifier, methods: Vec, }, /// @protocol name @@ -539,7 +541,7 @@ impl Stmt { |name| ClassData::get_method_data(data, name), data.map(|data| data.mutability.is_mutable()) .unwrap_or(false), - false, + true, &implied_features, context, ); @@ -595,7 +597,7 @@ impl Stmt { data.map(|data| data.mutability.is_mutable()) .or(superclass_data.map(|data| data.mutability.is_mutable())) .unwrap_or(false), - false, + true, // Even though the methods are originally defined // elsewhere, we're going to be _emitting_ them on // the current class, so that's also where we're @@ -610,22 +612,24 @@ impl Stmt { if methods.is_empty() { None } else { - Some(Self::ClassMethods { - source: MethodSource::Superclass(superclass_id.clone()), + Some(Self::ExternMethods { availability: Availability::parse(entity, context), cls: id.clone(), + source_superclass: Some(superclass_id.clone()), cls_generics: generics.clone(), + category_name: None, methods, }) } }) .collect(); - let methods = Self::ClassMethods { - source: MethodSource::Class, + let methods = Self::ExternMethods { availability: availability.clone(), cls: id.clone(), + source_superclass: None, cls_generics: generics.clone(), + category_name: None, methods, }; @@ -702,15 +706,97 @@ impl Stmt { .unwrap_or_default(); protocols.retain(|protocol| !skipped_protocols.contains(&protocol.name)); - let (methods, designated_initializers) = parse_methods( - entity, - |name| ClassData::get_method_data(data, name), - data.map(|data| data.mutability.is_mutable()) - .unwrap_or(false), - false, - &get_class_implied_features(&cls_entity, context), - context, - ); + // For ease-of-use, if the category is defined in the same + // library as the class, we just emit it as `extern_methods!`. + let output = if cls.library == category.library { + let (methods, designated_initializers) = parse_methods( + entity, + |name| ClassData::get_method_data(data, name), + data.map(|data| data.mutability.is_mutable()) + .unwrap_or(false), + true, + &get_class_implied_features(&cls_entity, context), + context, + ); + + if !designated_initializers.is_empty() { + warn!( + ?designated_initializers, + "designated initializer in category" + ); + } + + Some(Self::ExternMethods { + availability: availability.clone(), + cls: cls.clone(), + source_superclass: None, + cls_generics: generics.clone(), + category_name: category.name.clone(), + methods, + }) + } else { + if !generics.is_empty() { + panic!("external category: cannot handle generics"); + } + + // Rough heuristic to determine category name. + // + // Note: There isn't really a good way to do this, as + // category names are not part of the public API in + // Objective-C. + let id = category.clone().map_name(|name| match name { + None => format!("{}Category", cls.name), + Some(name) => { + if name.contains(&cls.name) + || name.contains(&cls.name.replace("Mutable", "")) + { + name.clone() + } else { + format!("{}{}", cls.name, name) + } + } + }); + + let (methods, designated_initializers) = parse_methods( + entity, + |name| ClassData::get_method_data(data, name), + false, + false, + &get_class_implied_features(&cls_entity, context), + context, + ); + + if !designated_initializers.is_empty() { + warn!( + ?designated_initializers, + "designated initializer in category" + ); + } + + // Categories are often used to implement protocols for a + // type, so as an optimization let's not emit empty + // external declarations. + // + // Additionally, if all methods are deprecated, then there + // really isn't a need for us to emit the category + // (especially on NSObject, as that just adds a bunch of + // clutter). + if methods + .iter() + .all(|method| method.availability.is_deprecated()) + { + None + } else { + Some(Self::ExternCategory { + id, + actual_name: category.name.clone(), + availability: availability.clone(), + cls: cls.clone(), + methods, + }) + } + } + .into_iter(); let (sendable, mainthreadonly) = parse_attributes(entity, context); if let Some(sendable) = sendable { @@ -720,13 +806,6 @@ impl Stmt { error!("@UIActor on category"); } - if !designated_initializers.is_empty() { - warn!( - ?designated_initializers, - "designated initializer in category" - ) - } - let subclass_methods = if let Mutability::ImmutableWithMutableSubclass(subclass) = data.map(|data| data.mutability.clone()).unwrap_or_default() { @@ -743,7 +822,7 @@ impl Stmt { data.map(|data| data.mutability.is_mutable()) .or(subclass_data.map(|data| data.mutability.is_mutable())) .unwrap_or(false), - false, + true, &get_class_implied_features(&cls_entity, context), context, ); @@ -751,14 +830,15 @@ impl Stmt { if methods.is_empty() { None } else { - Some(Self::ClassMethods { - source: MethodSource::SuperclassCategory(cls.clone(), category.clone()), + Some(Self::ExternMethods { + source_superclass: Some(cls.clone()), // Assume that immutable/mutable pairs have the // same availability. availability: availability.clone(), cls: subclass, // And that they have the same amount of generics. cls_generics: generics.clone(), + category_name: category.name.clone(), methods, }) } @@ -766,21 +846,15 @@ impl Stmt { None }; - iter::once(Self::ClassMethods { - source: MethodSource::Category(category), - availability: availability.clone(), - cls: cls.clone(), - cls_generics: generics.clone(), - methods, - }) - .chain(subclass_methods) - .chain(protocols.into_iter().map(|protocol| Self::ProtocolImpl { - cls: cls.clone(), - generics: generics.clone(), - availability: availability.clone(), - protocol, - })) - .collect() + output + .chain(subclass_methods) + .chain(protocols.into_iter().map(|protocol| Self::ProtocolImpl { + cls: cls.clone(), + generics: generics.clone(), + availability: availability.clone(), + protocol, + })) + .collect() } EntityKind::ObjCProtocolDecl => { let actual_id = ItemIdentifier::new(entity, context); @@ -808,7 +882,7 @@ impl Stmt { .unwrap_or_default() }, false, - true, + false, &[], context, ); @@ -1240,11 +1314,11 @@ impl Stmt { pub fn compare(&self, other: &Self) { if self != other { if let ( - Self::ClassMethods { + Self::ExternMethods { methods: self_methods, .. }, - Self::ClassMethods { + Self::ExternMethods { methods: other_methods, .. }, @@ -1268,6 +1342,7 @@ impl Stmt { let mut features = Features::new(); match self { Stmt::ClassDecl { id, .. } => features.add_item(id), + Stmt::ExternCategory { cls, .. } => features.add_item(cls), Stmt::FnDecl { arguments, result_type, @@ -1289,19 +1364,20 @@ impl Stmt { if *skipped { None } else { - Some(&*id.name) + Some(&id.name) } } - Stmt::ClassMethods { .. } => None, - Stmt::ProtocolDecl { id, .. } => Some(&*id.name), + Stmt::ExternMethods { .. } => None, + Stmt::ExternCategory { id, .. } => Some(&id.name), + Stmt::ProtocolDecl { id, .. } => Some(&id.name), Stmt::ProtocolImpl { .. } => None, - Stmt::StructDecl { id, .. } => Some(&*id.name), + Stmt::StructDecl { id, .. } => Some(&id.name), Stmt::EnumDecl { id, .. } => id.name.as_deref(), - Stmt::VarDecl { id, .. } => Some(&*id.name), + Stmt::VarDecl { id, .. } => Some(&id.name), Stmt::FnDecl { id, body, .. } if body.is_none() => Some(&*id.name), // TODO Stmt::FnDecl { .. } => None, - Stmt::AliasDecl { id, .. } => Some(&*id.name), + Stmt::AliasDecl { id, .. } => Some(&id.name), } } @@ -1494,40 +1570,27 @@ impl fmt::Display for Stmt { writeln!(f, "unsafe impl Sync for {} {{}}", id.name)?; } } - Self::ClassMethods { - source, - // TODO: Output `#[deprecated]` only on categories + Self::ExternMethods { availability: _, cls, + source_superclass, cls_generics, + category_name, methods, } => { writeln!(f, "extern_methods!(")?; - match source { - MethodSource::Class => {} - MethodSource::Superclass(superclass) => { - writeln!( - f, - " /// Methods declared on superclass `{}`", - superclass.name - )?; - } - MethodSource::Category(category) => { - if let Some(category_name) = &category.name { - writeln!(f, " /// {category_name}")?; - } - } - MethodSource::SuperclassCategory(superclass, category) => { - writeln!( - f, - " /// Methods declared on superclass `{}`", - superclass.name - )?; - if let Some(category_name) = &category.name { - writeln!(f, " ///")?; - writeln!(f, " /// {category_name}")?; - } + if let Some(source_superclass) = source_superclass { + writeln!( + f, + " /// Methods declared on superclass `{}`", + source_superclass.name + )?; + if let Some(category_name) = category_name { + writeln!(f, "///")?; + writeln!(f, " /// {category_name}")?; } + } else if let Some(category_name) = category_name { + writeln!(f, " /// {category_name}")?; } write!(f, " {}", Feature::new(cls).cfg_gate_ln())?; // TODO: Add ?Sized here once `extern_methods!` supports it. @@ -1562,6 +1625,46 @@ impl fmt::Display for Stmt { writeln!(f, "}}")?; } } + Self::ExternCategory { + id, + actual_name, + availability, + cls, + methods, + } => { + writeln!(f, "extern_category!(")?; + + if let Some(actual_name) = actual_name { + if *actual_name != id.name { + writeln!(f, " /// Category \"{actual_name}\" on [`{}`].", cls.name)?; + writeln!(f, " #[doc(alias = \"{actual_name}\")]")?; + } else { + writeln!(f, " /// Category on [`{}`].", cls.name)?; + } + } else { + writeln!(f, " /// Category on [`{}`].", cls.name)?; + } + + write!(f, " {}", Feature::new(cls).cfg_gate_ln())?; + write!(f, "{availability}")?; + writeln!(f, " pub unsafe trait {} {{", id.name)?; + for method in methods { + writeln!(f, "{method}")?; + } + writeln!(f, " }}")?; + + writeln!(f)?; + + write!(f, " {}", Feature::new(cls).cfg_gate_ln())?; + writeln!( + f, + " unsafe impl {} for {} {{}}", + id.name, + cls.path_in_relation_to(id), + )?; + + writeln!(f, ");")?; + } Self::ProtocolImpl { cls, generics, diff --git a/crates/header-translator/translation-config.toml b/crates/header-translator/translation-config.toml index c6bda9ee5..f833599ed 100644 --- a/crates/header-translator/translation-config.toml +++ b/crates/header-translator/translation-config.toml @@ -603,11 +603,9 @@ skipped = true [class.NSBundle.methods."localizedAttributedStringForKey:value:table:"] skipped = true -# Root classes, defined in `objc2` for now +# Root class, defined in `objc2` for now [class.NSProxy] skipped = true -[class.NSObject] -skipped = true # Also ignore categories on NSObject for now [protocol.NSObject] renamed = "NSObjectProtocol" @@ -1385,12 +1383,6 @@ NSCancelButton = { skipped = true } NSFileHandlingPanelCancelButton = { skipped = true } NSFileHandlingPanelOKButton = { skipped = true } -# Categories for classes defined in other frameworks -[class.CIImage] -skipped = true -[class.CIColor] -skipped = true - # Different definitions depending on target [enum.NSImageResizingMode] skipped = true diff --git a/crates/icrate/CHANGELOG.md b/crates/icrate/CHANGELOG.md index e5bb6ecf2..ad7a3e6f1 100644 --- a/crates/icrate/CHANGELOG.md +++ b/crates/icrate/CHANGELOG.md @@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## icrate Unreleased - YYYY-MM-DD +### Added +* Added `NSObject` categories, notably those used by key-value coding and + observing. + ### Changed * Updated SDK from Xcode 15.0.1 to 15.2. @@ -19,10 +23,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Marked `NSView::isFlipped`, `NSView::convertRect_toView`, `NSWindow::convertRectToScreen` and `NSWindow::convertPointFromScreen` as safe. +* **BREAKING**: Changed how categories are handled; now, when a library has + defined methods on a class defined in a different framework, a helper trait + is output with the methods, instead of the methods being implemented + directly on the type. ### Deprecated -* Deprecated `MainThreadMarker::run_on_main`, use the new free-standing function - `run_on_main` instead. +* Deprecated `Foundation::MainThreadMarker::run_on_main`, use the new + free-standing function `Foundation::run_on_main` instead. ### Removed * Removed private functionality in the `Speech` framework. This was never diff --git a/crates/icrate/src/common.rs b/crates/icrate/src/common.rs index 641f0fd61..978adf832 100644 --- a/crates/icrate/src/common.rs +++ b/crates/icrate/src/common.rs @@ -25,8 +25,8 @@ pub(crate) use objc2::runtime::{AnyClass, AnyObject, Bool, Sel}; pub(crate) use objc2::runtime::{NSObject, NSObjectProtocol, ProtocolObject}; #[cfg(feature = "objective-c")] pub(crate) use objc2::{ - __inner_extern_class, extern_class, extern_methods, extern_protocol, ClassType, Message, - ProtocolType, + __inner_extern_class, extern_category, extern_class, extern_methods, extern_protocol, + ClassType, Message, ProtocolType, }; #[cfg(feature = "block")] diff --git a/crates/icrate/src/generated b/crates/icrate/src/generated index bc86f1078..2d15f0886 160000 --- a/crates/icrate/src/generated +++ b/crates/icrate/src/generated @@ -1 +1 @@ -Subproject commit bc86f10788a430ebbf34800e116a0490d87b1c3b +Subproject commit 2d15f08867074d8ed601f7c280f70b90cf676487 diff --git a/crates/objc2/src/macros/extern_category.rs b/crates/objc2/src/macros/extern_category.rs new file mode 100644 index 000000000..408a15060 --- /dev/null +++ b/crates/objc2/src/macros/extern_category.rs @@ -0,0 +1,33 @@ +/// Not yet public API. +// +// Note: While this is not public, it is still a breaking change to change +// the API for this, since `icrate` relies on it. +#[doc(hidden)] +#[macro_export] +macro_rules! extern_category { + ( + $(#[$m:meta])* + $v:vis unsafe trait $name:ident { + $($methods:tt)* + } + + $(#[$impl_m:meta])* + unsafe impl $category:ident for $ty:ty {} + ) => { + $(#[$m])* + $v unsafe trait $name: ClassType { + // TODO: Do this better + $crate::__extern_protocol_rewrite_methods! { + $($methods)* + } + + #[doc(hidden)] + const __UNSAFE_INNER: (); + } + + $(#[$impl_m])* + unsafe impl $category for $ty { + const __UNSAFE_INNER: () = (); + } + }; +} diff --git a/crates/objc2/src/macros/mod.rs b/crates/objc2/src/macros/mod.rs index b82a19052..5321f9d05 100644 --- a/crates/objc2/src/macros/mod.rs +++ b/crates/objc2/src/macros/mod.rs @@ -3,6 +3,7 @@ mod __method_msg_send; mod __msg_send_parse; mod __rewrite_self_param; mod declare_class; +mod extern_category; mod extern_class; mod extern_methods; mod extern_protocol;