Skip to content

Commit

Permalink
Update protocol documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
madsmtm committed Jan 27, 2023
1 parent 9535d9a commit b0ed481
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 67 deletions.
43 changes: 41 additions & 2 deletions crates/objc2/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,58 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased - YYYY-MM-DD

### Added
* Added `NSObjectProtocol` trait for allowing `ProtocolObject` to implement
`Debug`, `Hash`, `PartialEq` and `Eq`.
* Support `#[cfg(...)]` attributes in `extern_class!` macro.
* Added support for selectors with multiple colons like `abc::` in the `sel!`,
`extern_class!`, `extern_protocol!` and `declare_class!` macros.
* Added ability to use `#[method_id(mySelector:)]` inside `declare_class!`,
just like you would do in `extern_methods!`.
* Added 16-fold impls for `EncodeArguments`, `MessageArguments`, and `MethodImplementation`.
* Added `NSObjectProtocol` trait for allowing `ProtocolObject` to implement
`Debug`, `Hash`, `PartialEq` and `Eq`.

### Changed
* **BREAKING**: Using the automatic `NSError**`-to-`Result` functionality in
`extern_methods!` now requires a trailing underscore (so now it's
`#[method(myMethod:error:_)]` instead of `#[method(myMethod:error:)]`).
* **BREAKING**: Fundamentally changed how protocols work. Instead of being
structs with inherent methods, they're now traits. This means that you can
use their methods much more naturally from your Objective-C objects.

An example:
```rust
// Before
extern_protocol!(
struct MyProtocol;

unsafe impl ProtocolType for MyProtocol {
#[method(myMethod)]
fn myMethod(&self);
}
);

let obj: &SomeObjectThatImplementsTheProtocol = ...;
let proto: &MyProtocol = obj.as_protocol();
proto.myMethod();

// After
extern_protocol!(
unsafe trait MyProtocol {
#[method(myMethod)]
fn myMethod(&self);
}

unsafe impl ProtocolType for dyn MyProtocol {}
);

let obj: &SomeObjectThatImplementsTheProtocol = ...;
obj.myMethod();
// Or
let proto: &ProtocolObject<dyn MyProtocol> = ProtocolObject::from_ref(obj);
proto.myMethod();
```

The `ConformsTo` trait has similarly been removed, and the `ImplementedBy`
trait and `ProtocolObject` struct has been introduced instead.
* **BREAKING**: Moved `NSObject::is_kind_of` to the new `NSObjectProtocol`.

