objc2 icon indicating copy to clipboard operation
objc2 copied to clipboard

Add `extern_protocol!` macro and `ProtocolType` trait

Open madsmtm opened this issue 1 year ago • 5 comments

Unsure about how to handle this.

The need arose because MTLBuffer is a protocol, not an actual class (it can't be instantiated by itself), but is useful to handle it as-if it's a class that you're holding instances to.

See https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html

madsmtm avatar Aug 29 '22 04:08 madsmtm

Need to figure out how we reconcile protocols and traits - for example, NSCopying is useful as a trait, while MTLDevice is useful as a type. Ideally we'd have one thing called NSCopying, if not, we could perhaps have NSCopying and NSCopyingTrait, with a link between these somehow?

The last usecase of protocols would be like NSWindowDelegate, which is useful inside declare_class!, but that is even less thought-out than the other two.

madsmtm avatar Sep 04 '22 02:09 madsmtm

Note also that there is not really such a thing as a "super" protocol; a protocol can have any number of parent/super protocols (similar to a trait having zero or more supertraits).

madsmtm avatar Sep 09 '22 05:09 madsmtm

Idea:

unsafe trait ProtocolType {
    // May not always return an protocol for _reasons_???
    pub fn protocol() -> Option<&'static Protocol>;
}
extern_protocol!(
    #[derive(Debug)]
    pub struct NSCopying;

    unsafe impl ProtocolType for NSCopying {}

    pub trait NSCopyingProtocol {
        #[sel_id(copy)]
        fn copy(&self) -> Id<Self, Shared>;
    }
);

// Becomes

pub struct NSCopying {
    inner: Object,
}

impl NSCopyingProtocol for NSCopying {}

pub trait NSCopyingProtocol {
    fn copy(&self) -> Id<Self, Shared> {
        ...
    }
}

madsmtm avatar Sep 19 '22 16:09 madsmtm

Need to actually figure out the use-cases for protocols.

So far I have:

  • Many classes implement, want to call a method on the protocol (e.g. NSCopying)
    • Maybe interesting to be able to use as a bound?
  • Ability to interact with protocol as a normal message-able object (e.g. as done in Metal)

But there's bound to be more than this!

madsmtm avatar Sep 21 '22 01:09 madsmtm

A bit about "formal" and "informal" protocols: https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/Protocol.html

madsmtm avatar Oct 04 '22 14:10 madsmtm

I have a use case---I'm trying to write bindings for the AuthenticationServices framework and there's the ASAuthorizationProvider protocol.

Tbh I'm not entirely sure why this is a protocol rather than a superclass, but in the framework it's used as the return type for ASAuthorizationRequest's provider property.

I guess probably all I'd need out of some protocol binding macro is appropriate From and TryFrom implementations between the "protocol object" (not sure if that's the write term) and the implementing types.

ericmarkmartin avatar Oct 30 '22 19:10 ericmarkmartin

Thanks for the use-case, I think it's very similar to what Metal does.

Also, further ideas:

// in objc2
unsafe trait ConformsTo<P: ProtocolType>: Message {
    fn as_protocol(&self) -> &P {
        ...
    }
    fn as_protocol_mut(&mut self) -> &mut P {
        ...
    }
}
// + Id::into_protocol

// Usage
extern_protocol!(
    #[derive(Debug)]
    pub struct ASAuthorizationProvider;

    unsafe impl ProtocolType for ASAuthorizationProvider {
        // No methods
        // The useful part of declaring the methods in here is that we'd be able to
        // do extra some tricks when using `declare_class!`.
    }
);

// The set of protocols that the type implements
unsafe impl ConformsTo<NSObject> for ASAuthorizationProvider {}

// When defining these types, we can state which protocols they implement
unsafe impl ConformsTo<ASAuthorizationProvider> for ASAuthorizationAppleIDProvider {}
unsafe impl ConformsTo<ASAuthorizationProvider> for ASAuthorizationPasswordProvider {}
unsafe impl ConformsTo<ASAuthorizationProvider> for ASAuthorizationPlatformPublicKeyCredentialProvider {}
unsafe impl ConformsTo<ASAuthorizationProvider> for ASAuthorizationSecurityKeyPublicKeyCredentialProvider {}
unsafe impl ConformsTo<ASAuthorizationProvider> for ASAuthorizationSingleSignOnProvider {}

// Maybe with a macro so that we can `impl AsRef<ASAuthorizationProvider> for X`?

fn main() {
    let obj = ASAuthorizationAppleIDProvider::new();
    let proto: &ASAuthorizationProvider = obj.as_protocol();
    // Do something with the protocol
}

madsmtm avatar Oct 31 '22 19:10 madsmtm

