You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 superclass INSObject, 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:
use objc2_foundation::{NSMutableArray,INSMutableArray,INSArray,INSObject};
(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:
use objc2_foundation::{INSObject,INSString,NSString};fnmy_fn(obj:&implINSObject){// Instead of just `obj: &NSObject`println!("{}", obj.description())}let obj = NSString::new();my_fn(&obj);
The traits contain default functions (e.g. INSObject::description), which are then made generic on use (the compiler creates NSObject::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 on NSObject.
This is not a new idea, fruity uses this approach, see its subclass! and objc_subclass! macros, though they don't create mutable references to objects. objrsdoes 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 from NSString, so you at least can't end up with thinking you have a NSString when the actual class is NSObject.
Some cases to consider:
Is &NSString -> &NSObject safe?
Yes NSString can be used in exactly the same way as an NSObject can. Also, implementing INSObject for NSString has the same effect.
Note that NSObject::new is not callable from NSString, so we can't accidentally create an Id<NSString, Owned> that way
NSObject::copy would not be present because not all NSObjects implement NSCopying, but it would be safe in this case (because NSString implement NSCopying).
We have INSObject::Ownership right now, which other things (NSArray?) might make extra assumptions from; we should perhaps move it to NSCopying::Ownership? Done in Remove INSObject::Ownership #90.
Note also that you could have two different variables at the same time, x: &NSString and y: &NSObject, pointing to the same object.
Is &mut NSString -> &mut NSObject safe?
Yes. Mutability does not change anything in this consideration, since the lifetime of &mut NSObject is tied to the original lifetime (and there is no way to get e.g. Id<NSObject, Owned> from that safely!)
Is &NSArray<T, O> -> &NSObject safe?
Type information is discarded!
But the lifetime of T is still present in the returned reference.
Is &NSMutableArray<T, O> -> &NSArray<T, O> safe?
Yes, the type parameters have the same semantic meaning.
See also the NSMutableString -> NSString discussion.
Is Id<NSString, Shared> -> Id<NSObject, Shared> safe?
Equivalent to the first point, except there is no lifetime carried over. NSString is 'static anyway, so this is safe.
Is Id<NSString, Shared> -> Id<NSObject, Owned> safe?
Of course no - this would violate aliasing rules. Included for completeness.
Is Id<NSString, Owned> -> Id<NSObject, Owned> safe?
The owned NSString would be consumed, and only the owned NSObject would remain; we would be free to mutate it as an ordinary object, since it is an ordinary object.
Is Id<NSString, Owned> -> Id<NSObject, Shared> safe?
Can be done through Ids From implementation, and the above.
Is Id<NSMutableString, Owned> -> Id<NSString, Owned> safe?
On initial inspection no.
But, it actually is! Remember, the reason we want to avoid &mut NSString is because copy returns the same string (and we would get aliasing references); but an instance that is actually NSMutableString, that we just treat as an NSString, will always return a new NSString in copy.
Is &MyObject<'a> -> &NSObject safe?
Yes, the lifetime information is contained in the reference!
Note that retaining an object that you only have a reference to is already not safe, so we can just piggy-back on that.
Is Id<MyObject<'a>, Shared> -> Id<NSObject, Shared> safe?
No, lifetime information is discarded. Same solution as above.
Is Id<NSArray<T, O>, O> -> Id<NSObject, O> safe?
Only when T: 'static!
Do we even need &mut NSObject? Can't we just do without?
I think it's fine to have, as far as I can see it doesn't cause us any problems.
Ergonomics
How do we downcast Ids? An inherent method on Id - I went with a trait, see Add ClassType trait #234
Add fn as_deref<T: Deref>(x: Id<T, O>) -> Id<T::Target, O>? Inherent or associated method? What should the bounds on T::Target be?
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_foundationby 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
NSMutableArrayyou often have to do:(Could be solved by adding a
preludemodule 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&NSMutableStringto&NSString, and that to&NSObject, and use methods defined onNSObject.This is not a new idea,
fruityuses this approach, see itssubclass!andobjc_subclass!macros, though they don't create mutable references to objects.objrsdoes 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::newwouldn't be callable fromNSString, so you at least can't end up with thinking you have aNSStringwhen the actual class isNSObject.Some cases to consider:
&NSString->&NSObjectsafe?NSStringcan be used in exactly the same way as anNSObjectcan. Also, implementingINSObjectforNSStringhas the same effect.NSObject::newis not callable fromNSString, so we can't accidentally create anId<NSString, Owned>that wayNSObject::copywould not be present because not allNSObjects implementNSCopying, but it would be safe in this case (becauseNSStringimplementNSCopying).We haveDone in RemoveINSObject::Ownershipright now, which other things (NSArray?) might make extra assumptions from; we should perhaps move it toNSCopying::Ownership?INSObject::Ownership#90.x: &NSStringandy: &NSObject, pointing to the same object.&mut NSString->&mut NSObjectsafe?&mut NSObjectis tied to the original lifetime (and there is no way to get e.g.Id<NSObject, Owned>from that safely!)&NSArray<T, O>->&NSObjectsafe?Tis still present in the returned reference.&NSMutableArray<T, O>->&NSArray<T, O>safe?NSMutableString -> NSStringdiscussion.Id<NSString, Shared>->Id<NSObject, Shared>safe?NSStringis'staticanyway, so this is safe.Id<NSString, Shared>->Id<NSObject, Owned>safe?Id<NSString, Owned>->Id<NSObject, Owned>safe?NSStringwould be consumed, and only the ownedNSObjectwould 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?IdsFromimplementation, and the above.Id<NSMutableString, Owned>->Id<NSString, Owned>safe?&mut NSStringis becausecopyreturns 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 newNSStringincopy.&MyObject<'a>->&NSObjectsafe?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
Ids? An inherent method onId- I went with a trait, see AddClassTypetrait #234Addfn as_deref<T: Deref>(x: Id<T, O>) -> Id<T::Target, O>? Inherent or associated method? What should the bounds onT::Targetbe?