Skip to content
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

The deal with mutability #265

Closed
madsmtm opened this issue Sep 8, 2022 · 10 comments · Fixed by #419
Closed

The deal with mutability #265

madsmtm opened this issue Sep 8, 2022 · 10 comments · Fixed by #419
Labels
A-framework Affects the framework crates and the translator for them bug Something isn't working documentation Improvements or additions to documentation help wanted Extra attention is needed
Milestone

Comments

@madsmtm
Copy link
Owner

madsmtm commented Sep 8, 2022

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 madsmtm added bug Something isn't working documentation Improvements or additions to documentation help wanted Extra attention is needed A-framework Affects the framework crates and the translator for them labels Sep 8, 2022
@madsmtm madsmtm added this to the objc2 v0.3 milestone Sep 8, 2022
@madsmtm
Copy link
Owner Author

madsmtm commented Sep 12, 2022

rust-windowing/winit#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
Copy link
Owner Author

madsmtm commented Sep 12, 2022

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
Copy link
Owner Author

madsmtm commented Oct 5, 2022

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
Copy link
Owner Author

madsmtm commented Nov 3, 2022

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_unchecked(&self, item: &T)?

@madsmtm
Copy link
Owner Author

madsmtm commented Nov 10, 2022

Would maybe make sense to take the Ownership parameter from NSCopying and move it to ClassType?

@notgull
Copy link
Contributor

notgull commented Dec 17, 2022

Truth be told, I'm fine with having a standard class that's !Send/!Sync and then having an escape-hatch unsafe adaptor that is Send/Sync. In the wide variety of cases, these types end up being thread-unsafe anyhow.

@madsmtm
Copy link
Owner Author

madsmtm commented Dec 21, 2022

Yeah if it was only thread safety that was the issue, then I would agree, but as noted in #265 (comment), that still won't allow us to make mutating methods like addObject: safe

@madsmtm
Copy link
Owner Author

madsmtm commented Jan 18, 2023

An update: Cell will soon be Encode, and as such usable in instance variables - which should allow using NSMutableString in non-Sync classes.

More importantly, Swift recently implemented a better concept of sendability, see NS_SWIFT_SENDABLE and NS_SWIFT_UI_ACTOR in Foundation/NSObjCRuntime.h.
Using these attributes, we can automatically mark NSError, NSDate, NSFileHandle and so on Send + Sync (not sure about NSLock and subclasses, seems like that shouldn't be either? Or at least not Send).

But, even more importantly, we can automatically mark NSView, NSWindow, ... as "needing a MainThreadMarker to be created". That is, for every function/method, do an algorithm like:

if method.return_type.includes_ns_swift_ui_actor() {
    if method.receiver.is_ns_swift_ui_actor() {
        break;
    }
    for argument in method.arguments {
        if argument.is_ns_swift_ui_actor() {
            break;
        }
    }
    method.arguments.push(("mtm", Ty::MainThreadMarker));
}

(Note here the difference between includes_ns_swift_ui_actor and is_ns_swift_ui_actor; includes_ returns true for NSArray<NSView> or Option<NSView>, while is_ doesn't).

So e.g. NSView::new() would become NSView::new(_mtm: MainThreadMarker), while NSWindow::view(&self) would stay as-is.

@madsmtm
Copy link
Owner Author

madsmtm commented Jan 23, 2023

A bit unsure where the MainThreadMarker argument is most convenient to put? As the first argument (after self, if present), or as the last one?

E.g. the difference between these two:

// As first
let window = NSWindow::initWithContentRect_styleMask_backing_defer(
    NSWindow::alloc(),
    MainThreadMarker::new(),
    NSRect::new(0.0, 0.0),
    NSWindowStyleMaskClosable
    | NSWindowStyleMaskMiniaturizable
    | NSWindowStyleMaskResizable
    | NSWindowStyleMaskTitled,
    NSBackingStoreBuffered,
    false,
);
// As last
let window = NSWindow::initWithContentRect_styleMask_backing_defer(
    NSWindow::alloc(),
    NSRect::new(0.0, 0.0),
    NSWindowStyleMaskClosable
    | NSWindowStyleMaskMiniaturizable
    | NSWindowStyleMaskResizable
    | NSWindowStyleMaskTitled,
    NSBackingStoreBuffered,
    false,
    MainThreadMarker::new(),
);

(Oh, fabulous contexts and capabilities, how you could relieve me of all my troubles with but the smallest of effort)!


EDIT: Parts of this is redundant since mtm.alloc(), the rest is moved to #359 (comment)

@madsmtm
Copy link
Owner Author

madsmtm commented Apr 21, 2023

The resolution after #419 is that we'll end up making most classes use interior mutability, but keep mutability in certain core classes like NSMutableString and NSMutableArray<T>, since that will allow us to do safe interior pointers and safe iteration.

Relating to the issues noted in the top comment, I believe most of them are addressed:

  1. Make NSArray<T>, NSDictionary<K, V> and such collection types simpler

Tracked in #316, also completed by the linked PR.

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

Interior mutability is the default setting for icrate, we only override it for a few select cases.

  1. It would be possible for users to inherit NSString, without them having to ensure that their object was Sync

Not sure one could do that anyhow, I think Objective-C itself may assume that NSString is thread-safe? In any case, not a big downside.

  1. Id::retain could be made safe(r?)

Tracked in #399, obj.retain() is safe and available for interior-mutable types.

  1. automatic bindings would be simpler

Since the type ownership is no longer on the Id, this problem is mitigated.

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

You can probably do Cell<Id<NSMutableString>> or similar to get that functionality? Or maybe RefCell<Id<NSMutableString>>. In any case, one can always make a custom helper class like NSMutableStringInteriorMutable instead.


So the only remaining things in this is the main thread safety stuff that is not really related to this issue, I'll track that in in #359 instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-framework Affects the framework crates and the translator for them bug Something isn't working documentation Improvements or additions to documentation help wanted Extra attention is needed
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants