objc2
objc2 copied to clipboard
The deal with mutability
I was once convinced that we could restrict mutation to &mut self
methods, but after having battled with AppKit and seen how Swift does it, I'm beginning to doubt that this is the best way.
NSString
is immutable while NSMutableString
is, naturally, mutable. Since our current implementation only allows mutation of NSMutableString
through &mut self
, it is safe to make both of these Send + Sync
; all is well and Rusty.
Now, remember that &NSMutableString -> &NSString
and Id<NSMutableString, O> -> Id<NSString, O>
is safe - so if we were to make mutation of NSMutableString
possible through &self
, not only would NSMutableString
be !Sync
, but so would NSString
! (They could still be Send
, that would simply allow moving them via. Id<T, Owned>
to a different thread - quite similar to std::cell::Cell
)
That's the primary downside: Our objects can no longer be shared across threads. Downsides that usually apply to Rust code (aliasing optimizations, ...) are void in our case, since NSString
is already UnsafeCell
.
On the other hand, if we were to remove mutability we could:
- Make
NSArray<T>
,NSDictionary<K, V>
and such collection types simpler - Since most frameworks use interior mutability and/or are not thread safe anyhow, the usage of the Foundation framework would match the usage of these.
- It would be possible for users to inherit
NSString
, without them having to ensure that their object wasSync
-
Id::retain
could be made safe(r?) - automatic bindings would be simpler
- Using
NSMutableString
inside adeclare_class!
that is not meant to be thread-safe anyhow is easier (e.g. means we won't have to use&mut
inwinit
)
So... Yeah, will think about this a bit, but I think we may have to sacrifice being able to use Objective-C classes across threads (exactly the same compromise Swift does, their String
is Sendable
but NSString
is not).
https://github.com/rust-windowing/winit/pull/2457 highlighted an issue: We'd have to return Id<WinitView, Owned>
if we wanted to make accepts_first_mouse
mutable after the fact - but you can very rarely have an owned view (because other code may hold references to it, unknown to you), so this would be a footgun.
Instead, making it Id<WinitView, Shared>
and then contain accepts_first_mouse: Cell<bool>
would be more correct.
Note that this is not currently possible because the layout of Cell
is not guaranteed, will request this from std
at some point.
Even ignoring thread safety, a problem arises with methods that return references to internal data. For example, the following fails at compile-time because set_bytes
requires &mut self
, but if we were to change things to no longer require mutable pointers, it would cause UB:
let mut data = NSMutableData::with_bytes(b"abc");
let b = data.bytes();
data.set_bytes(b"def");
assert_eq!(b, b"abc");
Similarly for -[NSString UTF8String]
, other methods annotated with NS_RETURNS_INNER_POINTER
, and probably also the collection types - perhaps also when iterating?
Swift "solves" this by just returning UnsafeRawPointer
. And when iterating over Array
, they make a local copy; when iterating over NSMutableArray
, they don't mark the add
method as mutating, so memory safety is quite easy to violate:
import Foundation
var arr = NSMutableArray(array: [1, 2, 3])
for elem in arr {
arr.add(4)
print("test", elem)
}
So yeah, I'm quite conflicted as to how we should do mutability; perhaps things will be clearer once I've done more of #264, and get to see if there are parts other than Foundation that would benefit from mutability.
Another solution: Just keep every mutating method unsafe
. Or maybe add two variants, one fn push(&mut self, item: &T)
and one unsafe fn push(&self, item: &T)
?