-
Notifications
You must be signed in to change notification settings - Fork 40
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
Downgrading using Deref
#58
Comments
Should also consider the safety of "upgrading", e.g. EDIT: While it may be possible in some instances (and even necessary, looking at you |
Instead, it is present on the types that actually need it. This is in preparation for bigger changes to objc2-foundation, see #58.
Instead, it is present on the types that actually need it. This is in preparation for bigger changes to objc2-foundation, see #58.
Downside: Objects like |
Regarding downcasting pub unsafe trait ObjectType: Message {}
pub unsafe trait NonRootObjectType: ObjectType {
type Super: ObjectType;
fn as_super(&self) -> &Self::Super;
fn as_super_mut(&mut self) -> &mut Self::Super;
}
impl<T: NonRootObjectType, O: Ownership> Id<T, O> {
pub fn into_super(self) -> Id<T::Super, O> { // as_deref
unsafe { self::transmute(self) } // Because of guarantees that `NonRootObjectType` requires
}
} Very similar to Alternative would be to place restrictions on |
Yet another thing: For better ergonomics many APIs will want to take But how should we support it for EDIT: Code idea: pub unsafe trait SubclassOf<T: Message>: Message { // Similar to `AsRef`
fn as_super(&self) -> &T;
fn as_super_mut(&mut self) -> &mut T;
}
// All objects are subclasses of themselves
unsafe impl<T: Message> SubclassOf<T> for T {
fn as_super(&self) -> &T {
self
}
fn as_super_mut(&mut self) -> &mut T {
self
}
}
impl<T: Message, U: Message, O: Ownership> Id<T, O>
where:
T: SubclassOf<U>,
{
pub fn into_super(self) -> Id<U, O> {
unsafe { self::transmute(self) } // Because of guarantees that `SubclassOf` requires
}
} |
See also: https://rust-unofficial.github.io/patterns/anti_patterns/deref.html (We should link to this in docs when this is done) |
Another downside: With this change we lose the ability to generically create classes of a certain type (e.g. generically creating an instance of A bit bigger issue is that class methods also can't be called generically. |
This bears resemblance to subtyping and variance, we should consider the safety in light of that. EDIT: I did, results below: Objective-C is designed with this somewhat in mind already: Looking at the variance table, we can determine that the following situations are always safe:
Interestingly, the conversion fn evil_feeder(pet: &mut NSAnimal) {
let spike: Id<NSDog, Owned> = ...;
let spike: Id<NSAnimal, Owned> = Id::into_superclass(spike);
*pet = spike; // Doesn't work, since `NSAnimal` is a ZST (and in the future, !Sized)
// mem::replace wouldn't work either
}
fn main() {
let mut mr_snuggles: Id<NSCat, Owned> = ...;
evil_feeder(&mut mr_snuggles); // Replaces mr_snuggles with a Dog
mr_snuggles.meow(); // OH NO, MEOWING DOG!
} This is also why we can allow In essence, since the actual instance of the class in Objective-C is moved to the runtime, we can soundly modify the class (call a mutating method) because the runtime remembers all the constraints of the original class. |
Note:
|
We often want to send messages to methods defined on the superclass; in Objective-C this is automatically supported by the runtime, and we utilize this in
objc2_foundation
by creating traits for each superclassINSObject
,INSString
, ... and implement them for our object.The problems
Usage is more cumbersome for the user, since they have to import both the object they want to use and all the traits for the superclasses. E.g. when using
NSMutableArray
you often have to do:(Could be solved by adding a
prelude
module which exports these traits).When defining functions taking an object the creator has to remember to create a generic function over the helper trait, instead of just taking the object they were interested in:
The traits contain default functions (e.g.
INSObject::description
), which are then made generic on use (the compiler createsNSObject::description
,NSString::description
,NSArray::description
, ...). These functions are identical however, leading to an increase in code-size.The solution
Rust does not have inheritance, but it has something that can emulate it;
Deref
/DerefMut
! Using these, we can "downgrade" objects, e.g. convert&NSMutableString
to&NSString
, and that to&NSObject
, and use methods defined onNSObject
.This is not a new idea,
fruity
uses this approach, see itssubclass!
andobjc_subclass!
macros, though they don't create mutable references to objects.objrs
does it as well in their#[objrs(class, ...)]
macro.Safety
Since objects are always just pointers into heap, and
msg_send!
doesn't care if the given pointer has one type or the other, the pointer casting part should be safe. However, we also have to ensure Rust's safety invariants are upheld, especially in relation to unique/aliased references and lifetimes.Notable is that
NSObject::new
wouldn't be callable fromNSString
, so you at least can't end up with thinking you have aNSString
when the actual class isNSObject
.Some cases to consider:
&NSString
->&NSObject
safe?NSString
can be used in exactly the same way as anNSObject
can. Also, implementingINSObject
forNSString
has the same effect.NSObject::new
is not callable fromNSString
, so we can't accidentally create anId<NSString, Owned>
that wayNSObject::copy
would not be present because not allNSObject
s implementNSCopying
, but it would be safe in this case (becauseNSString
implementNSCopying
).We haveDone in RemoveINSObject::Ownership
right now, which other things (NSArray
?) might make extra assumptions from; we should perhaps move it toNSCopying::Ownership
?INSObject::Ownership
#90.x: &NSString
andy: &NSObject
, pointing to the same object.&mut NSString
->&mut NSObject
safe?&mut NSObject
is tied to the original lifetime (and there is no way to get e.g.Id<NSObject, Owned>
from that safely!)&NSArray<T, O>
->&NSObject
safe?T
is still present in the returned reference.&NSMutableArray<T, O>
->&NSArray<T, O>
safe?NSMutableString -> NSString
discussion.Id<NSString, Shared>
->Id<NSObject, Shared>
safe?NSString
is'static
anyway, so this is safe.Id<NSString, Shared>
->Id<NSObject, Owned>
safe?Id<NSString, Owned>
->Id<NSObject, Owned>
safe?NSString
would be consumed, and only the ownedNSObject
would remain; we would be free to mutate it as an ordinary object, since it is an ordinary object.Id<NSString, Owned>
->Id<NSObject, Shared>
safe?Id
sFrom
implementation, and the above.Id<NSMutableString, Owned>
->Id<NSString, Owned>
safe?&mut NSString
is becausecopy
returns the same string (and we would get aliasing references); but an instance that is actuallyNSMutableString
, that we just treat as anNSString
, will always return a newNSString
incopy
.&MyObject<'a>
->&NSObject
safe?Id<MyObject<'a>, Shared>
->Id<NSObject, Shared>
safe?Id<NSArray<T, O>, O>
->Id<NSObject, O>
safe?T: 'static
!&mut NSObject
? Can't we just do without?Ergonomics
Id
s? An inherent method onId
- I went with a trait, see AddClassType
trait #234Addfn as_deref<T: Deref>(x: Id<T, O>) -> Id<T::Target, O>
? Inherent or associated method? What should the bounds onT::Target
be?The text was updated successfully, but these errors were encountered: