Skip to content

Commit

Permalink
Add downcasting helper through isKindOfClass:
Browse files Browse the repository at this point in the history
  • Loading branch information
cynecx committed Aug 15, 2023
1 parent 3d9b686 commit 0a552f4
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 10 deletions.
41 changes: 41 additions & 0 deletions crates/objc2/src/downcast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use crate::ClassType;

/// [`DowncastTarget`] is an unsafe marker trait that can be implemented on types that also
/// implement [`ClassType`]. This is a promise that the self-type is a valid "downcast target"
/// (What this exactly means is described in the Safety section).
///
/// # Safety
///
/// Ideally, every type that implements `ClassType` can 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.
///
/// In this case:
///
/// ```ignore
/// let obj: Id<NSObject> = Id::into_super(NSString::new());
/// // This works and is safe.
/// let obj: &NSString = obj.downcast::<NSString>().unwrap();
/// ```
///
/// `NSString` has a [`DowncastTarget`] implementation, which looks like:
///
/// ```ignore
/// // This is safe.
/// unsafe impl Downcastable for NSString { }
/// ```
///
/// However with generics:
///
/// ```ignore
/// let obj: Id<NSArray<NSString>> = NSArray::new();
/// // This is invalid and doesn't type check.
/// let obj = obj.downcast::<NSArray<NSData>>();
/// ```
///
/// The example above doesn't work because [`DowncastTarget`] isn't implemented for
/// `for<T> NSArray<T>`. Doing so would be unsound because downcasting can only trivially
/// determine whether the base class (in this case `NSArray`) matches the receiver class type.
///
pub unsafe trait DowncastTarget: ClassType {}
2 changes: 2 additions & 0 deletions crates/objc2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ extern "C" {}
pub use objc_sys as ffi;

