Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add downcasting capability based on isKindOfClass: #474

Merged
merged 13 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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