diff --git a/crates/icrate/CHANGELOG.md b/crates/icrate/CHANGELOG.md index a2ef383b4..01457813f 100644 --- a/crates/icrate/CHANGELOG.md +++ b/crates/icrate/CHANGELOG.md @@ -18,6 +18,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Added missing `NSCopying` and `NSMutableCopying` zone methods. * Added `Eq` and `Ord` implementations for `NSNumber`, since its handling of floating point values allows it. +* Added `NS[Mutable]Dictionary::from_id_slice` and + `NS[Mutable]Dictionary::from_slice`. +* Added `NSMutableDictionary::insert` and `NSMutableSet::insert` which can + be more efficient than the previous insertion methods. ### Changed * Moved the `ns_string!` macro to `icrate::Foundation::ns_string`. The old @@ -44,6 +48,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). // Do something with `app` and `view` ``` * **BREAKING**: Changed the `NSApp` static to be a function taking `MainThreadMarker`. +* **BREAKING**: Renamed `NS[Mutable]Dictionary::from_keys_and_objects` to + `NS[Mutable]Dictionary::from_vec`. +* **BREAKING**: Renamed `NSMutableDictionary::insert` and + `NSMutableSet::insert` to `insert_id`. ### Removed * **BREAKING**: Removed the `MainThreadMarker` argument from the closure @@ -51,6 +59,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * **BREAKING**: Removed `Foundation::CopyHelper` since it is superseded by `objc2::mutability::CounterpartOrSelf` +### Fixed +* **BREAKING**: Added `Eq + Hash` requirement on most `NSDictionary` and + `NSSet` methods, thereby making sure that the types are actually correct + to use in such hashing collections. +* **BREAKING**: Added `HasStableHash` requirement on `NSDictionary` and + `NSSet` creation methods, fixing a long-standing soundess issue. + ## icrate 0.0.4 - 2023-07-31 diff --git a/crates/icrate/examples/basic_usage.rs b/crates/icrate/examples/basic_usage.rs index 711273a10..7b40c67b4 100644 --- a/crates/icrate/examples/basic_usage.rs +++ b/crates/icrate/examples/basic_usage.rs @@ -36,8 +36,8 @@ fn main() { // Create a dictionary mapping strings to objects let keys = &[string]; - let vals = vec![obj]; - let dict = NSDictionary::from_keys_and_objects(keys, vals); + let objects = &[obj]; + let dict = NSDictionary::from_id_slice(keys, objects); println!("{:?}", dict.get(string)); println!("{}", dict.len()); } diff --git a/crates/icrate/src/additions/Foundation/dictionary.rs b/crates/icrate/src/additions/Foundation/dictionary.rs index 1d752b3da..89072d0f6 100644 --- a/crates/icrate/src/additions/Foundation/dictionary.rs +++ b/crates/icrate/src/additions/Foundation/dictionary.rs @@ -1,14 +1,16 @@ //! Utilities for the `NSDictionary` and `NSMutableDictionary` classes. #![cfg(feature = "Foundation_NSDictionary")] use alloc::vec::Vec; +use core::borrow::Borrow; use core::cmp::min; use core::fmt; +use core::hash::Hash; use core::mem; use core::ops::{Index, IndexMut}; use core::panic::{RefUnwindSafe, UnwindSafe}; use core::ptr::{self, NonNull}; -use objc2::mutability::{CounterpartOrSelf, IsMutable}; +use objc2::mutability::{CounterpartOrSelf, HasStableHash, IsIdCloneable, IsMutable, IsRetainable}; use super::iter; use super::util; @@ -17,47 +19,134 @@ use crate::common::*; use crate::Foundation::NSMutableDictionary; use crate::Foundation::{self, NSCopying, NSDictionary}; -impl NSDictionary { - pub fn from_keys_and_objects(keys: &[&T], mut vals: Vec>) -> Id +impl NSDictionary { + pub fn from_vec(keys: &[&Q], mut objects: Vec>) -> Id + where + Q: Message + NSCopying + CounterpartOrSelf, + { + // Find the minimum of the two provided lengths, to ensure that we + // don't read too far in one of the buffers. + // + // Note: We could also have chosen to just panic here, either would + // be fine. + let count = min(keys.len(), objects.len()); + + let keys: *mut NonNull = util::ref_ptr_cast_const(keys.as_ptr()); + let keys: *mut NonNull = keys.cast(); + let objects = util::id_ptr_cast(objects.as_mut_ptr()); + + // SAFETY: + // - The objects are valid, similar reasoning as `NSArray::from_vec`. + // + // - The key must not be mutated, as that may cause the hash value to + // change, which is unsound as stated in: + // https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/ObjectMutability/ObjectMutability.html#//apple_ref/doc/uid/TP40010810-CH5-SW69 + // + // The dictionary always copies its keys, which is why we require + // `NSCopying` and use `CounterpartOrSelf` on all input data - we + // want to ensure that it is very clear that it's not actually + // `NSMutableString` that is being stored, but `NSString`. + // + // But that is not by itself enough to verify that the key does not + // still contain interior mutable objects (e.g. if the copy was only + // a shallow copy), which is why we also require `HasStableHash`. + // + // - The length is lower than or equal to the length of the two arrays. + unsafe { Self::initWithObjects_forKeys_count(Self::alloc(), objects, keys, count) } + } + + pub fn from_id_slice(keys: &[&Q], objects: &[Id]) -> Id where - T: ClassType + NSCopying + CounterpartOrSelf, + Q: Message + NSCopying + CounterpartOrSelf, + V: IsIdCloneable, { - let count = min(keys.len(), vals.len()); + let count = min(keys.len(), objects.len()); - let keys: *mut NonNull = util::ref_ptr_cast_const(keys.as_ptr()); + let keys: *mut NonNull = util::ref_ptr_cast_const(keys.as_ptr()); let keys: *mut NonNull = keys.cast(); - let vals = util::id_ptr_cast(vals.as_mut_ptr()); + let objects = util::id_ptr_cast_const(objects.as_ptr()); - unsafe { Self::initWithObjects_forKeys_count(Self::alloc(), vals, keys, count) } + // SAFETY: See `NSDictionary::from_vec` and `NSArray::from_id_slice`. + unsafe { Self::initWithObjects_forKeys_count(Self::alloc(), objects, keys, count) } + } + + pub fn from_slice(keys: &[&Q], objects: &[&V]) -> Id + where + Q: Message + NSCopying + CounterpartOrSelf, + V: IsRetainable, + { + let count = min(keys.len(), objects.len()); + + let keys: *mut NonNull = util::ref_ptr_cast_const(keys.as_ptr()); + let keys: *mut NonNull = keys.cast(); + let objects = util::ref_ptr_cast_const(objects.as_ptr()); + + // SAFETY: See `NSDictionary::from_vec` and `NSArray::from_slice`. + unsafe { Self::initWithObjects_forKeys_count(Self::alloc(), objects, keys, count) } } } #[cfg(feature = "Foundation_NSMutableDictionary")] -impl NSMutableDictionary { - pub fn from_keys_and_objects(keys: &[&T], mut vals: Vec>) -> Id +impl NSMutableDictionary { + pub fn from_vec(keys: &[&Q], mut objects: Vec>) -> Id where - T: ClassType + NSCopying + CounterpartOrSelf, + Q: Message + NSCopying + CounterpartOrSelf, { - let count = min(keys.len(), vals.len()); + let count = min(keys.len(), objects.len()); - let keys: *mut NonNull = util::ref_ptr_cast_const(keys.as_ptr()); + let keys: *mut NonNull = util::ref_ptr_cast_const(keys.as_ptr()); let keys: *mut NonNull = keys.cast(); - let vals = util::id_ptr_cast(vals.as_mut_ptr()); + let objects = util::id_ptr_cast(objects.as_mut_ptr()); - unsafe { Self::initWithObjects_forKeys_count(Self::alloc(), vals, keys, count) } + // SAFETY: See `NSDictionary::from_vec` + unsafe { Self::initWithObjects_forKeys_count(Self::alloc(), objects, keys, count) } } -} -extern_methods!( - unsafe impl NSDictionary { - pub fn len(&self) -> usize { - self.count() - } + pub fn from_id_slice(keys: &[&Q], objects: &[Id]) -> Id + where + Q: Message + NSCopying + CounterpartOrSelf, + V: IsIdCloneable, + { + let count = min(keys.len(), objects.len()); - pub fn is_empty(&self) -> bool { - self.len() == 0 - } + let keys: *mut NonNull = util::ref_ptr_cast_const(keys.as_ptr()); + let keys: *mut NonNull = keys.cast(); + let objects = util::id_ptr_cast_const(objects.as_ptr()); + + // SAFETY: See `NSDictionary::from_vec` and `NSArray::from_id_slice`. + unsafe { Self::initWithObjects_forKeys_count(Self::alloc(), objects, keys, count) } + } + + pub fn from_slice(keys: &[&Q], objects: &[&V]) -> Id + where + Q: Message + NSCopying + CounterpartOrSelf, + V: IsRetainable, + { + let count = min(keys.len(), objects.len()); + + let keys: *mut NonNull = util::ref_ptr_cast_const(keys.as_ptr()); + let keys: *mut NonNull = keys.cast(); + let objects = util::ref_ptr_cast_const(objects.as_ptr()); + // SAFETY: See `NSDictionary::from_vec` and `NSArray::from_slice`. + unsafe { Self::initWithObjects_forKeys_count(Self::alloc(), objects, keys, count) } + } +} + +// Note: We'd like to make getter methods take `K: Borrow` like +// `std::collections::HashMap`, so that e.g. +// `NSDictionary` could take a `&NSObject` as input, +// and still make that work since `NSString` borrows to `NSObject`. +// +// But we can't really, at least not with extra `unsafe` / an extra +// trait, since we don't control how the comparisons happen. +// +// The most useful alternative would probably be to take +// `impl AsRef`, but type deref to their superclass anyhow, let's +// just use a simple normal reference. + +extern_methods!( + unsafe impl NSDictionary { #[doc(alias = "objectForKey:")] #[method(objectForKey:)] pub fn get(&self, key: &K) -> Option<&V>; @@ -93,7 +182,7 @@ extern_methods!( /// use icrate::Foundation::{ns_string, NSMutableDictionary, NSMutableString, NSString}; /// /// let mut dict = NSMutableDictionary::new(); - /// dict.insert(NSString::from_str("one"), NSMutableString::new()); + /// dict.insert_id(ns_string!("one"), NSMutableString::new()); /// println!("{:?}", dict.get_mut(ns_string!("one"))); /// ``` #[doc(alias = "objectForKey:")] @@ -105,6 +194,14 @@ extern_methods!( ); impl NSDictionary { + pub fn len(&self) -> usize { + self.count() + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + #[doc(alias = "getObjects:andKeys:")] pub fn keys_vec(&self) -> Vec<&K> { let len = self.len(); @@ -117,8 +214,7 @@ impl NSDictionary { } } - // We don't provide `keys_mut_vec`, since keys are expected to be - // immutable. + // We don't provide `keys_mut_vec`, since keys are immutable. #[doc(alias = "getObjects:andKeys:")] pub fn values_vec(&self) -> Vec<&V> { @@ -150,10 +246,10 @@ impl NSDictionary { )), doc = "```ignore" )] - /// use icrate::Foundation::{NSMutableDictionary, NSMutableString, NSString}; + /// use icrate::Foundation::{ns_string, NSMutableDictionary, NSMutableString, NSString}; /// /// let mut dict = NSMutableDictionary::new(); - /// dict.insert(NSString::from_str("one"), NSMutableString::from_str("two")); + /// dict.insert_id(ns_string!("one"), NSMutableString::from_str("two")); /// for val in dict.values_mut() { /// println!("{:?}", val); /// } @@ -186,10 +282,35 @@ impl NSDictionary { (mem::transmute(keys), mem::transmute(objs)) } } + + /// Returns an [`NSArray`] containing the dictionary's values. + /// + /// [`NSArray`]: crate::Foundation::NSArray + /// + /// + /// # Examples + /// + /// ``` + /// use icrate::Foundation::{ns_string, NSMutableDictionary, NSObject, NSString}; + /// + /// let mut dict = NSMutableDictionary::new(); + /// dict.insert_id(ns_string!("one"), NSObject::new()); + /// let array = dict.to_array(); + /// assert_eq!(array.len(), 1); + /// ``` + #[cfg(feature = "Foundation_NSArray")] + pub fn to_array(&self) -> Id> + where + V: IsIdCloneable, + { + // SAFETY: The elements are retainable behind `Id`, so getting + // another reference to them (via. `NSArray`) is sound. + unsafe { self.allValues() } + } } #[cfg(feature = "Foundation_NSMutableDictionary")] -impl NSMutableDictionary { +impl NSMutableDictionary { /// Inserts a key-value pair into the dictionary. /// /// If the dictionary did not have this key present, None is returned. @@ -199,69 +320,93 @@ impl NSMutableDictionary { /// # Examples /// /// ``` - /// use icrate::Foundation::{NSMutableDictionary, NSObject, NSString}; + /// use icrate::Foundation::{NSMutableDictionary, NSObject, ns_string}; /// /// let mut dict = NSMutableDictionary::new(); - /// dict.insert(NSString::from_str("one"), NSObject::new()); + /// dict.insert_id(ns_string!("one"), NSObject::new()); /// ``` #[doc(alias = "setObject:forKey:")] - pub fn insert(&mut self, key: Id, value: Id) -> Option> { + pub fn insert_id(&mut self, key: &K, value: Id) -> Option> + where + K: NSCopying + CounterpartOrSelf, + { // SAFETY: We remove the object from the dictionary below let old_obj = self - .get(&key) + .get(key) .map(|old_obj| unsafe { util::mutable_collection_retain_removed_id(old_obj) }); - // SAFETY: It is always safe to transmute an `Id` to `AnyObject`. - let key: Id = unsafe { Id::cast(key) }; - // SAFETY: We have ownership over both the key and the value. - unsafe { self.setObject_forKey(&value, &key) }; + // SAFETY: It is always safe to transmute an `&T` where `T: Message` + // to `&AnyObject`. + let key: NonNull = NonNull::from(key); + let key: NonNull = key.cast(); + let key: &AnyObject = unsafe { key.as_ref() }; + // SAFETY: The key is NSCopying (see `NSDictionary::from_vec`), and we + // have ownership over the value. + unsafe { self.setObject_forKey(&value, key) }; old_obj } - /// Removes a key from the dictionary, returning the value at the key - /// if the key was previously in the dictionary. + /// Inserts a key-value pair into the dictionary. + /// + /// If the dictionary did not have this key present, None is returned. + /// If the dictionary did have this key present, the value is updated, + /// and the old value is returned. /// /// # Examples /// /// ``` - /// use icrate::Foundation::{ns_string, NSMutableDictionary, NSObject, NSString}; + /// use icrate::Foundation::{ns_string, NSCopying, NSMutableDictionary}; /// /// let mut dict = NSMutableDictionary::new(); - /// dict.insert(NSString::from_str("one"), NSObject::new()); - /// dict.remove(ns_string!("one")); - /// assert!(dict.is_empty()); + /// dict.insert_id(ns_string!("key"), ns_string!("value").copy()); /// ``` - #[doc(alias = "removeObjectForKey:")] - pub fn remove(&mut self, key: &K) -> Option> { + #[doc(alias = "setObject:forKey:")] + pub fn insert(&mut self, key: &K, value: &V) -> Option> + where + K: NSCopying + CounterpartOrSelf, + V: IsRetainable, + { // SAFETY: We remove the object from the dictionary below let old_obj = self .get(key) .map(|old_obj| unsafe { util::mutable_collection_retain_removed_id(old_obj) }); - self.removeObjectForKey(key); + + // SAFETY: It is always safe to transmute an `&T` where `T: Message` + // to `&AnyObject`. + let key: NonNull = NonNull::from(key); + let key: NonNull = key.cast(); + let key: &AnyObject = unsafe { key.as_ref() }; + // SAFETY: The key is NSCopying (see `NSDictionary::from_vec`), and + // the value is `IsRetainable` (and hence safe for the collection to + // retain). + unsafe { self.setObject_forKey(value, key) }; old_obj } - /// Returns an [`NSArray`] containing the dictionary's values, - /// consuming the dictionary. - /// - /// [`NSArray`]: crate::Foundation::NSArray - /// + /// Removes a key from the dictionary, returning the value at the key + /// if the key was previously in the dictionary. /// /// # Examples /// /// ``` - /// use icrate::Foundation::{NSMutableDictionary, NSObject, NSString}; + /// use icrate::Foundation::{ns_string, NSMutableDictionary, NSObject}; /// /// let mut dict = NSMutableDictionary::new(); - /// dict.insert(NSString::from_str("one"), NSObject::new()); - /// let array = NSMutableDictionary::into_values_array(dict); - /// println!("{:?}", array); + /// dict.insert_id(ns_string!("one"), NSObject::new()); + /// dict.remove(ns_string!("one")); + /// assert!(dict.is_empty()); /// ``` - #[cfg(feature = "Foundation_NSArray")] - pub fn into_values_array(this: Id) -> Id> { - // SAFETY: We've consumed the dictionary, so getting an array from - // it is safe. - unsafe { this.allValues() } + #[doc(alias = "removeObjectForKey:")] + pub fn remove(&mut self, key: &K) -> Option> + where + K: CounterpartOrSelf, + { + // SAFETY: We remove the object from the dictionary below + let old_obj = self + .get(key) + .map(|old_obj| unsafe { util::mutable_collection_retain_removed_id(old_obj) }); + self.removeObjectForKey(key); + old_obj } } @@ -418,7 +563,7 @@ mod iter_helpers { #[cfg(feature = "Foundation_NSEnumerator")] pub use self::iter_helpers::*; -impl<'a, K: Message, V: Message> Index<&'a K> for NSDictionary { +impl<'a, K: Message + Eq + Hash, V: Message> Index<&'a K> for NSDictionary { type Output = V; fn index<'s>(&'s self, index: &'a K) -> &'s V { @@ -427,7 +572,7 @@ impl<'a, K: Message, V: Message> Index<&'a K> for NSDictionary { } #[cfg(feature = "Foundation_NSMutableDictionary")] -impl<'a, K: Message, V: Message> Index<&'a K> for NSMutableDictionary { +impl<'a, K: Message + Eq + Hash, V: Message> Index<&'a K> for NSMutableDictionary { type Output = V; fn index<'s>(&'s self, index: &'a K) -> &'s V { @@ -435,14 +580,14 @@ impl<'a, K: Message, V: Message> Index<&'a K> for NSMutableDictionary { } } -impl<'a, K: Message, V: IsMutable> IndexMut<&'a K> for NSDictionary { +impl<'a, K: Message + Eq + Hash, V: IsMutable> IndexMut<&'a K> for NSDictionary { fn index_mut<'s>(&'s mut self, index: &'a K) -> &'s mut V { self.get_mut(index).unwrap() } } #[cfg(feature = "Foundation_NSMutableDictionary")] -impl<'a, K: Message, V: IsMutable> IndexMut<&'a K> for NSMutableDictionary { +impl<'a, K: Message + Eq + Hash, V: IsMutable> IndexMut<&'a K> for NSMutableDictionary { fn index_mut<'s>(&'s mut self, index: &'a K) -> &'s mut V { self.get_mut(index).unwrap() } diff --git a/crates/icrate/src/additions/Foundation/set.rs b/crates/icrate/src/additions/Foundation/set.rs index 0f3c9f7db..0d338b8fb 100644 --- a/crates/icrate/src/additions/Foundation/set.rs +++ b/crates/icrate/src/additions/Foundation/set.rs @@ -2,9 +2,10 @@ #![cfg(feature = "Foundation_NSSet")] use alloc::vec::Vec; use core::fmt; +use core::hash::Hash; use core::panic::{RefUnwindSafe, UnwindSafe}; -use objc2::mutability::IsRetainable; +use objc2::mutability::{HasStableHash, IsRetainable}; use objc2::rc::IdFromIterator; use super::iter; @@ -15,6 +16,38 @@ use crate::Foundation::NSMutableSet; use crate::Foundation::{self, NSSet}; impl NSSet { + /// Returns the number of elements in the set. + /// + /// # Examples + /// + /// ``` + /// use icrate::Foundation::{NSSet, NSString}; + /// + /// let strs = ["one", "two", "three"].map(NSString::from_str); + /// let set = NSSet::from_id_slice(&strs); + /// assert_eq!(set.len(), 3); + /// ``` + #[doc(alias = "count")] + pub fn len(&self) -> usize { + self.count() + } + + /// Returns `true` if the set contains no elements. + /// + /// # Examples + /// + /// ``` + /// use icrate::Foundation::{NSSet, NSString}; + /// + /// let set = NSSet::::new(); + /// assert!(set.is_empty()); + /// ``` + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl NSSet { /// Creates an [`NSSet`] from a vector. /// /// # Examples @@ -25,7 +58,10 @@ impl NSSet { /// let strs = ["one", "two", "three"].map(NSString::from_str).to_vec(); /// let set = NSSet::from_vec(strs); /// ``` - pub fn from_vec(mut vec: Vec>) -> Id { + pub fn from_vec(mut vec: Vec>) -> Id + where + T: HasStableHash, + { let len = vec.len(); let ptr = util::id_ptr_cast(vec.as_mut_ptr()); // SAFETY: Same as `NSArray::from_vec`. @@ -44,7 +80,7 @@ impl NSSet { /// ``` pub fn from_id_slice(slice: &[Id]) -> Id where - T: IsIdCloneable, + T: HasStableHash + IsIdCloneable, { let len = slice.len(); let ptr = util::id_ptr_cast_const(slice.as_ptr()); @@ -54,7 +90,7 @@ impl NSSet { pub fn from_slice(slice: &[&T]) -> Id where - T: IsRetainable, + T: HasStableHash + IsRetainable, { let len = slice.len(); let ptr = util::ref_ptr_cast_const(slice.as_ptr()); @@ -126,7 +162,7 @@ impl NSSet { } #[cfg(feature = "Foundation_NSMutableSet")] -impl NSMutableSet { +impl NSMutableSet { /// Creates an [`NSMutableSet`] from a vector. /// /// # Examples @@ -137,7 +173,10 @@ impl NSMutableSet { /// let strs = ["one", "two", "three"].map(NSString::from_str).to_vec(); /// let set = NSMutableSet::from_vec(strs); /// ``` - pub fn from_vec(mut vec: Vec>) -> Id { + pub fn from_vec(mut vec: Vec>) -> Id + where + T: HasStableHash, + { let len = vec.len(); let ptr = util::id_ptr_cast(vec.as_mut_ptr()); // SAFETY: Same as `NSArray::from_vec`. @@ -156,7 +195,7 @@ impl NSMutableSet { /// ``` pub fn from_id_slice(slice: &[Id]) -> Id where - T: IsIdCloneable, + T: HasStableHash + IsIdCloneable, { let len = slice.len(); let ptr = util::id_ptr_cast_const(slice.as_ptr()); @@ -166,7 +205,7 @@ impl NSMutableSet { pub fn from_slice(slice: &[&T]) -> Id where - T: IsRetainable, + T: HasStableHash + IsRetainable, { let len = slice.len(); let ptr = util::ref_ptr_cast_const(slice.as_ptr()); @@ -197,36 +236,6 @@ impl NSMutableSet { extern_methods!( unsafe impl NSSet { - /// Returns the number of elements in the set. - /// - /// # Examples - /// - /// ``` - /// use icrate::Foundation::{NSSet, NSString}; - /// - /// let strs = ["one", "two", "three"].map(NSString::from_str); - /// let set = NSSet::from_id_slice(&strs); - /// assert_eq!(set.len(), 3); - /// ``` - #[doc(alias = "count")] - pub fn len(&self) -> usize { - self.count() - } - - /// Returns `true` if the set contains no elements. - /// - /// # Examples - /// - /// ``` - /// use icrate::Foundation::{NSSet, NSString}; - /// - /// let set = NSSet::::new(); - /// assert!(set.is_empty()); - /// ``` - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - /// Returns a reference to one of the objects in the set, or `None` if /// the set is empty. /// @@ -245,10 +254,7 @@ extern_methods!( pub fn get_any(&self) -> Option<&T>; } - // We're explicit about `T` being `PartialEq` for these methods because - // the set compares the input value(s) with elements in the set. For - // comparison: Rust's HashSet requires similar methods to be `Hash` + `Eq` - unsafe impl NSSet { + unsafe impl NSSet { /// Returns `true` if the set contains a value. /// /// # Examples @@ -292,7 +298,7 @@ extern_methods!( } // Note: No `get_mut` method exposed on sets, since their objects' - // hashes are supposed to be immutable. + // hashes are immutable. /// Returns `true` if the set is a subset of another, i.e., `other` /// contains at least all the values in `self`. @@ -353,8 +359,34 @@ extern_methods!( ); #[cfg(feature = "Foundation_NSMutableSet")] -impl NSMutableSet { - /// Adds a value to the set. Returns whether the value was +impl NSMutableSet { + /// Add a value to the set. Returns whether the value was + /// newly inserted. + /// + /// # Examples + /// + /// ``` + /// use icrate::Foundation::{NSNumber, NSMutableSet}; + /// + /// let mut set = NSMutableSet::new(); + /// + /// assert_eq!(set.insert(&*NSNumber::new_u32(42)), true); + /// assert_eq!(set.insert(&*NSNumber::new_u32(42)), false); + /// assert_eq!(set.len(), 1); + /// ``` + #[doc(alias = "addObject:")] + pub fn insert(&mut self, value: &T) -> bool + where + T: HasStableHash + IsRetainable, + { + let contains_value = self.contains(value); + // SAFETY: Because of the `T: IsRetainable` bound, it is safe for the + // set to retain the object here. + unsafe { self.addObject(value) }; + !contains_value + } + + /// Add an `Id` to the set. Returns whether the value was /// newly inserted. /// /// # Examples @@ -364,12 +396,15 @@ impl NSMutableSet { /// /// let mut set = NSMutableSet::new(); /// - /// assert_eq!(set.insert(NSString::from_str("one")), true); - /// assert_eq!(set.insert(NSString::from_str("one")), false); + /// assert_eq!(set.insert_id(NSString::from_str("one")), true); + /// assert_eq!(set.insert_id(NSString::from_str("one")), false); /// assert_eq!(set.len(), 1); /// ``` #[doc(alias = "addObject:")] - pub fn insert(&mut self, value: Id) -> bool { + pub fn insert_id(&mut self, value: Id) -> bool + where + T: HasStableHash, + { let contains_value = self.contains(&value); // SAFETY: We've consumed ownership of the object. unsafe { self.addObject(&value) }; @@ -386,18 +421,24 @@ impl NSMutableSet { /// /// let mut set = NSMutableSet::new(); /// - /// set.insert(NSString::from_str("one")); + /// set.insert_id(NSString::from_str("one")); /// assert_eq!(set.remove(ns_string!("one")), true); /// assert_eq!(set.remove(ns_string!("one")), false); /// ``` #[doc(alias = "removeObject:")] - pub fn remove(&mut self, value: &T) -> bool { + pub fn remove(&mut self, value: &T) -> bool + where + T: HasStableHash, + { let contains_value = self.contains(value); unsafe { self.removeObject(value) }; contains_value } } +// Iteration is not supposed to touch the elements, not even do comparisons. +// +// TODO: Verify that this is actually the case. impl NSSet { /// An iterator visiting all elements in arbitrary order. /// @@ -499,32 +540,31 @@ impl fmt::Debug for NSSet { } #[cfg(feature = "Foundation_NSMutableSet")] -impl Extend> for NSMutableSet { +impl Extend> for NSMutableSet { fn extend>>(&mut self, iter: I) { iter.into_iter().for_each(move |item| { - self.insert(item); + self.insert_id(item); }) } } #[cfg(feature = "Foundation_NSMutableSet")] -impl<'a, T: IsRetainable + PartialEq> Extend<&'a T> for NSMutableSet { +impl<'a, T: IsRetainable + Eq + Hash + HasStableHash> Extend<&'a T> for NSMutableSet { fn extend>(&mut self, iter: I) { - // SAFETY: Because of the `T: IsRetainable` bound, it is safe for the - // set to retain the object here. - iter.into_iter() - .for_each(move |item| unsafe { self.addObject(item) }) + iter.into_iter().for_each(move |item| { + self.insert(item); + }) } } -impl<'a, T: IsRetainable + 'a> IdFromIterator<&'a T> for NSSet { +impl<'a, T: IsRetainable + Eq + Hash + HasStableHash + 'a> IdFromIterator<&'a T> for NSSet { fn id_from_iter>(iter: I) -> Id { let vec = Vec::from_iter(iter); Self::from_slice(&vec) } } -impl IdFromIterator> for NSSet { +impl IdFromIterator> for NSSet { fn id_from_iter>>(iter: I) -> Id { let vec = Vec::from_iter(iter); Self::from_vec(vec) @@ -532,7 +572,9 @@ impl IdFromIterator> for NSSet { } #[cfg(feature = "Foundation_NSMutableSet")] -impl<'a, T: IsRetainable + 'a> IdFromIterator<&'a T> for NSMutableSet { +impl<'a, T: IsRetainable + Eq + Hash + HasStableHash + 'a> IdFromIterator<&'a T> + for NSMutableSet +{ fn id_from_iter>(iter: I) -> Id { let vec = Vec::from_iter(iter); Self::from_slice(&vec) @@ -540,7 +582,7 @@ impl<'a, T: IsRetainable + 'a> IdFromIterator<&'a T> for NSMutableSet { } #[cfg(feature = "Foundation_NSMutableSet")] -impl IdFromIterator> for NSMutableSet { +impl IdFromIterator> for NSMutableSet { fn id_from_iter>>(iter: I) -> Id { let vec = Vec::from_iter(iter); Self::from_vec(vec) diff --git a/crates/icrate/src/fixes/Foundation/copying.rs b/crates/icrate/src/fixes/Foundation/copying.rs index 7cf70cfa0..25b865bb6 100644 --- a/crates/icrate/src/fixes/Foundation/copying.rs +++ b/crates/icrate/src/fixes/Foundation/copying.rs @@ -46,6 +46,9 @@ extern_protocol!( } ); +// FIXME: Remove this hack to make NSMutableDictionary tests work +unsafe impl NSCopying for objc2::rc::__RcTestObject {} + extern_protocol!( /// A protocol to provide mutable copies of objects. /// diff --git a/crates/icrate/tests/attributed_string.rs b/crates/icrate/tests/attributed_string.rs index 6cc0acb13..c41a0d745 100644 --- a/crates/icrate/tests/attributed_string.rs +++ b/crates/icrate/tests/attributed_string.rs @@ -60,10 +60,7 @@ fn test_debug() { let s = unsafe { NSAttributedString::new_with_attributes( &NSString::from_str("abc"), - &Foundation::NSDictionary::from_keys_and_objects( - &[&*NSString::from_str("test")], - vec![obj], - ), + &Foundation::NSDictionary::from_vec(&[&*NSString::from_str("test")], vec![obj]), ) }; let expected = if cfg!(feature = "gnustep-1-7") { diff --git a/crates/icrate/tests/dictionary.rs b/crates/icrate/tests/dictionary.rs index e390b13d3..7fd5414f5 100644 --- a/crates/icrate/tests/dictionary.rs +++ b/crates/icrate/tests/dictionary.rs @@ -7,7 +7,7 @@ use icrate::Foundation::{NSDictionary, NSObject, NSString}; fn sample_dict(key: &str) -> Id> { let string = NSString::from_str(key); let obj = NSObject::new(); - NSDictionary::from_keys_and_objects(&[&*string], vec![obj]) + NSDictionary::from_vec(&[&*string], vec![obj]) } #[test] @@ -87,7 +87,7 @@ fn test_arrays() { assert_eq!(keys[0].as_str(pool), "abcd"); }); - // let objs = NSDictionary::into_values_array(dict); + // let objs = dict.to_array(); // assert_eq!(objs.len(), 1); } @@ -95,6 +95,6 @@ fn test_arrays() { fn test_debug() { let key = NSString::from_str("a"); let val = NSString::from_str("b"); - let dict = NSDictionary::from_keys_and_objects(&[&*key], vec![val]); + let dict = NSDictionary::from_id_slice(&[&*key], &[val]); assert_eq!(format!("{dict:?}"), r#"{"a": "b"}"#); } diff --git a/crates/icrate/tests/mutable_dictionary.rs b/crates/icrate/tests/mutable_dictionary.rs index 83ed4ba67..c20a0fa0b 100644 --- a/crates/icrate/tests/mutable_dictionary.rs +++ b/crates/icrate/tests/mutable_dictionary.rs @@ -6,19 +6,19 @@ use objc2::rc::{Id, __RcTestObject, __ThreadTestData}; use icrate::Foundation::{NSMutableDictionary, NSNumber, NSObject}; fn sample_dict() -> Id> { - NSMutableDictionary::from_keys_and_objects( + NSMutableDictionary::from_id_slice( &[ &*NSNumber::new_i32(1), &*NSNumber::new_i32(2), &*NSNumber::new_i32(3), ], - vec![NSObject::new(), NSObject::new(), NSObject::new()], + &[NSObject::new(), NSObject::new(), NSObject::new()], ) } #[cfg(feature = "Foundation_NSMutableString")] fn sample_dict_mut() -> Id> { - NSMutableDictionary::from_keys_and_objects( + NSMutableDictionary::from_vec( &[ &*NSNumber::new_i32(1), &*NSNumber::new_i32(2), @@ -58,29 +58,33 @@ fn test_values_mut() { #[test] fn test_insert() { - let mut dict = NSMutableDictionary::new(); - assert!(dict.insert(NSNumber::new_i32(1), NSObject::new()).is_none()); - assert!(dict.insert(NSNumber::new_i32(2), NSObject::new()).is_none()); - assert!(dict.insert(NSNumber::new_i32(3), NSObject::new()).is_none()); - assert!(dict.insert(NSNumber::new_i32(1), NSObject::new()).is_some()); + let mut dict = >::new(); + assert!(dict + .insert_id(&NSNumber::new_i32(1), NSObject::new()) + .is_none()); + assert!(dict + .insert_id(&NSNumber::new_i32(2), NSObject::new()) + .is_none()); + assert!(dict + .insert_id(&NSNumber::new_i32(3), NSObject::new()) + .is_none()); + assert!(dict + .insert_id(&NSNumber::new_i32(1), NSObject::new()) + .is_some()); assert_eq!(dict.len(), 3); } #[test] fn test_insert_key_copies() { - let mut dict = NSMutableDictionary::new(); + let mut dict = >::new(); let key1 = __RcTestObject::new(); let mut expected = __ThreadTestData::current(); - let _ = dict.insert(key1, NSNumber::new_i32(1)); + let _ = dict.insert_id(&key1, NSNumber::new_i32(1)); // Create copy expected.copy += 1; expected.alloc += 1; expected.init += 1; - - // Release passed-in key - expected.release += 1; - expected.dealloc += 1; expected.assert_current(); dict.removeAllObjects(); @@ -90,14 +94,26 @@ fn test_insert_key_copies() { expected.assert_current(); } +#[test] +fn test_get_key_copies() { + let mut dict = >::new(); + let key1 = __RcTestObject::new(); + let _ = dict.insert_id(&key1, NSNumber::new_i32(1)); + let expected = __ThreadTestData::current(); + + let _ = dict.get(&key1); + // No change, getting doesn't do anything to the key! + expected.assert_current(); +} + #[test] fn test_insert_value_retain_release() { - let mut dict = NSMutableDictionary::new(); - dict.insert(NSNumber::new_i32(1), __RcTestObject::new()); + let mut dict = >::new(); + dict.insert_id(&NSNumber::new_i32(1), __RcTestObject::new()); let to_insert = __RcTestObject::new(); let mut expected = __ThreadTestData::current(); - let old = dict.insert(NSNumber::new_i32(1), to_insert); + let old = dict.insert(&NSNumber::new_i32(1), &to_insert); // Grab old value expected.retain += 1; @@ -105,9 +121,6 @@ fn test_insert_value_retain_release() { expected.retain += 1; expected.release += 1; - // Release passed-in `Id` - expected.release += 1; - expected.assert_current(); drop(old); @@ -138,9 +151,9 @@ fn test_clear() { #[test] fn test_remove_clear_release_dealloc() { - let mut dict = NSMutableDictionary::new(); + let mut dict = >::new(); for i in 0..4 { - dict.insert(NSNumber::new_i32(i), __RcTestObject::new()); + dict.insert_id(&NSNumber::new_i32(i), __RcTestObject::new()); } let mut expected = __ThreadTestData::current(); @@ -165,9 +178,9 @@ fn test_remove_clear_release_dealloc() { #[test] #[cfg(feature = "Foundation_NSArray")] -fn test_into_values_array() { +fn test_to_array() { let dict = sample_dict(); - let array = NSMutableDictionary::into_values_array(dict); + let array = dict.to_array(); assert_eq!(array.len(), 3); } diff --git a/crates/icrate/tests/mutable_set.rs b/crates/icrate/tests/mutable_set.rs index ba47c3310..da18d0266 100644 --- a/crates/icrate/tests/mutable_set.rs +++ b/crates/icrate/tests/mutable_set.rs @@ -10,9 +10,9 @@ fn test_insert() { let mut set = NSMutableSet::new(); assert!(set.is_empty()); - assert!(set.insert(NSString::from_str("one"))); - assert!(!set.insert(NSString::from_str("one"))); - assert!(set.insert(NSString::from_str("two"))); + assert!(set.insert_id(NSString::from_str("one"))); + assert!(!set.insert_id(NSString::from_str("one"))); + assert!(set.insert_id(NSString::from_str("two"))); } #[test] @@ -69,7 +69,7 @@ fn test_mutable_copy() { let set1 = NSSet::from_id_slice(&["one", "two", "three"].map(NSString::from_str)); let mut set2 = set1.mutableCopy(); - set2.insert(NSString::from_str("four")); + set2.insert_id(NSString::from_str("four")); assert!(set1.is_subset(&set2)); assert_ne!(set1.mutableCopy(), set2); @@ -77,34 +77,28 @@ fn test_mutable_copy() { #[test] fn test_insert_retain_release() { - let mut set = NSMutableSet::new(); + let mut set = >::new(); let obj1 = __RcTestObject::new(); let obj2 = __RcTestObject::new(); let obj2_copy = obj2.retain(); let mut expected = __ThreadTestData::current(); - set.insert(obj1); + set.insert(&obj1); // Retain to store in set expected.retain += 1; - // Release passed in object - expected.release += 1; expected.assert_current(); assert_eq!(set.len(), 1); assert_eq!(set.get_any(), set.get_any()); - set.insert(obj2); + set.insert(&obj2); // Retain to store in set expected.retain += 1; - // Release passed in object - expected.release += 1; expected.assert_current(); assert_eq!(set.len(), 2); - set.insert(obj2_copy); + set.insert(&obj2_copy); // No retain, since the object is already in the set expected.retain += 0; - // Release passed in object - expected.release += 1; expected.assert_current(); assert_eq!(set.len(), 2); } @@ -113,7 +107,7 @@ fn test_insert_retain_release() { fn test_clear_release_dealloc() { let mut set = NSMutableSet::new(); for _ in 0..4 { - set.insert(__RcTestObject::new()); + set.insert_id(__RcTestObject::new()); } let mut expected = __ThreadTestData::current(); diff --git a/crates/icrate/tests/set.rs b/crates/icrate/tests/set.rs index 5979bf77e..5f3d5ca6a 100644 --- a/crates/icrate/tests/set.rs +++ b/crates/icrate/tests/set.rs @@ -4,7 +4,7 @@ use objc2::rc::{__RcTestObject, __ThreadTestData}; -use icrate::Foundation::{self, ns_string, NSNumber, NSObject, NSSet, NSString}; +use icrate::Foundation::{self, ns_string, NSNumber, NSSet, NSString}; #[test] fn test_new() { @@ -48,7 +48,11 @@ fn test_len() { let set = NSSet::from_id_slice(&["one", "two", "two"].map(NSString::from_str)); assert_eq!(set.len(), 2); - let set = NSSet::from_vec(vec![NSObject::new(), NSObject::new(), NSObject::new()]); + let set = NSSet::from_vec(vec![ + NSNumber::new_i32(1), + NSNumber::new_i32(2), + NSNumber::new_i32(3), + ]); assert_eq!(set.len(), 3); } diff --git a/crates/objc2/CHANGELOG.md b/crates/objc2/CHANGELOG.md index b2c01daca..c458ad14e 100644 --- a/crates/objc2/CHANGELOG.md +++ b/crates/objc2/CHANGELOG.md @@ -7,8 +7,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased - YYYY-MM-DD ### Added -* Added `mutability::IsMainThreadOnly`. -* Added `mutability::CounterpartOrSelf`. +* Added the following traits to the `mutability` module (see the documentation + for motivation and usage info): + - `HasStableHash`. + - `IsMainThreadOnly`. + - `CounterpartOrSelf`. * Added new `encode` traits `EncodeReturn`, `EncodeArgument` and `EncodeArguments`. diff --git a/crates/objc2/src/mutability.rs b/crates/objc2/src/mutability.rs index 0138a1e21..4882fe1e3 100644 --- a/crates/objc2/src/mutability.rs +++ b/crates/objc2/src/mutability.rs @@ -136,10 +136,17 @@ pub struct ImmutableWithMutableSubclass { /// Marker type for mutable classes that have a immutable counterpart. /// -/// This is effectively the same as [`Mutable`], except that the immutable +/// This is effectively the same as [`Mutable`], except for the immutable /// counterpart being be specified as the type parameter `IS` to allow /// `NSCopying` and `NSMutableCopying` to return the correct type. /// +/// Functionality that is provided with this: +/// - [`IsAllocableAnyThread`] -> [`ClassType::alloc`]. +/// - [`IsMutable`] -> [`impl DerefMut for Id`][crate::rc::Id#impl-DerefMut-for-Id]. +/// - You are allowed to hand out pointers / references to an instance's +/// internal data, since you know such data will never be mutated without +/// the borrowchecker catching it. +/// /// /// # Example /// @@ -201,6 +208,11 @@ pub struct InteriorMutable { /// /// It is unsound to implement [`Send`] or [`Sync`] on a type with this /// mutability. +/// +/// Functionality that is provided with this: +/// - [`IsRetainable`] -> [`ClassType::retain`]. +/// - [`IsIdCloneable`] -> [`Id::clone`][crate::rc::Id#impl-Clone-for-Id]. +/// - [`IsMainThreadOnly`] -> `MainThreadMarker::from`. // // While Xcode's Main Thread Checker doesn't report `alloc` and `dealloc` as // unsafe from other threads, things like `NSView` and `NSWindow` still do a @@ -253,6 +265,12 @@ mod private { pub trait MutabilityIsMainThreadOnly: Mutability {} impl MutabilityIsMainThreadOnly for MainThreadOnly {} + pub trait MutabilityHashIsStable: Mutability {} + impl MutabilityHashIsStable for Immutable {} + impl MutabilityHashIsStable for Mutable {} + impl MutabilityHashIsStable for ImmutableWithMutableSubclass {} + impl MutabilityHashIsStable for MutableWithImmutableSuperclass {} + pub trait MutabilityCounterpartOrSelf: Mutability { type Immutable: ?Sized + ClassType; type Mutable: ?Sized + ClassType; @@ -295,9 +313,6 @@ mod private { type Immutable = T; type Mutable = T; } - - // TODO: Trait for objects whose `hash` is guaranteed to never change, - // which allows it to be used as a key in `NSDictionary`. } /// Marker trait for the different types of mutability a class can have. @@ -392,6 +407,24 @@ impl IsMainThreadOnly for T where { } +/// Marker trait for classes whose `hash` and `isEqual:` methods are stable. +/// +/// This is useful for hashing collection types like `NSDictionary` and +/// `NSSet` which require that their keys never change. +/// +/// This is implemented for classes whose [`ClassType::Mutability`] is one of: +/// - [`Immutable`]. +/// - [`Mutable`]. +/// - [`ImmutableWithMutableSubclass`]. +/// - [`MutableWithImmutableSuperclass`]. +/// +/// Since all of these do not use interior mutability, and since the `hash` +/// and `isEqual:` methods are required to not use external sources like +/// thread locals or randomness to determine their result, we can guarantee +/// that the hash is stable for these types. +pub trait HasStableHash: ClassType {} +impl HasStableHash for T where T::Mutability: private::MutabilityHashIsStable {} + /// Retrieve the immutable/mutable counterpart class, and fall back to `Self` /// if not applicable. /// diff --git a/crates/objc2/src/rc/test_object.rs b/crates/objc2/src/rc/test_object.rs index 339a8b0ae..2faed6de2 100644 --- a/crates/objc2/src/rc/test_object.rs +++ b/crates/objc2/src/rc/test_object.rs @@ -60,7 +60,7 @@ std::thread_local! { declare_class!( /// A helper object that counts how many times various reference-counting /// primitives are called. - #[derive(Debug, PartialEq, Eq)] + #[derive(Debug, PartialEq, Eq, Hash)] #[doc(hidden)] pub struct __RcTestObject; diff --git a/crates/objc2/src/runtime/nsobject.rs b/crates/objc2/src/runtime/nsobject.rs index 549c330b0..4b2570818 100644 --- a/crates/objc2/src/runtime/nsobject.rs +++ b/crates/objc2/src/runtime/nsobject.rs @@ -102,6 +102,8 @@ unsafe impl ClassType for NSObject { /// that implements the `NSObject` protocol. #[allow(non_snake_case)] pub unsafe trait NSObjectProtocol { + // Note: This method must remain `unsafe` to override, since hashing + // collections like `NSDictionary` and `NSSet` rely on it being stable. #[doc(hidden)] fn __isEqual(&self, other: &Self) -> bool where @@ -110,6 +112,8 @@ pub unsafe trait NSObjectProtocol { unsafe { msg_send![self, isEqual: other] } } + // Note: This method must remain `unsafe` to override, since hashing + // collections like `NSDictionary` and `NSSet` rely on it being stable. #[doc(hidden)] fn __hash(&self) -> usize where diff --git a/crates/test-ui/Cargo.toml b/crates/test-ui/Cargo.toml index 244b0b684..a61d07a3e 100644 --- a/crates/test-ui/Cargo.toml +++ b/crates/test-ui/Cargo.toml @@ -21,6 +21,7 @@ default = [ "icrate/Foundation_NSArray", "icrate/Foundation_NSMutableArray", "icrate/Foundation_NSValue", + "icrate/Foundation_NSSet", "objc2/unstable-msg-send-always-comma", ] std = ["block2/std", "objc2/std", "icrate/std"] diff --git a/crates/test-ui/ui/msg_send_invalid_error.stderr b/crates/test-ui/ui/msg_send_invalid_error.stderr index be3b2a06e..00ee6e294 100644 --- a/crates/test-ui/ui/msg_send_invalid_error.stderr +++ b/crates/test-ui/ui/msg_send_invalid_error.stderr @@ -42,7 +42,7 @@ error[E0277]: the trait bound `i32: Message` is not satisfied NSMutableArray NSDictionary NSMutableDictionary - NSEnumerator + NSSet and $N others note: required by a bound in `__send_message_error` --> $WORKSPACE/crates/objc2/src/message/mod.rs diff --git a/crates/test-ui/ui/nsset_from_nsobject.rs b/crates/test-ui/ui/nsset_from_nsobject.rs new file mode 100644 index 000000000..955e2a08a --- /dev/null +++ b/crates/test-ui/ui/nsset_from_nsobject.rs @@ -0,0 +1,6 @@ +//! Test that `NSSet` can't be created from types with an unknown stable hash. +use icrate::Foundation::{NSObject, NSSet}; + +fn main() { + let _ = NSSet::from_vec(vec![NSObject::new()]); +} diff --git a/crates/test-ui/ui/nsset_from_nsobject.stderr b/crates/test-ui/ui/nsset_from_nsobject.stderr new file mode 100644 index 000000000..3fb60164a --- /dev/null +++ b/crates/test-ui/ui/nsset_from_nsobject.stderr @@ -0,0 +1,22 @@ +error[E0277]: the trait bound `Root: mutability::private::MutabilityHashIsStable` is not satisfied + --> ui/nsset_from_nsobject.rs + | + | let _ = NSSet::from_vec(vec![NSObject::new()]); + | --------------- ^^^^^^^^^^^^^^^^^^^^^ the trait `mutability::private::MutabilityHashIsStable` is not implemented for `Root` + | | + | required by a bound introduced by this call + | + = help: the following other types implement trait `mutability::private::MutabilityHashIsStable`: + Immutable + Mutable + ImmutableWithMutableSubclass + MutableWithImmutableSuperclass + = note: required for `NSObject` to implement `HasStableHash` +note: required by a bound in `set::>::from_vec` + --> $WORKSPACE/crates/icrate/src/generated/Foundation/../../additions/Foundation/set.rs + | + | pub fn from_vec(mut vec: Vec>) -> Id + | -------- required by a bound in this associated function + | where + | T: HasStableHash, + | ^^^^^^^^^^^^^ required by this bound in `set::>::from_vec`