Skip to content

Commit

Permalink
Merge pull request #474 from cynecx/member-cast
Browse files Browse the repository at this point in the history
Add `AnyObject::downcast_ref` and `Retained::downcast`
  • Loading branch information
madsmtm authored Sep 18, 2024
2 parents 3d6ba5e + 24a42e4 commit 5780702
Show file tree
Hide file tree
Showing 24 changed files with 410 additions and 80 deletions.
5 changes: 5 additions & 0 deletions crates/objc2/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
the main thread.
* Added `ClassType::alloc_main_thread`.
* Added `IsMainThreadOnly::mtm`.
* Added `DowncastTarget`, `AnyObject::downcast_ref` and `Retained::downcast`
to allow safely casting between Objective-C objects.

### Changed
* **BREAKING**: Changed how you specify a class to only be available on the
Expand Down Expand Up @@ -92,6 +94,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `ffi::objc_ivar` is merged into `runtime::Ivar`.
- `ffi::BOOL` and constants is merged into `runtime::Bool`.
* Deprecated `ffi::id`. Use `AnyObject` instead.
* Deprecated `NSObjectProtocol::is_kind_of`, use `isKindOfClass` or the new
`AnyObject::downcast_ref` method instead.
* Deprecated `Retained::cast`, this has been renamed to `Retained::cast_unchecked`.