Note: Some protocols are "virtual", see the objc_non_runtime_protocol attribute, so as part of this it would make sense to store information about the protocol's methods in a way that we could then check that the protocol is properly implemented in declare_class! (which we already do, but only for non-virtual protocols).

EDIT: Usage of this property is very rare, I confused it with something else.

madsmtm avatar Nov 03 '22 23:11 madsmtm

I took a look at a lot of protocols, as a summary I've tried to categorize the different usage of protocols as follows:

  1. The user can implement it to change the behaviour of some class (aka. delegates, or delegates in disguise). If not a delegate, these usually have existing classes that implement them.
  2. Capture common behaviour on a few classes, and the user wants to call those methods.
  3. Has no methods, meant as a "marker" protocol - may be interesting to use as a generic bound, also interesting to use as a concrete type.

A. Uses instancetype in return type to refer to the implementing class. B. Protocols that really require some sort of associated type because of arg/return type in their methods. C. The protocol is an "informal" protocol in the sense that types don't actually directly implement it, it is mostly there to make it look like they do (?)

Protocols in Foundation:

NSPortDelegate, NSURLDownloadDelegate, and anything else that ends with `Delegate`: 1
NSDecimalNumberBehaviors: 1
NSExtensionRequestHandling: 1
NSFilePresenter: 1
NSItemProviderWriting: 1
NSItemProviderReading: 1, A
NSCoding: 1, 2, A
NSLocking: 2
NSSecureCoding: 1
NSCopying: 2, B
NSMutableCopying: 2, B
NSFastEnumeration: 2, B
NSDiscardableContent: 1, (2)
NSProgressReporting: 1, (2)
NSURLAuthenticationChallengeSender: 1
NSURLHandleClient: 1
NSURLProtocolClient: 2
NSXPCProxyCreating: 2

Protocols in CoreData:

NSFetchedResultsSectionInfo: 2
NSFetchRequestResult: 3

Protocols in AppKit:

NSWindowDelegate, other delegates: 1
NSAccessibilityGroup: 3
NSAccessibility...: 1, 2
NSAlignmentFeedbackToken: 3
NSAnimatablePropertyContainer: 1, 2, A
NSAppearanceCustomization: 2
NSServicesMenuRequestor: 2, C
NSCollectionViewElement: 1
... many more!

Metal should mostly be case 2 as well (I at least know it doesn't do case A).


The current design with creating concrete types for each protocol, and allowing bounds to use ConformsTo, seems to work for case 1, 2, 3 and C, but falls a bit short in case A and B.

I think I'll go with defining custom traits for a few of those important cases like NSCopying, NSMutableCopying, NSFastEnumeration and NSCoding, and then NSItemProviderReading and NSAnimatablePropertyContainer will probably just have to live with it being a bit suboptimal (translating instancetype to Self is still sound in those cases, just not as precise as it could be).

madsmtm avatar Nov 11 '22 05:11 madsmtm

Wow, can't believe I didn't think of this before: Protocols can have class methods!

They're quite rare though, the only instances in Foundation + AppKit are the following (so understandable that I forgot this):

+[NSSecureCoding supportsSecureCoding] // property
+[NSItemProviderWriting writableTypeIdentifiersForItemProvider] // property
+[NSItemProviderWriting itemProviderVisibilityForRepresentationWithTypeIdentifier:]
+[NSItemProviderReading readableTypeIdentifiersForItemProvider] // property
+[NSItemProviderReading objectWithItemProviderData:typeIdentifier:error:]
+[NSAnimatablePropertyContainer defaultAnimationForKey:]
+[NSPasteboardReading readableTypesForPasteboard:]
+[NSPasteboardReading readingOptionsForType:pasteboard:]
+[NSWindowRestoration restoreWindowWithIdentifier:state:completionHandler:]

Question is: Is that few enough that we'll just shrug it off and say "we won't try to handle this", or do we need to figure out a way to handle this?

madsmtm avatar Nov 11 '22 09:11 madsmtm

We need some way to specify to the macro "this protocol inherits from NSObject", so that the __inner field can be NSObject, and #[derive(Debug, PartialEq, Eq, Hash)] works.

But maybe the macro can just extract those derives and call let obj: &NSObject = self.as_protocol();?

madsmtm avatar Nov 15 '22 15:11 madsmtm

Alternatively: We could allow specifying a parent type for the protocols that have a direct parent (such as MTLBuffer) - this would allow Deref impls to those as well, which would be beneficial for usability.

madsmtm avatar Nov 15 '22 15:11 madsmtm

I had an idea for how we might even further improve things, but in the interest of speeding this PR up, I moved that to https://github.com/madsmtm/objc2/issues/291.

Missing parts of this is basically just getting declare_class! to work with ConformsTo.

madsmtm avatar Nov 16 '22 20:11 madsmtm