objc2 icon indicating copy to clipboard operation
objc2 copied to clipboard

The deal with mutability

Open madsmtm opened this issue 1 year ago • 3 comments

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:

  1. Make NSArray<T>, NSDictionary<K, V> and such collection types simpler
  2. 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.
  3. It would be possible for users to inherit NSString, without them having to ensure that their object was Sync
  4. Id::retain could be made safe(r?)
  5. automatic bindings would be simpler
  6. Using NSMutableString inside a declare_class! that is not meant to be thread-safe anyhow is easier (e.g. means we won't have to use &mut in winit)

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).

madsmtm avatar Sep 08 '22 13:09 madsmtm

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.

madsmtm avatar Sep 12 '22 22:09 madsmtm

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.

madsmtm avatar Sep 12 '22 22:09 madsmtm

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.

madsmtm avatar Oct 05 '22 00:10 madsmtm

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)?

madsmtm avatar Nov 03 '22 22:11 madsmtm