### Removed
* **BREAKING**: Removed the `ffi::SEL` and `ffi::objc_selector` types. Use
Expand Down
1 change: 1 addition & 0 deletions crates/objc2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ block2 = { path = "../block2", default-features = false }
objc2-foundation = { path = "../../framework-crates/objc2-foundation", default-features = false, features = [
"NSDate",
"NSDictionary",
"NSEnumerator",
"NSGeometry",
"NSKeyValueObserving",
"NSNotification",
Expand Down
50 changes: 50 additions & 0 deletions crates/objc2/src/downcast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use crate::ClassType;

/// Classes that can be safely downcasted to.
///
/// [`DowncastTarget`] is an unsafe marker trait that can be implemented on
/// types that also implement [`ClassType`].
///
/// Ideally, every type that implements `ClassType` would also be a valid
/// downcast target, however this would be unsound when used with generics,
/// because we can only trivially decide whether the "base container" is an
/// instance of some class type, but anything related to the generic arguments
/// is unknown.
///
/// This trait is implemented automatically by the [`extern_class!`] and
/// [`declare_class!`] macros.
///
/// [`extern_class!`]: crate::extern_class
/// [`declare_class!`]: crate::declare_class
///
///
/// # Safety
///
/// The type must not have any generic arguments other than [`AnyObject`].
///
/// [`AnyObject`]: crate::runtime::AnyObject
///
///
/// # Examples
///
/// Implementing [`DowncastTarget`] for `NSString`:
///
/// ```ignore
/// // SAFETY: NSString does not have any generic parameters.
/// unsafe impl DowncastTarget for NSString {}
/// ```
///
/// However, implementing it for `NSArray` can only be done when the object
/// type is `AnyObject`.
///
/// ```ignore
/// // SAFETY: NSArray does not have any generic parameters set (the generic
/// // defaults to `AnyObject`).
/// unsafe impl DowncastTarget for NSArray {}
///
/// // This would not be valid, since downcasting can only trivially determine
/// // whether the base class (in this case `NSArray`) matches the receiver
/// // class type.
/// // unsafe impl<T: Message> DowncastTarget for NSArray<T> {}
/// ```
pub unsafe trait DowncastTarget: ClassType + 'static {}
4 changes: 2 additions & 2 deletions crates/objc2/src/exception.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ mod tests {
let obj = NSObject::new();
// TODO: Investigate why this is required on GNUStep!
let _obj2 = obj.clone();
let obj: Retained<Exception> = unsafe { Retained::cast(obj) };
let obj: Retained<Exception> = unsafe { Retained::cast_unchecked(obj) };
let ptr: *const Exception = &*obj;

let result = unsafe { catch(|| throw(obj)) };
Expand All @@ -401,7 +401,7 @@ mod tests {
#[ignore = "currently aborts"]
fn throw_catch_unwind() {
let obj = NSObject::new();
let obj: Retained<Exception> = unsafe { Retained::cast(obj) };
let obj: Retained<Exception> = unsafe { Retained::cast_unchecked(obj) };

let result = catch_unwind(|| throw(obj));
let _ = result.unwrap_err();
Expand Down
2 changes: 2 additions & 0 deletions crates/objc2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ compile_error!("The `std` feature currently must be enabled.");
extern crate alloc;
extern crate std;

pub use self::downcast::DowncastTarget;
#[doc(no_inline)]
pub use self::encode::{Encode, Encoding, RefEncode};
pub use self::main_thread_marker::MainThreadMarker;
Expand Down Expand Up @@ -197,6 +198,7 @@ macro_rules! __hash_idents {
pub mod __framework_prelude;
#[doc(hidden)]
pub mod __macro_helpers;
mod downcast;
pub mod encode;
pub mod exception;
pub mod ffi;
Expand Down
8 changes: 6 additions & 2 deletions crates/objc2/src/macros/declare_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,14 +314,14 @@
/// let obj = MyCustomObject::new(3);
/// assert_eq!(obj.ivars().foo, 3);
/// assert_eq!(obj.ivars().bar, 42);
/// assert!(obj.ivars().object.is_kind_of::<NSObject>());
/// assert!(obj.ivars().object.isKindOfClass(NSObject::class()));
///
/// # let obj: Retained<MyCustomObject> = unsafe { msg_send_id![&obj, copy] };
/// # #[cfg(available_in_foundation)]
/// let obj = obj.copy();
///
/// assert_eq!(obj.get_foo(), 3);
/// assert!(obj.get_object().is_kind_of::<NSObject>());
/// assert!(obj.get_object().isKindOfClass(NSObject::class()));
///
/// assert!(MyCustomObject::my_class_method());
/// }
Expand Down Expand Up @@ -523,6 +523,10 @@ macro_rules! declare_class {
$crate::__declare_class_output_impls! {
$($impls)*
}

// SAFETY: This macro only allows non-generic classes and non-generic
// classes are always valid downcast targets.
unsafe impl $crate::DowncastTarget for $name {}
};
}

Expand Down
15 changes: 15 additions & 0 deletions crates/objc2/src/macros/extern_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,13 @@ macro_rules! __inner_extern_class {
}
}

// SAFETY: This maps `SomeClass<T, ...>` to a single `SomeClass<AnyObject, ...>` type and
// implements `DowncastTarget` on that type. This is safe because the "base container" class
// is the same and each generic argument is replaced with `AnyObject`, which can represent
// any Objective-C class instance.
$(#[$impl_m])*
unsafe impl $crate::DowncastTarget for $name<$($crate::__extern_class_map_anyobject!($t_for)),*> {}

$(#[$impl_m])*
unsafe impl<$($t_for $(: $(?$b_sized_for +)? $b_for)?),*> ClassType for $for {
type Super = $superclass;
Expand All @@ -360,6 +367,14 @@ macro_rules! __inner_extern_class {
};
}

#[doc(hidden)]
#[macro_export]
macro_rules! __extern_class_map_anyobject {
($t:ident) => {
$crate::runtime::AnyObject
};
}

#[doc(hidden)]
#[macro_export]
macro_rules! __extern_class_impl_traits {
Expand Down
100 changes: 81 additions & 19 deletions crates/objc2/src/rc/retained.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use core::panic::{RefUnwindSafe, UnwindSafe};
use core::ptr::{self, NonNull};

use super::AutoreleasePool;
use crate::runtime::{objc_release_fast, objc_retain_fast};
use crate::{ffi, ClassType, Message};
use crate::runtime::{objc_release_fast, objc_retain_fast, AnyObject};
use crate::{ffi, ClassType, DowncastTarget, Message};

/// A reference counted pointer type for Objective-C objects.
///
Expand Down Expand Up @@ -281,22 +281,83 @@ impl<T: ?Sized + Message> Retained<T> {

// TODO: Add ?Sized bound
impl<T: Message> Retained<T> {
/// Attempt to downcast the object to a class of type `U`.
///
/// See [`AnyObject::downcast_ref`] for more details.
///
/// # Errors
///
/// If casting failed, this will return the object back as the [`Err`]
/// type. If you do not care about this, and just want an [`Option`], use
/// `.downcast().ok()`.
///
/// # Example
///
/// Cast a string to an object, and back again.
///
/// ```
/// use objc2_foundation::{NSString, NSObject};
///
/// let string = NSString::new();
/// // The string is an object
/// let obj = string.downcast::<NSObject>().unwrap();
/// // And it is also a string
/// let string = obj.downcast::<NSString>().unwrap();
/// ```
///
/// Try to cast an object to a string, which will fail and return the
/// object in [`Err`].
///
/// ```
/// use objc2_foundation::{NSString, NSObject};
///
/// let obj = NSObject::new();
/// let obj = obj.downcast::<NSString>().unwrap_err();
/// ```
//
// NOTE: This is _not_ an associated method, since we want it to be easy
// to call, and it does not conflict with `AnyObject::downcast_ref`.
#[inline]
pub fn downcast<U: DowncastTarget>(self) -> Result<Retained<U>, Retained<T>>
where
Self: 'static,
{
let ptr: *const AnyObject = Self::as_ptr(&self).cast();
// SAFETY: All objects are valid to re-interpret as `AnyObject`, even
// if the object has a lifetime (which it does not in our case).
let obj: &AnyObject = unsafe { &*ptr };

if obj.is_kind_of_class(U::class()).as_bool() {
// SAFETY: Just checked that the object is a class of type `U`,
// and `T` is `'static`.
//
// Generic `U` like `NSArray<NSString>` are ruled out by
// `U: DowncastTarget`.
Ok(unsafe { Self::cast_unchecked::<U>(self) })
} else {
Err(self)
}
}

/// Convert the type of the given object to another.
///
/// This is equivalent to a `cast` between two pointers.
///
/// See [`Retained::into_super`] and [`ProtocolObject::from_retained`] for
/// safe alternatives.
/// See [`Retained::into_super`], [`ProtocolObject::from_retained`] and
/// [`Retained::downcast`] for safe alternatives.
///
/// This is common to do when you know that an object is a subclass of
/// a specific class (e.g. casting an instance of `NSString` to `NSObject`
/// is safe because `NSString` is a subclass of `NSObject`).
/// is safe because `NSString` is a subclass of `NSObject`), but do not
/// want to pay the (very slight) performance price of dynamically
/// checking that precondition with a [`downcast`].
///
/// All `'static` objects can safely be cast to [`AnyObject`], since that
/// assumes no specific class.
///
/// [`AnyObject`]: crate::runtime::AnyObject
/// [`ProtocolObject::from_retained`]: crate::runtime::ProtocolObject::from_retained
/// [`downcast`]: Self::downcast
///
///
/// # Safety
Expand All @@ -309,24 +370,26 @@ impl<T: Message> Retained<T> {
///
/// Additionally, you must ensure that any safety invariants that the new
/// type has are upheld.
///
/// Note that it is generally discouraged to cast e.g. `NSString` to
/// `NSMutableString`, even if you've checked at runtime that the object
/// is an instance of `NSMutableString`! This is because APIs are
/// generally allowed to return mutable objects internally, but still
/// assume that no-one mutates those objects if the API declares the
/// object as immutable, see [Apple's documentation on this][recv-mut].
///
/// [recv-mut]: https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/ObjectMutability/ObjectMutability.html#//apple_ref/doc/uid/TP40010810-CH5-SW66
#[inline]
pub unsafe fn cast<U: Message>(this: Self) -> Retained<U> {
pub unsafe fn cast_unchecked<U: Message>(this: Self) -> Retained<U> {
let ptr = ManuallyDrop::new(this).ptr.cast();
// SAFETY: The object is forgotten, so we have +1 retain count.
//
// Caller verifies that the returned object is of the correct type.
unsafe { Retained::new_nonnull(ptr) }
}

/// Deprecated alias of [`Retained::cast_unchecked`].
///
/// # Safety
///
/// See [`Retained::cast_unchecked`].
#[inline]
#[deprecated = "Use `downcast`, or `cast_unchecked` instead"]
pub unsafe fn cast<U: Message>(this: Self) -> Retained<U> {
unsafe { Self::cast_unchecked(this) }
}

/// Retain the pointer and construct an [`Retained`] from it.
///
/// Returns `None` if the pointer was NULL.
Expand Down Expand Up @@ -640,7 +703,7 @@ where
// - Both types are `'static`, so no lifetime information is lost
// (this could maybe be relaxed a bit, but let's be on the safe side
// for now).
unsafe { Self::cast::<T::Super>(this) }
unsafe { Self::cast_unchecked::<T::Super>(this) }
}
}

Expand Down Expand Up @@ -884,11 +947,10 @@ mod tests {
let expected = ThreadTestData::current();

// SAFETY: Any object can be cast to `AnyObject`
let obj: Retained<AnyObject> = unsafe { Retained::cast(obj) };
let obj: Retained<AnyObject> = unsafe { Retained::cast_unchecked(obj) };
expected.assert_current();

// SAFETY: The object was originally `__RcTestObject`
let _obj: Retained<RcTestObject> = unsafe { Retained::cast(obj) };
let _obj: Retained<RcTestObject> = Retained::downcast(obj).unwrap();
expected.assert_current();
}

Expand Down
6 changes: 3 additions & 3 deletions crates/objc2/src/runtime/declare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -935,9 +935,9 @@ mod tests {
assert_ne!(hashstate1.finish(), hashstate2.finish());

// isKindOfClass:
assert!(obj1.is_kind_of::<NSObject>());
assert!(obj1.is_kind_of::<Custom>());
assert!(obj1.is_kind_of::<Custom>());
assert!(obj1.isKindOfClass(NSObject::class()));
assert!(obj1.isKindOfClass(Custom::class()));
assert!((**obj1).isKindOfClass(Custom::class()));
}

#[test]
Expand Down
Loading

0 comments on commit 5780702

Please sign in to comment.