From 29d26f582bdd92d7a87f9c600bcec439e6addf6a Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Fri, 29 Jul 2022 16:49:04 +0200 Subject: [PATCH] Remove generics from NSValue, and add NSNumber --- objc2/CHANGELOG_FOUNDATION.md | 6 + objc2/src/foundation/array.rs | 6 +- objc2/src/foundation/enumerator.rs | 10 +- objc2/src/foundation/mod.rs | 5 +- objc2/src/foundation/number.rs | 413 +++++++++++++++++++ objc2/src/foundation/value.rs | 265 +++++++----- objc2/src/macros/extern_class.rs | 4 +- test-ui/ui/msg_send_id_invalid_return.stderr | 4 +- test-ui/ui/nsvalue_f32_not_eq.rs | 12 - test-ui/ui/nsvalue_f32_not_eq.stderr | 22 - test-ui/ui/object_not_send_sync.rs | 7 +- test-ui/ui/object_not_send_sync.stderr | 37 ++ 12 files changed, 643 insertions(+), 148 deletions(-) create mode 100644 objc2/src/foundation/number.rs delete mode 100644 test-ui/ui/nsvalue_f32_not_eq.rs delete mode 100644 test-ui/ui/nsvalue_f32_not_eq.stderr diff --git a/objc2/CHANGELOG_FOUNDATION.md b/objc2/CHANGELOG_FOUNDATION.md index 4018d0f96..7a86bccbd 100644 --- a/objc2/CHANGELOG_FOUNDATION.md +++ b/objc2/CHANGELOG_FOUNDATION.md @@ -9,15 +9,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased - YYYY-MM-DD ### Added +* Added `NSNumber`. * Implement `UnwindSafe` and `RefUnwindSafe` for all objects. * Implemented `IntoIterator` for references to `NSArray`, `NSMutableArray`, `NSData` and `NSMutableData`. * Implemented `Extend` for `NSMutableArray`. * Add extra `Extend<&u8>` impl for `NSMutableData`. +* Added function `NSValue::contains_encoding` for determining if the encoding + of the `NSValue` matches the encoding of the given type. ### Changed * **BREAKING**: Moved from external crate `objc2_foundation` into `objc2::foundation`. +* **BREAKING**: Made `NSValue` not generic any more. While we loose some + type-safety from this, it makes `NSValue` much more useful in the real + world! ### Fixed * Made `Debug` impls for all objects print something useful. diff --git a/objc2/src/foundation/array.rs b/objc2/src/foundation/array.rs index bfe368c58..6ae44109c 100644 --- a/objc2/src/foundation/array.rs +++ b/objc2/src/foundation/array.rs @@ -243,7 +243,7 @@ mod tests { use alloc::vec::Vec; use super::*; - use crate::foundation::{NSString, NSValue}; + use crate::foundation::{NSNumber, NSString}; use crate::rc::autoreleasepool; fn sample_array(len: usize) -> Id, Owned> { @@ -254,10 +254,10 @@ mod tests { NSArray::from_vec(vec) } - fn sample_number_array(len: u8) -> Id, Shared>, Shared> { + fn sample_number_array(len: u8) -> Id, Shared> { let mut vec = Vec::with_capacity(len as usize); for i in 0..len { - vec.push(NSValue::new(i)); + vec.push(NSNumber::new_u8(i)); } NSArray::from_vec(vec) } diff --git a/objc2/src/foundation/enumerator.rs b/objc2/src/foundation/enumerator.rs index 8c69965f4..69dd187b8 100644 --- a/objc2/src/foundation/enumerator.rs +++ b/objc2/src/foundation/enumerator.rs @@ -168,29 +168,29 @@ impl<'a, C: NSFastEnumeration + ?Sized> Iterator for NSFastEnumerator<'a, C> { #[cfg(test)] mod tests { use super::NSFastEnumeration; - use crate::foundation::{NSArray, NSValue}; + use crate::foundation::{NSArray, NSNumber}; #[test] fn test_enumerator() { - let vec = (0usize..4).map(NSValue::new).collect(); + let vec = (0..4).map(NSNumber::new_usize).collect(); let array = NSArray::from_vec(vec); let enumerator = array.iter(); assert_eq!(enumerator.count(), 4); let enumerator = array.iter(); - assert!(enumerator.enumerate().all(|(i, obj)| obj.get() == i)); + assert!(enumerator.enumerate().all(|(i, obj)| obj.as_usize() == i)); } #[test] fn test_fast_enumerator() { - let vec = (0usize..4).map(NSValue::new).collect(); + let vec = (0..4).map(NSNumber::new_usize).collect(); let array = NSArray::from_vec(vec); let enumerator = array.iter_fast(); assert_eq!(enumerator.count(), 4); let enumerator = array.iter_fast(); - assert!(enumerator.enumerate().all(|(i, obj)| obj.get() == i)); + assert!(enumerator.enumerate().all(|(i, obj)| obj.as_usize() == i)); } } diff --git a/objc2/src/foundation/mod.rs b/objc2/src/foundation/mod.rs index ef038a9b2..905b8fcb6 100644 --- a/objc2/src/foundation/mod.rs +++ b/objc2/src/foundation/mod.rs @@ -39,6 +39,7 @@ pub use self::mutable_array::NSMutableArray; pub use self::mutable_attributed_string::NSMutableAttributedString; pub use self::mutable_data::NSMutableData; pub use self::mutable_string::NSMutableString; +pub use self::number::NSNumber; pub use self::object::NSObject; pub use self::process_info::NSProcessInfo; pub use self::range::NSRange; @@ -77,6 +78,7 @@ mod mutable_array; mod mutable_attributed_string; mod mutable_data; mod mutable_string; +mod number; mod object; mod process_info; mod range; @@ -141,6 +143,7 @@ mod tests { assert_auto_traits::(); assert_auto_traits::(); assert_auto_traits::(); + assert_auto_traits::(); // assert_auto_traits::(); // Intentional assert_auto_traits::(); assert_auto_traits::(); @@ -149,7 +152,7 @@ mod tests { assert_auto_traits::(); #[cfg(not(macos_10_7))] assert_auto_traits::(); - assert_auto_traits::>(); + // assert_auto_traits::(); // Intentional assert_unwindsafe::(); // Intentional } } diff --git a/objc2/src/foundation/number.rs b/objc2/src/foundation/number.rs new file mode 100644 index 000000000..1aadfe718 --- /dev/null +++ b/objc2/src/foundation/number.rs @@ -0,0 +1,413 @@ +use core::cmp::Ordering; +use core::fmt; +use core::hash; +use core::panic::{RefUnwindSafe, UnwindSafe}; +use std::os::raw::{ + c_char, c_double, c_float, c_int, c_longlong, c_short, c_uchar, c_uint, c_ulonglong, c_ushort, +}; + +use super::{ + CGFloat, NSComparisonResult, NSCopying, NSInteger, NSObject, NSString, NSUInteger, NSValue, +}; +use crate::rc::{Id, Shared}; +use crate::runtime::Bool; +use crate::Encoding; +use crate::{extern_class, msg_send, msg_send_bool, msg_send_id}; + +extern_class! { + /// An object wrapper for primitive scalars. + /// + /// This is the Objective-C equivalant of a Rust enum containing the + /// common scalar types `i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `i64`, + /// `u64`, `f32`, `f64` and the two C types `c_long` and `c_ulong`. + /// + /// All accessor methods are safe, though they may return unexpected + /// results if the number was not created from said type. Consult [Apple's + /// documentation][apple-doc] for details. + /// + /// Note that due to limitations in Objective-C type encodings, it is not + /// possible to distinguish between an `NSNumber` created from [`bool`], + /// and one created from an [`i8`]/[`u8`]. You should use the getter + /// methods that fit your use-case instead! + /// + /// This does not implement [`Eq`] nor [`Ord`], since it may contain a + /// floating point value. Beware that the implementation of [`PartialEq`] + /// and [`PartialOrd`] does not properly handle NaNs either. Compare + /// [`NSNumber::encoding`] with [`Encoding::Float`] or + /// [`Encoding::Double`], and use [`NSNumber::as_f32`] or + /// [`NSNumber::as_f64`] to get the desired floating point value directly. + /// + /// See [Apple's documentation][apple-doc] for more information. + /// + /// [apple-doc]: https://developer.apple.com/documentation/foundation/nsnumber?language=objc + unsafe pub struct NSNumber: NSValue, NSObject; +} + +// SAFETY: `NSNumber` is just a wrapper around an integer/float/bool, and it +// is immutable. +unsafe impl Sync for NSNumber {} +unsafe impl Send for NSNumber {} + +impl UnwindSafe for NSNumber {} +impl RefUnwindSafe for NSNumber {} + +macro_rules! def_new_fn { + {$( + $(#[$($m:meta)*])* + ($fn_name:ident($fn_inp:ty); $method_name:ident: $method_inp:ty), + )*} => {$( + $(#[$($m)*])* + pub fn $fn_name(val: $fn_inp) -> Id { + let val = val as $method_inp; + unsafe { + msg_send_id![Self::class(), $method_name: val].expect("unexpected NULL NSNumber") + } + } + )*} +} + +/// Creation methods. +impl NSNumber { + pub fn new_bool(val: bool) -> Id { + let val = Bool::new(val); + unsafe { + msg_send_id![Self::class(), numberWithBool: val].expect("unexpected NULL NSNumber") + } + } + + def_new_fn! { + (new_i8(i8); numberWithChar: c_char), + (new_u8(u8); numberWithUnsignedChar: c_uchar), + (new_i16(i16); numberWithShort: c_short), + (new_u16(u16); numberWithUnsignedShort: c_ushort), + (new_i32(i32); numberWithInt: c_int), + (new_u32(u32); numberWithUnsignedInt: c_uint), + (new_i64(i64); numberWithLongLong: c_longlong), + (new_u64(u64); numberWithUnsignedLongLong: c_ulonglong), + (new_isize(isize); numberWithInteger: NSInteger), + (new_usize(usize); numberWithUnsignedInteger: NSUInteger), + (new_f32(f32); numberWithFloat: c_float), + (new_f64(f64); numberWithDouble: c_double), + } + + #[inline] + pub fn new_cgfloat(val: CGFloat) -> Id { + #[cfg(target_pointer_width = "64")] + { + Self::new_f64(val) + } + #[cfg(not(target_pointer_width = "64"))] + { + Self::new_f32(val) + } + } +} + +macro_rules! def_get_fn { + {$( + $(#[$($m:meta)*])* + ($fn_name:ident -> $fn_ret:ty; $method_name:ident -> $method_ret:ty), + )*} => {$( + $(#[$($m)*])* + pub fn $fn_name(&self) -> $fn_ret { + let ret: $method_ret = unsafe { msg_send![self, $method_name] }; + ret as $fn_ret + } + )*} +} + +/// Getter methods. +impl NSNumber { + pub fn as_bool(&self) -> bool { + unsafe { msg_send_bool![self, boolValue] } + } + + def_get_fn! { + (as_i8 -> i8; charValue -> c_char), + (as_u8 -> u8; unsignedCharValue -> c_uchar), + (as_i16 -> i16; shortValue -> c_short), + (as_u16 -> u16; unsignedShortValue -> c_ushort), + (as_i32 -> i32; intValue -> c_int), + (as_u32 -> u32; unsignedIntValue -> c_uint), + // TODO: Getter methods for `long` and `unsigned long` + (as_i64 -> i64; longLongValue -> c_longlong), + (as_u64 -> u64; unsignedLongLongValue -> c_ulonglong), + (as_isize -> isize; integerValue -> NSInteger), + (as_usize -> usize; unsignedIntegerValue -> NSUInteger), + (as_f32 -> f32; floatValue -> c_float), + (as_f64 -> f64; doubleValue -> c_double), + } + + #[inline] + pub fn as_cgfloat(&self) -> CGFloat { + #[cfg(target_pointer_width = "64")] + { + self.as_f64() + } + #[cfg(not(target_pointer_width = "64"))] + { + self.as_f32() + } + } + + /// The Objective-C encoding of this `NSNumber`. + /// + /// This is guaranteed to return one of: + /// - [`Encoding::Char`] + /// - [`Encoding::UChar`] + /// - [`Encoding::Short`] + /// - [`Encoding::UShort`] + /// - [`Encoding::Int`] + /// - [`Encoding::UInt`] + /// - [`Encoding::Long`] + /// - [`Encoding::ULong`] + /// - [`Encoding::LongLong`] + /// - [`Encoding::ULongLong`] + /// - [`Encoding::Float`] + /// - [`Encoding::Double`] + /// + /// + /// # Examples + /// + /// Convert an `NSNumber` to/from an enumeration describing the different + /// number properties. + /// + /// ``` + /// use objc2::Encoding; + /// use objc2::foundation::NSNumber; + /// use objc2::rc::{Id, Shared}; + /// + /// // Note: `bool` would convert to either `Signed` or `Unsigned`, + /// // depending on platform + /// #[derive(Copy, Clone)] + /// pub enum Number { + /// Signed(i64), + /// Unsigned(u64), + /// Floating(f64), + /// } + /// + /// impl Number { + /// fn into_nsnumber(self) -> Id { + /// match self { + /// Self::Signed(val) => NSNumber::new_i64(val), + /// Self::Unsigned(val) => NSNumber::new_u64(val), + /// Self::Floating(val) => NSNumber::new_f64(val), + /// } + /// } + /// } + /// + /// impl From<&NSNumber> for Number { + /// fn from(n: &NSNumber) -> Self { + /// match n.encoding() { + /// Encoding::Char + /// | Encoding::Short + /// | Encoding::Int + /// | Encoding::Long + /// | Encoding::LongLong => Self::Signed(n.as_i64()), + /// Encoding::UChar + /// | Encoding::UShort + /// | Encoding::UInt + /// | Encoding::ULong + /// | Encoding::ULongLong => Self::Unsigned(n.as_u64()), + /// Encoding::Float + /// | Encoding::Double => Self::Floating(n.as_f64()), + /// _ => unreachable!(), + /// } + /// } + /// } + /// ``` + pub fn encoding(&self) -> Encoding<'static> { + // Use NSValue::encoding + let enc = (**self) + .encoding() + .expect("NSNumber must have an encoding!"); + + // Guaranteed under "Subclassing Notes" + // + match enc { + "c" => Encoding::Char, + "C" => Encoding::UChar, + "s" => Encoding::Short, + "S" => Encoding::UShort, + "i" => Encoding::Int, + "I" => Encoding::UInt, + "l" => Encoding::Long, + "L" => Encoding::ULong, + "q" => Encoding::LongLong, + "Q" => Encoding::ULongLong, + "f" => Encoding::Float, + "d" => Encoding::Double, + _ => unreachable!("invalid encoding for NSNumber"), + } + } + + fn string(&self) -> Id { + unsafe { + msg_send_id![self, stringValue].expect("unexpected NULL from -[NSNumber stringValue]") + } + } +} + +unsafe impl NSCopying for NSNumber { + type Ownership = Shared; + type Output = NSNumber; +} + +impl alloc::borrow::ToOwned for NSNumber { + type Owned = Id; + fn to_owned(&self) -> Self::Owned { + self.copy() + } +} + +/// Beware: This uses the Objective-C method "isEqualToNumber:", which has +/// different floating point NaN semantics than Rust! +impl PartialEq for NSNumber { + #[doc(alias = "isEqualToNumber:")] + fn eq(&self, other: &Self) -> bool { + // Use isEqualToNumber: instaed of isEqual: since it is faster + unsafe { msg_send_bool![self, isEqualToNumber: other] } + } +} + +impl hash::Hash for NSNumber { + fn hash(&self, state: &mut H) { + // Delegate to NSObject + (***self).hash(state) + } +} + +/// Beware: This uses the Objective-C method "compare:", which has different +/// floating point NaN semantics than Rust! +impl PartialOrd for NSNumber { + #[doc(alias = "compare:")] + fn partial_cmp(&self, other: &Self) -> Option { + // Use Objective-C semantics for comparison + let res: NSComparisonResult = unsafe { msg_send![self, compare: other] }; + Some(res.into()) + } +} + +impl fmt::Display for NSNumber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.string(), f) + } +} + +impl fmt::Debug for NSNumber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Delegate to -[NSObject description] + // (happens to return the same as -[NSNumber stringValue]) + fmt::Debug::fmt(&***self, f) + } +} + +#[cfg(test)] +mod tests { + use alloc::format; + + use super::*; + + #[test] + fn basic() { + let val = NSNumber::new_u32(13); + assert_eq!(val.as_u32(), 13); + } + + #[test] + fn roundtrip() { + assert!(NSNumber::new_bool(true).as_bool()); + assert!(!NSNumber::new_bool(false).as_bool()); + + fn assert_roundtrip_signed(val: i64) { + assert_eq!(NSNumber::new_i8(val as i8).as_i8(), val as i8); + assert_eq!(NSNumber::new_i16(val as i16).as_i16(), val as i16); + assert_eq!(NSNumber::new_i32(val as i32).as_i32(), val as i32); + assert_eq!(NSNumber::new_i64(val).as_i64(), val); + assert_eq!(NSNumber::new_isize(val as isize).as_isize(), val as isize); + } + + assert_roundtrip_signed(i64::MIN); + assert_roundtrip_signed(i32::MIN as i64); + assert_roundtrip_signed(i16::MIN as i64); + assert_roundtrip_signed(i8::MIN as i64); + assert_roundtrip_signed(-1); + assert_roundtrip_signed(0); + assert_roundtrip_signed(1); + assert_roundtrip_signed(i8::MAX as i64); + assert_roundtrip_signed(i16::MAX as i64); + assert_roundtrip_signed(i32::MAX as i64); + assert_roundtrip_signed(i64::MAX); + + fn assert_roundtrip_unsigned(val: u64) { + assert_eq!(NSNumber::new_u8(val as u8).as_u8(), val as u8); + assert_eq!(NSNumber::new_u16(val as u16).as_u16(), val as u16); + assert_eq!(NSNumber::new_u32(val as u32).as_u32(), val as u32); + assert_eq!(NSNumber::new_u64(val).as_u64(), val); + assert_eq!(NSNumber::new_usize(val as usize).as_usize(), val as usize); + } + + assert_roundtrip_unsigned(0); + assert_roundtrip_unsigned(1); + assert_roundtrip_unsigned(u8::MAX as u64); + assert_roundtrip_unsigned(u16::MAX as u64); + assert_roundtrip_unsigned(u32::MAX as u64); + assert_roundtrip_unsigned(u64::MAX); + + fn assert_roundtrip_float(val: f64) { + assert_eq!(NSNumber::new_f32(val as f32).as_f32(), val as f32); + assert_eq!(NSNumber::new_f64(val).as_f64(), val); + } + + assert_roundtrip_float(0.0); + assert_roundtrip_float(-1.0); + assert_roundtrip_float(1.0); + assert_roundtrip_float(f64::INFINITY); + assert_roundtrip_float(-f64::INFINITY); + assert_roundtrip_float(f64::MAX); + assert_roundtrip_float(f64::MIN); + assert_roundtrip_float(f64::MIN_POSITIVE); + + assert!(NSNumber::new_f32(f32::NAN).as_f32().is_nan()); + assert!(NSNumber::new_f64(f64::NAN).as_f64().is_nan()); + assert!(NSNumber::new_f32(-f32::NAN).as_f32().is_nan()); + assert!(NSNumber::new_f64(-f64::NAN).as_f64().is_nan()); + } + + #[test] + fn cast_between_types() { + assert_eq!(NSNumber::new_bool(true).as_i8(), 1); + assert_eq!(NSNumber::new_i32(i32::MAX).as_u32(), i32::MAX as u32); + assert_eq!(NSNumber::new_f32(1.0).as_u32(), 1); + assert_eq!(NSNumber::new_f32(1.0).as_u32(), 1); + } + + #[test] + fn equality() { + let val1 = NSNumber::new_u32(123); + let val2 = NSNumber::new_u32(123); + let val3 = NSNumber::new_u8(123); + assert_eq!(val1, val1); + assert_eq!(val1, val2); + assert_eq!(val1, val3); + + let val4 = NSNumber::new_u32(456); + assert_ne!(val1, val4); + } + + #[test] + fn display_debug() { + fn assert_display_debug(val: T, expected: &str) { + // The two impls for these happen to be the same + assert_eq!(format!("{}", val), expected); + assert_eq!(format!("{:?}", val), expected); + } + assert_display_debug(NSNumber::new_u8(171), "171"); + assert_display_debug(NSNumber::new_i8(-12), "-12"); + assert_display_debug(NSNumber::new_u32(0xdeadbeef), "3735928559"); + assert_display_debug(NSNumber::new_f32(1.1), "1.1"); + assert_display_debug(NSNumber::new_f32(1.0), "1"); + assert_display_debug(NSNumber::new_bool(true), "1"); + assert_display_debug(NSNumber::new_bool(false), "0"); + } +} diff --git a/objc2/src/foundation/value.rs b/objc2/src/foundation/value.rs index 5146e8ae9..8821daa80 100644 --- a/objc2/src/foundation/value.rs +++ b/objc2/src/foundation/value.rs @@ -1,66 +1,131 @@ use alloc::string::ToString; -use core::cmp::Ordering; use core::ffi::c_void; -use core::marker::PhantomData; +use core::fmt; +use core::hash; use core::mem::MaybeUninit; -use core::panic::{RefUnwindSafe, UnwindSafe}; use core::ptr::NonNull; -use core::{fmt, str}; +use core::str; use std::ffi::{CStr, CString}; use std::os::raw::c_char; use super::{NSCopying, NSObject}; -use crate::rc::{DefaultId, Id, Shared}; +use crate::rc::{Id, Shared}; use crate::Encode; -use crate::{__inner_extern_class, msg_send, msg_send_id}; - -__inner_extern_class! { - // `T: Eq` bound to prevent `NSValue` from being `Eq` - // (even though `[NAN isEqual: NAN]` is true in Objective-C). - #[derive(PartialEq, Eq, Hash)] - unsafe pub struct NSValue: NSObject { - value: PhantomData, - } -} +use crate::{extern_class, msg_send, msg_send_bool, msg_send_id}; -// SAFETY: `NSValue` is basically just a wrapper around an inner type, and -// is immutable. -unsafe impl Sync for NSValue {} -unsafe impl Send for NSValue {} +extern_class! { + /// A container wrapping any encodable type as an Obective-C object. + /// + /// Since Objective-C collections like [`NSArray`] can only contain + /// objects, it is common to wrap pointers or structures like [`NSRange`]. + /// + /// Note that creating `NSValue`s is not `unsafe`, but almost all usage of + /// it is, since we cannot guarantee that the type that was used to + /// construct it is the same as the expected output type. + /// + /// See also the [`NSNumber`] subclass for when you want to wrap numbers. + /// + /// See [Apple's documentation][apple-doc] for more information. + /// + /// [`NSArray`]: super::NSArray + /// [`NSRange`]: super::NSRange + /// [`NSNumber`]: super::NSNumber + /// [apple-doc]: https://developer.apple.com/documentation/foundation/nsnumber?language=objc + unsafe pub struct NSValue: NSObject; +} -impl UnwindSafe for NSValue {} -impl RefUnwindSafe for NSValue {} +// We can't implement any auto traits for NSValue, since it can contain an +// arbitary object! -impl NSValue { +/// Creation methods. +impl NSValue { // Default / empty new is not provided because `-init` returns `nil` on // Apple and GNUStep throws an exception on all other messages to this // invalid instance. - /// TODO. + /// Create a new `NSValue` containing the given type. /// - /// Note that this is broken on GNUStep for some types, see - /// [gnustep/libs-base#216]. + /// Be careful when using this since you may accidentally pass a reference + /// when you wanted to pass a concrete type instead. /// - /// [gnustep/libs-base#216]: https://github.com/gnustep/libs-base/pull/216 - pub fn get(&self) -> T { - if let Some(encoding) = self.encoding() { - // TODO: This can be a debug assertion (?) - assert!(T::ENCODING.equivalent_to_str(encoding), "Wrong encoding"); - unsafe { self.get_unchecked() } - } else { - panic!("Missing NSValue encoding"); + /// + /// # Examples + /// + /// Create an `NSValue` containing an [`NSPoint`][super::NSPoint]. + /// + /// ``` + /// use objc2::foundation::{NSPoint, NSValue}; + /// # #[cfg(feature = "gnustep-1-7")] + /// # unsafe { objc2::__gnustep_hack::get_class_to_force_linkage() }; + /// let val = NSValue::new::(NSPoint::new(1.0, 1.0)); + /// ``` + pub fn new(value: T) -> Id { + let bytes: *const T = &value; + let bytes: *const c_void = bytes.cast(); + let encoding = CString::new(T::ENCODING.to_string()).unwrap(); + unsafe { + msg_send_id![ + msg_send_id![Self::class(), alloc], + initWithBytes: bytes, + objCType: encoding.as_ptr(), + ] + .expect("unexpected NULL NSValue") } } +} - /// TODO +/// Getter methods. +impl NSValue { + /// Retrieve the data contained in the `NSValue`. + /// + /// Note that this is broken on GNUStep for some types, see + /// [gnustep/libs-base#216]. + /// + /// [gnustep/libs-base#216]: https://github.com/gnustep/libs-base/pull/216 + /// /// /// # Safety /// - /// The user must ensure that the inner value is properly initialized. - pub unsafe fn get_unchecked(&self) -> T { + /// The type of `T` must be what the NSValue actually stores, and any + /// safety invariants that the value has must be upheld. + /// + /// Note that it may be, but is not always, enough to simply check whether + /// [`contains_encoding`] returns `true`. For example, `NonNull` have + /// the same encoding as `*const T`, but `NonNull` is clearly not + /// safe to return from this function even if you've checked the encoding + /// beforehand. + /// + /// [`contains_encoding`]: Self::contains_encoding + /// + /// + /// # Examples + /// + /// Store a pointer in `NSValue`, and retrieve it again afterwards. + /// + /// ``` + /// use std::ffi::c_void; + /// use std::ptr; + /// use objc2::foundation::NSValue; + /// + /// # #[cfg(feature = "gnustep-1-7")] + /// # unsafe { objc2::__gnustep_hack::get_class_to_force_linkage() }; + /// let val = NSValue::new::<*const c_void>(ptr::null()); + /// // SAFETY: The value was just created with a pointer + /// let res = unsafe { val.get::<*const c_void>() }; + /// assert!(res.is_null()); + /// ``` + pub unsafe fn get(&self) -> T { + debug_assert!( + self.contains_encoding::(), + "wrong encoding. NSValue tried to return something with encoding {}, but the encoding of the given type was {}", + self.encoding().unwrap_or("(NULL)"), + T::ENCODING, + ); let mut value = MaybeUninit::::uninit(); let ptr: *mut c_void = value.as_mut_ptr().cast(); let _: () = unsafe { msg_send![self, getValue: ptr] }; + // SAFETY: We know that `getValue:` initialized the value, and user + // ensures that it is safe to access. unsafe { value.assume_init() } } @@ -69,81 +134,66 @@ impl NSValue { result.map(|s| unsafe { CStr::from_ptr(s.as_ptr()) }.to_str().unwrap()) } - pub fn new(value: T) -> Id { - let cls = Self::class(); - let bytes: *const T = &value; - let bytes: *const c_void = bytes.cast(); - let encoding = CString::new(T::ENCODING.to_string()).unwrap(); - unsafe { - let obj = msg_send_id![cls, alloc]; - msg_send_id![ - obj, - initWithBytes: bytes, - objCType: encoding.as_ptr(), - ] - .unwrap() + pub fn contains_encoding(&self) -> bool { + if let Some(encoding) = self.encoding() { + T::ENCODING.equivalent_to_str(encoding) + } else { + panic!("missing NSValue encoding"); } } } -unsafe impl NSCopying for NSValue { +unsafe impl NSCopying for NSValue { type Ownership = Shared; - type Output = NSValue; + type Output = NSValue; } -impl alloc::borrow::ToOwned for NSValue { - type Owned = Id, Shared>; +impl alloc::borrow::ToOwned for NSValue { + type Owned = Id; fn to_owned(&self) -> Self::Owned { self.copy() } } -impl Ord for NSValue { - fn cmp(&self, other: &Self) -> Ordering { - self.get().cmp(&other.get()) +impl PartialEq for NSValue { + #[doc(alias = "isEqualToValue:")] + fn eq(&self, other: &Self) -> bool { + // Use isEqualToValue: instaed of isEqual: since it is faster + unsafe { msg_send_bool![self, isEqualToValue: other] } } } -impl PartialOrd for NSValue { - fn partial_cmp(&self, other: &Self) -> Option { - self.get().partial_cmp(&other.get()) +impl hash::Hash for NSValue { + fn hash(&self, state: &mut H) { + // Delegate to NSObject + (**self).hash(state) } } -impl DefaultId for NSValue { - type Ownership = Shared; - - #[inline] - fn default_id() -> Id { - Self::new(Default::default()) - } -} - -impl fmt::Display for NSValue { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.get(), f) - } -} - -impl fmt::Debug for NSValue { +impl fmt::Debug for NSValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&self.get(), f) + let enc = self.encoding().unwrap_or("(NULL)"); + let bytes = &**self; // Delegate to -[NSObject description] + f.debug_struct("NSValue") + .field("encoding", &enc) + .field("bytes", bytes) + .finish() } } #[cfg(test)] mod tests { use alloc::format; + use core::slice; use super::*; use crate::foundation::NSRange; use crate::msg_send; #[test] - fn test_value() { + fn basic() { let val = NSValue::new(13u32); - assert_eq!(val.get(), 13); - assert!(u32::ENCODING.equivalent_to_str(val.encoding().unwrap())); + assert_eq!(unsafe { val.get::() }, 13); } #[test] @@ -159,41 +209,56 @@ mod tests { #[test] fn test_equality_across_types() { - let val1 = NSValue::new(123); - let val2: Id, Shared> = NSValue::new(123); - let val2: Id, Shared> = unsafe { core::mem::transmute(val2) }; + let val1 = NSValue::new(123i32); + let val2 = NSValue::new(123u32); // Test that `objCType` is checked when comparing equality assert_ne!(val1, val2); } #[test] - fn test_display_debug() { - fn assert_display_debug(val: T, expected: &str) { - // The two impls for these happen to be the same - assert_eq!(format!("{}", val), expected); - assert_eq!(format!("{:?}", val), expected); - } - assert_display_debug(NSValue::new(171u8), "171"); - assert_display_debug(NSValue::new(-12i8), "-12"); - assert_display_debug(NSValue::new(0xdeadbeefu32), "3735928559"); - assert_display_debug(NSValue::new(1.1f32), "1.1"); - assert_display_debug(NSValue::new(true), "true"); - assert_display_debug(NSValue::new(false), "false"); - - let val = NSValue::new(1.0f32); - assert_eq!(format!("{}", val), "1"); - assert_eq!(format!("{:?}", val), "1.0"); + #[ignore = "the debug output changes depending on OS version"] + fn test_debug() { + let expected = if cfg!(feature = "gnustep-1-7") { + r#"NSValue { encoding: "C", bytes: (C) }"# + } else if cfg!(newer_apple) { + r#"NSValue { encoding: "C", bytes: {length = 1, bytes = 0xab} }"# + } else { + r#"NSValue { encoding: "C", bytes: }"# + }; + assert_eq!(format!("{:?}", NSValue::new(171u8)), expected); } #[test] fn test_value_nsrange() { let val = NSValue::new(NSRange::from(1..2)); - assert!(NSRange::ENCODING.equivalent_to_str(val.encoding().unwrap())); + assert!(val.contains_encoding::()); let range: NSRange = unsafe { msg_send![&val, rangeValue] }; assert_eq!(range, NSRange::from(1..2)); // NSValue -getValue is broken on GNUStep for some types #[cfg(not(feature = "gnustep-1-7"))] - assert_eq!(val.get(), NSRange::from(1..2)); + assert_eq!(unsafe { val.get::() }, NSRange::from(1..2)); + } + + #[test] + fn store_str() { + let s = "abc"; + let val = NSValue::new(s.as_ptr()); + assert!(val.contains_encoding::<*const u8>()); + let slice = unsafe { slice::from_raw_parts(val.get(), s.len()) }; + let s2 = str::from_utf8(slice).unwrap(); + assert_eq!(s2, s); + } + + #[test] + fn store_cstr() { + // The following Apple article says that NSValue can't easily store + // C-strings, but apparently that doesn't apply to us! + // + let s = CStr::from_bytes_with_nul(b"test123\0").unwrap(); + let val = NSValue::new(s.as_ptr()); + assert!(val.contains_encoding::<*const c_char>()); + let s2 = unsafe { CStr::from_ptr(val.get()) }; + assert_eq!(s2, s); } } diff --git a/objc2/src/macros/extern_class.rs b/objc2/src/macros/extern_class.rs index 6d6f84420..540369db1 100644 --- a/objc2/src/macros/extern_class.rs +++ b/objc2/src/macros/extern_class.rs @@ -251,8 +251,8 @@ macro_rules! __inner_extern_class { // Any lifetime information that the object may have been holding is // safely kept in the returned reference. // - // Generics are discarded (for example in the case of `&NSValue` to - // `&NSObject`), but if the generic contained a lifetime, that + // Generics are discarded (for example in the case of `&NSArray` + // to `&NSObject`), but if the generic contained a lifetime, that // lifetime is still included in the returned reference. // // Note that you can easily have two different variables pointing to diff --git a/test-ui/ui/msg_send_id_invalid_return.stderr b/test-ui/ui/msg_send_id_invalid_return.stderr index 67cdb8ae4..af8e04793 100644 --- a/test-ui/ui/msg_send_id_invalid_return.stderr +++ b/test-ui/ui/msg_send_id_invalid_return.stderr @@ -25,7 +25,7 @@ error[E0277]: the trait bound `objc2::runtime::Class: Message` is not satisfied NSException NSMutableArray NSMutableAttributedString - and 9 others + and 10 others = note: required because of the requirements on the impl of `MsgSendId<&objc2::runtime::Class, Id>` for `RetainSemantics` = note: this error originates in the macro `msg_send_id` (in Nightly builds, run with -Z macro-backtrace for more info) @@ -53,7 +53,7 @@ error[E0277]: the trait bound `objc2::runtime::Class: Message` is not satisfied NSException NSMutableArray NSMutableAttributedString - and 9 others + and 10 others = note: required because of the requirements on the impl of `MsgSendId<&objc2::runtime::Class, Id, Shared>>` for `RetainSemantics` = note: this error originates in the macro `msg_send_id` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/test-ui/ui/nsvalue_f32_not_eq.rs b/test-ui/ui/nsvalue_f32_not_eq.rs deleted file mode 100644 index 910084718..000000000 --- a/test-ui/ui/nsvalue_f32_not_eq.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Test that NSValue has proper bounds on its Eq implementation. - -use objc2::foundation::NSValue; - -fn needs_eq() {} - -fn main() { - // Compiles fine - needs_eq::>(); - // Fails - needs_eq::>(); -} diff --git a/test-ui/ui/nsvalue_f32_not_eq.stderr b/test-ui/ui/nsvalue_f32_not_eq.stderr deleted file mode 100644 index b84d7834f..000000000 --- a/test-ui/ui/nsvalue_f32_not_eq.stderr +++ /dev/null @@ -1,22 +0,0 @@ -error[E0277]: the trait bound `f32: Eq` is not satisfied - --> ui/nsvalue_f32_not_eq.rs - | - | needs_eq::>(); - | ^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Eq` is not implemented for `f32` - | - = help: the following other types implement trait `Eq`: - i128 - i16 - i32 - i64 - i8 - isize - u128 - u16 - and 4 others - = note: required because of the requirements on the impl of `Eq` for `NSValue` -note: required by a bound in `needs_eq` - --> ui/nsvalue_f32_not_eq.rs - | - | fn needs_eq() {} - | ^^ required by this bound in `needs_eq` diff --git a/test-ui/ui/object_not_send_sync.rs b/test-ui/ui/object_not_send_sync.rs index 5287707d5..7c765cfaf 100644 --- a/test-ui/ui/object_not_send_sync.rs +++ b/test-ui/ui/object_not_send_sync.rs @@ -1,7 +1,10 @@ //! Test that Object and NSObject are not Send and Sync, because their //! subclasses might not be. +//! +//! Also test that `NSValue` is not Send nor Sync, because its contained value +//! might not be. -use objc2::foundation::NSObject; +use objc2::foundation::{NSObject, NSValue}; use objc2::runtime::Object; fn needs_sync() {} @@ -12,4 +15,6 @@ fn main() { needs_send::(); needs_sync::(); needs_send::(); + needs_sync::(); + needs_send::(); } diff --git a/test-ui/ui/object_not_send_sync.stderr b/test-ui/ui/object_not_send_sync.stderr index a26d7fb04..5f7e13f91 100644 --- a/test-ui/ui/object_not_send_sync.stderr +++ b/test-ui/ui/object_not_send_sync.stderr @@ -65,3 +65,40 @@ note: required by a bound in `needs_send` | | fn needs_send() {} | ^^^^ required by this bound in `needs_send` + +error[E0277]: `UnsafeCell, PhantomPinned)>>` cannot be shared between threads safely + --> ui/object_not_send_sync.rs + | + | needs_sync::(); + | ^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell, PhantomPinned)>>` cannot be shared between threads safely + | + = help: within `NSValue`, the trait `Sync` is not implemented for `UnsafeCell, PhantomPinned)>>` + = note: required because it appears within the type `objc_object` + = note: required because it appears within the type `objc2::runtime::Object` + = note: required because it appears within the type `NSObject` + = note: required because it appears within the type `NSValue` +note: required by a bound in `needs_sync` + --> ui/object_not_send_sync.rs + | + | fn needs_sync() {} + | ^^^^ required by this bound in `needs_sync` + +error[E0277]: `*const UnsafeCell<()>` cannot be sent between threads safely + --> ui/object_not_send_sync.rs + | + | needs_send::(); + | ^^^^^^^^^^^^^^^^^^^^^ `*const UnsafeCell<()>` cannot be sent between threads safely + | + = help: within `NSValue`, the trait `Send` is not implemented for `*const UnsafeCell<()>` + = note: required because it appears within the type `(*const UnsafeCell<()>, PhantomPinned)` + = note: required because it appears within the type `PhantomData<(*const UnsafeCell<()>, PhantomPinned)>` + = note: required because it appears within the type `UnsafeCell, PhantomPinned)>>` + = note: required because it appears within the type `objc_object` + = note: required because it appears within the type `objc2::runtime::Object` + = note: required because it appears within the type `NSObject` + = note: required because it appears within the type `NSValue` +note: required by a bound in `needs_send` + --> ui/object_not_send_sync.rs + | + | fn needs_send() {} + | ^^^^ required by this bound in `needs_send`