madsmtm / objc2

Bindings to Apple's frameworks in Rust
https://docs.rs/objc2/
MIT License
290 stars 35 forks source link

`declare_class!`: Allow safe overriding of methods / safe implementation of protocols #437

Open madsmtm opened 1 year ago

madsmtm commented 1 year ago

It would be nice if icrate could mark certain protocols / methods as safe to override, such that declare_class! could (after checking that all types are the same) allow just doing:

declare_class!(
    // ...

    impl MyClass {
        #[method_id(init)]
        fn init(this: Allocated<Self>) -> Id<Self> {
            todo!()
        }
    }

    impl MyProtocol for MyClass {
        #[method(protocolMethod)]
        fn protocol_method(&self) {}
    }
);

(Notice the lack of unsafe impl)

madsmtm commented 11 months ago

Actually, creating news methods (i.e. not overriding) is safe (provided they are not special methods like retain, or private underscored methods).

So perhaps we need a #[override] attribute (that is checked at runtime), similar to Swift?

madsmtm commented 11 months ago

Okay so we have two cases that I see a solution to:

The hardest part here is overriding methods. At the very least, it requires an #[override] attribute, to also allow calling the super method safely: https://github.com/madsmtm/objc2/issues/455

But checking that the signatures are equal is difficult! Possibly we could do the following check in the type system: Self::Super::$method_name == Self::$method_name? Though that won't work for overriding methods further up in the superclass hierarchy, and it would also fail for methods mentioning Self.

Note that having matching method encodings is not enough to make overriding safe.

madsmtm commented 11 months ago

Another option is to have traits for each implementation, that the user can then select. So e.g.:

trait NSView_methods {
    fn drawRect(&self, rect: NSRect);
}

// Usage

declare_class!(
    struct MyView;

    // unsafe impl ClassType ...

    impl NSView_methods for MyView {
        #[method(drawRect:)]
        fn drawRect(&self, rect: NSRect) {
            // Do drawing
        }
    }
);

Though this is just... Not very nice, and can be confusing to find the right trait for the methods you want (especially for NSView, which has a lot of categories).

madsmtm commented 11 months ago

Though maybe the idea of specifying which class' method you're overriding is not a bad one? It would allow us to get the type from NSView::drawRect directly, and maybe with a bit (read: a lot) of type system and macro trickery, we can compare the two methods anyhow? Though I'm doubtful it's going to be particularly stable, since we also want the user to be able to write fn drawRect(self: &MyView, rect: NSRect) { ... }, which while a bit contrived, still needs to work.

madsmtm commented 11 months ago

Note that we're assuming the user uses the same method names as the superclass/protocol, which they don't necessarily have to, so we also need a way to verify that.

A nice thing about that though is that we could maybe avoid the need to specify a selector in declare_class!? That would make it very "Rusty"!