pub use self::class_type::ClassType;
pub use self::downcast::DowncastTarget;
#[doc(no_inline)]
pub use self::encode::{Encode, Encoding, RefEncode};
pub use self::message::{Message, MessageArguments, MessageReceiver};
Expand All @@ -208,6 +209,7 @@ macro_rules! __hash_idents {
pub mod __macro_helpers;
mod class_type;
pub mod declare;
mod downcast;
pub mod encode;
pub mod exception;
mod macros;
Expand Down
24 changes: 16 additions & 8 deletions crates/objc2/src/macros/declare_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ macro_rules! declare_class {

$ivar_helper_module_v:vis mod $ivar_helper_module:ident;

unsafe impl ClassType for $for:ty {
unsafe impl ClassType for $for:ident {
$(#[inherits($($inheritance_rest:ty),+)])?
type Super = $superclass:ty;

Expand Down Expand Up @@ -419,7 +419,7 @@ macro_rules! declare_class {
$($fields:tt)*
}

unsafe impl ClassType for $for:ty {
unsafe impl ClassType for $for:ident {
$(#[inherits($($inheritance_rest:ty),+)])?
type Super = $superclass:ty;

Expand Down Expand Up @@ -463,7 +463,7 @@ macro_rules! declare_class {
$(#[$m:meta])*
$v:vis struct $name:ident;

unsafe impl ClassType for $for:ty {
unsafe impl ClassType for $for:ident {
$(#[inherits($($inheritance_rest:ty),+)])?
type Super = $superclass:ty;

Expand Down Expand Up @@ -509,7 +509,7 @@ macro_rules! __inner_declare_class {
{
($($ivar_helper_module:ident)?)

unsafe impl ClassType for $for:ty {
unsafe impl ClassType for $for:ident {
$(#[inherits($($inheritance_rest:ty),+)])?
type Super = $superclass:ty;

Expand Down Expand Up @@ -628,6 +628,14 @@ macro_rules! __inner_declare_class {
$crate::__declare_class_methods! {
$($methods)*
}

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

Expand All @@ -650,7 +658,7 @@ macro_rules! __declare_class_methods {
// With protocol
(
$(#[$m:meta])*
unsafe impl $protocol:ident for $for:ty {
unsafe impl $protocol:ident for $for:ident {
$($methods:tt)*
}

Expand All @@ -677,7 +685,7 @@ macro_rules! __declare_class_methods {
// Without protocol
(
$(#[$m:meta])*
unsafe impl $for:ty {
unsafe impl $for:ident {
$($methods:tt)*
}

Expand Down Expand Up @@ -712,7 +720,7 @@ macro_rules! __declare_class_register_methods {
($builder:ident)

$(#[$($m:tt)*])*
unsafe impl $protocol:ident for $for:ty {
unsafe impl $protocol:ident for $for:ident {
$($methods:tt)*
}

Expand Down Expand Up @@ -757,7 +765,7 @@ macro_rules! __declare_class_register_methods {
($builder:ident)

$(#[$($m:tt)*])*
unsafe impl $for:ty {
unsafe impl $for:ident {
$($methods:tt)*
}

Expand Down
21 changes: 21 additions & 0 deletions crates/objc2/src/macros/extern_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,18 @@ 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 ObjC class instance.
$(#[$impl_m])*
unsafe impl $crate::DowncastTarget
for $name<$($crate::__extern_class_map_anyobject!($t_for)),*>
where
Self: 'static,
{
}

$(#[$impl_m])*
unsafe impl<$($t_for $(: $b_for)?),*> ClassType for $for {
type Super = $superclass;
Expand All @@ -382,6 +394,15 @@ 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
40 changes: 39 additions & 1 deletion crates/objc2/src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,13 @@ pub(crate) use self::method_encoding_iter::{EncodingParseError, MethodEncodingIt
pub(crate) use self::retain_release_fast::{objc_release_fast, objc_retain_fast};
use crate::encode::__unstable::{EncodeArguments, EncodeConvertReturn, EncodeReturn};
use crate::encode::{Encode, Encoding, OptionEncode, RefEncode};
use crate::verify::{verify_method_signature, Inner};
use crate::DowncastTarget;
use crate::{ffi, Message};
use crate::{
sel,
verify::{verify_method_signature, Inner},
MessageReceiver,
};

// Note: While these are not public, they are still a breaking change to
// remove, since `icrate` relies on them.
Expand Down Expand Up @@ -1266,6 +1271,39 @@ impl AnyObject {
unsafe { *self.ivar_mut::<T>(name) = value };
}

/// Attempts to downcast self to class type `T` (which must be a valid
/// [downcast target](crate::DowncastTarget)).
///
/// # Notes
///
/// This works by calling `[self isKindOfClass:class]`. That also means that the receiver object
/// must have the instance method `isKindOfClass:`. This is usually the case for any object
/// that uses `NSObject` as the root. Downcasting will fail with `None` when `isKindOfClass:`
/// is not available or in general when `isKindOfClass:` returns false.
pub fn downcast<T>(&self) -> Option<&T>
where
Self: 'static,
T: DowncastTarget + 'static,
{
let is_kind_of_class = sel!(isKindOfClass:);

self.class()
.verify_sel::<(&AnyClass,), bool>(is_kind_of_class)
.ok()?;

if unsafe {
MessageReceiver::send_message::<(&AnyClass,), bool>(
self,
is_kind_of_class,
(T::class(),),
)
} {
Some(unsafe { &*(self as *const Self).cast::<T>() })
} else {
None
}
}

// objc_setAssociatedObject
// objc_getAssociatedObject
// objc_removeAssociatedObjects
Expand Down
4 changes: 3 additions & 1 deletion crates/objc2/src/runtime/nsobject.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use core::fmt;
use core::hash;

use crate::mutability::Root;
use crate::rc::{DefaultId, Id};
use crate::runtime::{AnyClass, AnyObject, AnyProtocol, ImplementedBy, ProtocolObject};
use crate::{extern_methods, msg_send, msg_send_id, Message};
use crate::{mutability::Root, DowncastTarget};
use crate::{ClassType, ProtocolType};

crate::__emit_struct! {
Expand Down Expand Up @@ -81,6 +81,8 @@ unsafe impl ClassType for NSObject {
}
}

unsafe impl DowncastTarget for NSObject {}

/// The methods that are fundamental to most Objective-C objects.
///
/// This represents the [`NSObject` protocol][proto].
Expand Down
1 change: 1 addition & 0 deletions crates/tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ catch-all = ["objc2/catch-all", "exception"]
Foundation_all = [
"icrate/Foundation",
"icrate/Foundation_NSString",
"icrate/Foundation_NSMutableString",
"icrate/Foundation_NSException",
"icrate/Foundation_NSArray",
"icrate/Foundation_NSNumber",
Expand Down
27 changes: 27 additions & 0 deletions crates/tests/src/test_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::os::raw::c_int;

#[cfg(feature = "Foundation_all")]
use icrate::Foundation::NSNumber;
use icrate::Foundation::{NSArray, NSException, NSMutableString, NSString};
use objc2::encode::{Encoding, RefEncode};
use objc2::rc::{autoreleasepool, AutoreleasePool, Id};
use objc2::runtime::{
Expand Down Expand Up @@ -329,3 +330,29 @@ fn test_protocol() {
// Check that transforming to `NSObject` works
let _obj: &ProtocolObject<NSObject> = ProtocolObject::from_ref(&*proto);
}

#[test]
fn downcast_basics() {
let obj = NSString::new();
assert!(matches!(obj.downcast::<NSString>(), Some(_)));

let obj = Id::into_super(obj);
assert!(matches!(obj.downcast::<NSNumber>(), None));
assert!(matches!(obj.downcast::<NSString>(), Some(_)));

let obj = NSMutableString::new();
assert!(matches!(obj.downcast::<NSMutableString>(), Some(_)));
assert!(matches!(obj.downcast::<NSString>(), Some(_)));
assert!(matches!(obj.downcast::<NSObject>(), Some(_)));
assert!(matches!(obj.downcast::<NSException>(), None));

let obj = Id::into_super(Id::into_super(obj));
assert!(matches!(obj.downcast::<NSMutableString>(), Some(_)));
assert!(matches!(obj.downcast::<NSString>(), Some(_)));
assert!(matches!(obj.downcast::<NSObject>(), Some(_)));
assert!(matches!(obj.downcast::<NSException>(), None));

let obj: Id<NSArray<NSString>> = NSArray::new();
assert!(matches!(obj.downcast::<NSString>(), None));
assert!(matches!(obj.downcast::<NSArray<AnyObject>>(), Some(_)));
}

0 comments on commit 0a552f4

Please sign in to comment.