### Fixed
Expand Down
23 changes: 16 additions & 7 deletions crates/objc2/src/macros/declare_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
/// with cutting down on boilerplate, in particular when defining delegate
/// classes!
///
/// [`extern_class!`]: crate::extern_class
/// [`declare`]: crate::declare
///
///
/// # Specification
///
Expand Down Expand Up @@ -35,6 +38,7 @@
/// in [`declare::IvarDrop`]), this macro will generate a `dealloc` method
/// automatically.
///
/// [`declare::Ivar`]: crate::declare::Ivar
/// [`ClassType::class`]: crate::ClassType::class
/// [`declare::IvarDrop`]: crate::declare::IvarDrop
///
Expand Down Expand Up @@ -83,12 +87,16 @@
/// You can specify protocols that the class should implement, along with any
/// required/optional methods for said protocols.
///
/// The protocol must have been previously defined with [`extern_protocol!`].
///
/// The methods work exactly as normal, they're only put "under" the protocol
/// definition to make things easier to read.
///
/// Putting attributes on the `impl` item such as `cfg`, `allow`, `doc`,
/// `deprecated` and so on is supported.
///
/// [`extern_protocol!`]: crate::extern_protocol
///
///
/// # Panics
///
Expand Down Expand Up @@ -141,10 +149,10 @@
/// ProtocolType,
/// };
///
/// // Declare the NSCopying protocol so that we can implement it (since
/// // NSCopying is a trait currently).
/// // Declare the NSCopying protocol so that we can implement it.
/// //
/// // TODO: Remove the need for this!
/// // In practice, you wouldn't have to do this, since it is done for you in
/// // `icrate`.
/// extern_protocol!(
/// unsafe trait NSCopying {
/// #[method(copyWithZone:)]
Expand Down Expand Up @@ -219,6 +227,10 @@
/// *obj.bar = *self.bar;
/// obj.autorelease_return()
/// }
///
/// // If we have tried to add other methods here, or had forgotten
/// // to implement the method, we would have gotten an error with the
/// // `verify` feature enabled.
/// }
/// );
///
Expand All @@ -240,6 +252,7 @@
/// }
/// }
///
/// // TODO: `NSCopying` from `icrate` works a bit differently
/// // unsafe impl icrate::Foundation::NSCopying for MyCustomObject {
/// // type Ownership = Owned;
/// // type Output = Self;
Expand Down Expand Up @@ -318,10 +331,6 @@
///
/// @end
/// ```
///
/// [`extern_class!`]: crate::extern_class
/// [`declare`]: crate::declare
/// [`declare::Ivar`]: crate::declare::Ivar
#[doc(alias = "@interface")]
#[doc(alias = "@implementation")]
#[macro_export]
Expand Down
11 changes: 11 additions & 0 deletions crates/objc2/src/macros/extern_class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@
/// }
/// );
///
/// // We can specify the protocols that `NSFormatter` conforms to like this.
/// // (These should be created using the `extern_protocol!` macro).
/// //
/// // unsafe impl NSCoding for NSFormatter {}
/// // unsafe impl NSCopying for NSFormatter {}
///
/// // Provided by the implementation of `ClassType`
/// let cls = NSFormatter::class();
///
Expand Down Expand Up @@ -115,6 +121,11 @@
/// type Super = NSFormatter;
/// }
/// );
///
/// // Similarly, we can specify the protocols that this implements here:
/// //
/// // unsafe impl NSCoding for NSFormatter {}
/// // unsafe impl NSCopying for NSFormatter {}
/// ```
///
/// See the source code of `icrate` for many more examples.
Expand Down
152 changes: 117 additions & 35 deletions crates/objc2/src/macros/extern_protocol.rs
Original file line number Diff line number Diff line change
@@ -1,66 +1,148 @@
/// Create a new type to represent an Objective-C protocol.
/// Create a new trait to represent an Objective-C protocol.
///
/// This is similar to a `@protocol` declaration in Objective-C.
///
/// See [Protocols - The Objective-C Programming Language][protocols] and
/// [Working with Protocols - Programming with Objective-C][working-with] for
/// general information about protocols in Objective-C.
///
/// This macro will create an `unsafe` trait with methods which all have
/// default implementations, such that an object that conforms to the protocol
/// can simply write `unsafe impl MyProtocol for MyClass {}`, and get access
/// to the functionality exposed by the protocol.
///
/// Objective-C has a smart feature where you can write `id<MyProtocol>`, and
/// then work with the protocol as-if it was an object; this is very similar
/// to `dyn` traits in Rust, and we implement it in a similar way, see
/// [`ProtocolObject`] for details.
///
/// [protocols]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjectiveC/Chapters/ocProtocols.html
/// [working-with]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html
/// [`ProtocolObject`]: crate::ProtocolObject
///
///
/// # Tradeoffs
///
/// It should come as no surprise that Objective-C and Rust are not the same
/// language! This is in particular very prominent in the way that we map
/// protocols to Rust; one could choose to map them as traits, which has some
/// upsides, but also prevents using them as objects.
/// # Specification
///
/// If we could customize how `dyn Trait` works in Rust, then it may have been
/// beneficial to map them as traits, but as the sitation stands, we choose
/// to map them as structs that implement [`Message`], similar to how we map
/// classes. This means that you can use protocols inside [`rc::Id`], which is
/// very important for a lot of use-cases.
/// The syntax is similar enough to Rust syntax that if you invoke the macro
/// with parentheses (as opposed to curly brackets), `rustfmt` will be able to
/// format the contents.
///
/// If you have ideas for how to improve this situation, please help us out in
/// [#291]!
/// This macro creates an `unsafe` trait with the specified methods. A default
/// implementation of the method is generated based on the selector specified
/// with `#[method(a:selector:)]` or `#[method_id(a:selector:)]`.
///
/// [`Message`]: crate::Message
/// [`rc::Id`]: crate::rc::Id
/// [#291]: https://github.com/madsmtm/objc2/issues/291
/// Other protocols that this protocol conforms to / inherits can be specified
/// as supertraits.
///
/// The new trait `T` is automatically implemented for
/// [`ProtocolObject<dyn T>`], which also means that [`ProtocolType`] is
/// implemented for `dyn T`.
///
/// # Specification
/// Finally, you can use the `#[optional]` attribute to mark optional methods.
/// This currently doesn't have any effect, but probably will have one in the
/// future when implementing protocols in [`declare_class!`].
///
/// This macro shares many similarities with [`extern_class!`] and
/// This macro otherwise shares similarities with [`extern_class!`] and
/// [`extern_methods!`], if you are familiar with those, it should be fairly
/// straightforward to use.
///
/// It differs in a few aspects though:
/// - You can use the `#[optional]` attribute to mark optional methods. This
/// currently doesn't have any effect, but will probably in the future.
/// - Class methods are not (yet) supported.
/// - Protocols do not have a direct parent/child relationship, so specifying
/// a parent is not required. However, to make usage easier if the protocol
/// only directly conforms to one protocol, you may specify a "parent"
/// protocol that this protocol will `Deref` to.
/// - Other protocols that this protocol conforms to can be specified using
/// the `#[conforms_to(...)]` attribute, similar to `#[inherits(...)]` in
/// [`extern_class!`].
///
/// [`ProtocolObject<dyn T>`]: crate::ProtocolObject
/// [`ProtocolType`]: crate::ProtocolType
/// [`declare_class`]: crate::declare_class
/// [`extern_class!`]: crate::extern_class
/// [`extern_methods!`]: crate::extern_methods
///
///
/// # Safety
///
/// The following are required for using the macro itself:
/// - The specified name must be an exisiting Objective-C protocol.
/// - The protocol must actually inherit/conform to the protocols specified in
/// `#[conforms_to(...)]`.
/// - If a parent protocol is specified, the protocol must inherit/conform to
/// said protocol.
/// - The protocol must actually inherit/conform to the protocols specified
/// as supertraits.
/// - The protocol's methods must be correctly specified.
///
/// While the following are required when implementing the `unsafe` trait for
/// a new type:
/// - The type must represent an object that implements the protocol.
///
///
/// # Examples
///
/// Create a trait to represent the `NSItemProviderWriting` protocol.
///
/// ```
/// use std::ffi::c_void;
/// use objc2::ffi::NSInteger;
/// use objc2::rc::{Id, Shared};
/// use objc2::runtime::{NSObject, NSObjectProtocol};
/// use objc2::{extern_protocol, ProtocolType};
///
/// // Assume these were correctly define, as if the came from `icrate`
/// type NSArray<T> = T;
/// type NSString = NSObject;
/// type NSProgress = NSObject;
/// type NSItemProviderRepresentationVisibility = NSInteger;
///
/// extern_protocol!(
/// /// This comment will appear on the trait as expected.
/// pub unsafe trait NSItemProviderWriting: NSObjectProtocol {
/// // ^^^^^^^^^^^^^^^^
/// // This trait inherits from `NSObject`
///
/// // This method we mark as `unsafe`, since we aren't using the correct
/// // type for the completion handler
/// #[method_id(loadDataWithTypeIdentifier:forItemProviderCompletionHandler:)]
/// unsafe fn loadData(
/// &self,
/// type_identifier: &NSString,
/// completion_handler: *mut c_void,
/// ) -> Option<Id<NSProgress, Shared>>;
///
/// #[method_id(writableTypeIdentifiersForItemProvider)]
/// fn writableTypeIdentifiersForItemProvider_class()
/// -> Id<NSArray<NSString>, Shared>;
///
/// // The rest of these are optional, which means that a user of
/// // `declare_class!` would not need to implement them.
///
/// #[optional]
/// #[method_id(writableTypeIdentifiersForItemProvider)]
/// fn writableTypeIdentifiersForItemProvider(&self)
/// -> Id<NSArray<NSString>, Shared>;
///
/// #[optional]
/// #[method(itemProviderVisibilityForRepresentationWithTypeIdentifier:)]
/// fn itemProviderVisibilityForRepresentation_class(
/// type_identifier: &NSString,
/// ) -> NSItemProviderRepresentationVisibility;
///
/// #[optional]
/// #[method(itemProviderVisibilityForRepresentationWithTypeIdentifier:)]
/// fn itemProviderVisibilityForRepresentation(
/// &self,
/// type_identifier: &NSString,
/// ) -> NSItemProviderRepresentationVisibility;
/// }
///
/// // SAFETY:
/// // - The name is correct
/// // - The protocol does inherit `NSObject`
/// // - The methods are correctly specified
/// unsafe impl ProtocolType for dyn NSItemProviderWriting {
/// // ^^^ - Remember to add this `dyn`.
///
/// // We could have named the trait something else on the Rust-side,
/// // and then used this to keep it correct from Objective-C.
/// //
/// // const NAME: &'static str = "NSItemProviderWriting";
/// }
/// );
///
/// // Types can now implement `NSItemProviderWriting`, and use the methods
/// // from it as we specified.
/// ```
///
/// See the source code of `icrate` for many more examples.
#[doc(alias = "@protocol")]
#[macro_export]
macro_rules! extern_protocol {
Expand Down
Loading

0 comments on commit b0ed481

Please sign in to comment.