Skip to content

Commit

Permalink
Merge pull request #250 from madsmtm/extern-protocol-macro
Browse files Browse the repository at this point in the history
Add `extern_protocol!` macro and `ProtocolType` trait
  • Loading branch information
madsmtm authored Nov 23, 2022
2 parents 9fb9f23 + b05c3fc commit bf28404
Show file tree
Hide file tree
Showing 24 changed files with 1,267 additions and 143 deletions.
5 changes: 5 additions & 0 deletions crates/objc2/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
* Added `#[method_id(...)]` attribute to `extern_methods!`.
* Added `"verify"` feature as a replacement for the `"verify_message"`
feature.
* Added `extern_protocol!` macro and `ProtocolType` trait.
* Added `ConformsTo` trait for marking that a type conforms to a specific
protocol.

### Changed
* Allow other types than `&Class` as the receiver in `msg_send_id!` methods
Expand All @@ -50,6 +53,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
* **BREAKING**: Message verification is now enabled by default. Your message
sends might panic with `debug_assertions` enabled if they are detected to
be invalid. Test your code to see if that is the case!
* **BREAKING**: `declare_class!` uses `ConformsTo<...>` instead of the
temporary `Protocol<...>` syntax.

### Fixed
* Fixed duplicate selector extraction in `extern_methods!`.
Expand Down
1 change: 1 addition & 0 deletions crates/objc2/CHANGELOG_FOUNDATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
* Added `CGSize`, `CGPoint` and `CGRect` (just aliases to equivalent
`NS`-types, but helps readability).
* Added `NSString::write_to_file`.
* Added `NSLock` class and `NSLocking` protocol.

### Changed
* **BREAKING**: `NSSize::new` no longer requires it's arguments to be
Expand Down
162 changes: 95 additions & 67 deletions crates/objc2/src/__macro_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use core::ptr;
use std::collections::HashSet;

use crate::declare::ClassBuilder;
#[cfg(all(debug_assertions, feature = "verify"))]
use crate::declare::MethodImplementation;
use crate::encode::Encode;
use crate::message::__TupleExtender;
Expand Down Expand Up @@ -478,74 +477,96 @@ impl ModuleInfo {

impl ClassBuilder {
#[doc(hidden)]
#[cfg(all(debug_assertions, feature = "verify"))]
pub fn __add_protocol_methods<'a, 'b>(
&'a mut self,
protocol: &'b Protocol,
protocol: Option<&'b Protocol>,
) -> ClassProtocolMethodsBuilder<'a, 'b> {
self.add_protocol(protocol);
ClassProtocolMethodsBuilder {
builder: self,
protocol,
required_instance_methods: protocol.method_descriptions(true),
optional_instance_methods: protocol.method_descriptions(false),
registered_instance_methods: HashSet::new(),
required_class_methods: protocol.class_method_descriptions(true),
optional_class_methods: protocol.class_method_descriptions(false),
registered_class_methods: HashSet::new(),
if let Some(protocol) = protocol {
self.add_protocol(protocol);
}
}

#[doc(hidden)]
#[cfg(not(all(debug_assertions, feature = "verify")))]
#[inline]
pub fn __add_protocol_methods(&mut self, protocol: &Protocol) -> &mut Self {
self.add_protocol(protocol);
self
#[cfg(all(debug_assertions, feature = "verify"))]
{
ClassProtocolMethodsBuilder {
builder: self,
protocol,
required_instance_methods: protocol
.map(|p| p.method_descriptions(true))
.unwrap_or_default(),
optional_instance_methods: protocol
.map(|p| p.method_descriptions(false))
.unwrap_or_default(),
registered_instance_methods: HashSet::new(),
required_class_methods: protocol
.map(|p| p.class_method_descriptions(true))
.unwrap_or_default(),
optional_class_methods: protocol
.map(|p| p.class_method_descriptions(false))
.unwrap_or_default(),
registered_class_methods: HashSet::new(),
}
}

#[cfg(not(all(debug_assertions, feature = "verify")))]
{
ClassProtocolMethodsBuilder {
builder: self,
protocol,
}
}
}
}

/// Helper for ensuring that:
/// - Only methods on the protocol are overriden.
/// - TODO: The methods have the correct signature.
/// - All required methods are overridden.
#[cfg(all(debug_assertions, feature = "verify"))]
pub struct ClassProtocolMethodsBuilder<'a, 'b> {
builder: &'a mut ClassBuilder,
protocol: &'b Protocol,
#[allow(unused)]
protocol: Option<&'b Protocol>,
#[cfg(all(debug_assertions, feature = "verify"))]
required_instance_methods: Vec<MethodDescription>,
#[cfg(all(debug_assertions, feature = "verify"))]
optional_instance_methods: Vec<MethodDescription>,
#[cfg(all(debug_assertions, feature = "verify"))]
registered_instance_methods: HashSet<Sel>,
#[cfg(all(debug_assertions, feature = "verify"))]
required_class_methods: Vec<MethodDescription>,
#[cfg(all(debug_assertions, feature = "verify"))]
optional_class_methods: Vec<MethodDescription>,
#[cfg(all(debug_assertions, feature = "verify"))]
registered_class_methods: HashSet<Sel>,
}

#[cfg(all(debug_assertions, feature = "verify"))]
impl ClassProtocolMethodsBuilder<'_, '_> {
#[inline]
pub unsafe fn add_method<T, F>(&mut self, sel: Sel, func: F)
where
T: Message + ?Sized,
F: MethodImplementation<Callee = T>,
{
let _types = self
.required_instance_methods
.iter()
.chain(&self.optional_instance_methods)
.find(|desc| desc.sel == sel)
.map(|desc| desc.types)
.unwrap_or_else(|| {
panic!(
"failed overriding protocol method -[{} {:?}]: method not found",
self.protocol.name(),
sel
)
});
#[cfg(all(debug_assertions, feature = "verify"))]
if let Some(protocol) = self.protocol {
let _types = self
.required_instance_methods
.iter()
.chain(&self.optional_instance_methods)
.find(|desc| desc.sel == sel)
.map(|desc| desc.types)
.unwrap_or_else(|| {
panic!(
"failed overriding protocol method -[{} {:?}]: method not found",
protocol.name(),
sel
)
});
}

// SAFETY: Checked by caller
unsafe { self.builder.add_method(sel, func) };

#[cfg(all(debug_assertions, feature = "verify"))]
if !self.registered_instance_methods.insert(sel) {
unreachable!("already added")
}
Expand All @@ -556,49 +577,56 @@ impl ClassProtocolMethodsBuilder<'_, '_> {
where
F: MethodImplementation<Callee = Class>,
{
let _types = self
.required_class_methods
.iter()
.chain(&self.optional_class_methods)
.find(|desc| desc.sel == sel)
.map(|desc| desc.types)
.unwrap_or_else(|| {
panic!(
"failed overriding protocol method +[{} {:?}]: method not found",
self.protocol.name(),
sel
)
});
#[cfg(all(debug_assertions, feature = "verify"))]
if let Some(protocol) = self.protocol {
let _types = self
.required_class_methods
.iter()
.chain(&self.optional_class_methods)
.find(|desc| desc.sel == sel)
.map(|desc| desc.types)
.unwrap_or_else(|| {
panic!(
"failed overriding protocol method +[{} {:?}]: method not found",
protocol.name(),
sel
)
});
}

// SAFETY: Checked by caller
unsafe { self.builder.add_class_method(sel, func) };

#[cfg(all(debug_assertions, feature = "verify"))]
if !self.registered_class_methods.insert(sel) {
unreachable!("already added")
}
}
}

#[cfg(all(debug_assertions, feature = "verify"))]
impl Drop for ClassProtocolMethodsBuilder<'_, '_> {
fn drop(&mut self) {
for desc in &self.required_instance_methods {
if !self.registered_instance_methods.contains(&desc.sel) {
panic!(
"must implement required protocol method -[{} {:?}]",
self.protocol.name(),
desc.sel
)
pub fn __finish(self) {
#[cfg(all(debug_assertions, feature = "verify"))]
if let Some(protocol) = self.protocol {
for desc in &self.required_instance_methods {
if !self.registered_instance_methods.contains(&desc.sel) {
panic!(
"must implement required protocol method -[{} {:?}]",
protocol.name(),
desc.sel
)
}
}
}

for desc in &self.required_class_methods {
if !self.registered_class_methods.contains(&desc.sel) {
panic!(
"must implement required protocol method +[{} {:?}]",
self.protocol.name(),
desc.sel
)
#[cfg(all(debug_assertions, feature = "verify"))]
if let Some(protocol) = self.protocol {
for desc in &self.required_class_methods {
if !self.registered_class_methods.contains(&desc.sel) {
panic!(
"must implement required protocol method +[{} {:?}]",
protocol.name(),
desc.sel
)
}
}
}
}
Expand Down
32 changes: 20 additions & 12 deletions crates/objc2/src/declare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,19 @@ mod tests {
use super::*;
use crate::foundation::{NSObject, NSZone};
use crate::test_utils;
use crate::{declare_class, msg_send, ClassType};
use crate::{declare_class, extern_protocol, msg_send, ClassType, ConformsTo, ProtocolType};

extern_protocol!(
struct NSCopyingObject;

unsafe impl ProtocolType for NSCopyingObject {
const NAME: &'static str = "NSCopying";

#[allow(unused)]
#[method(copyWithZone:)]
fn copy_with_zone(&self, _zone: *const NSZone) -> *mut Self;
}
);

#[test]
fn test_classbuilder_duplicate() {
Expand Down Expand Up @@ -795,8 +807,7 @@ mod tests {
}

#[test]
#[should_panic = "could not find protocol NotAProtocolName"]
fn test_declare_class_protocol_not_found() {
fn test_declare_class_protocol() {
declare_class!(
struct Custom {}

Expand All @@ -805,20 +816,17 @@ mod tests {
const NAME: &'static str = "TestDeclareClassProtocolNotFound";
}

// Implementing this should work
unsafe impl Protocol<NSCopying> for Custom {
unsafe impl ConformsTo<NSCopyingObject> for Custom {
#[method(copyWithZone:)]
#[allow(unreachable_code)]
fn copy_with_zone(&self, _zone: *const NSZone) -> *mut Self {
unimplemented!()
}
}

// But this should fail
unsafe impl Protocol<NotAProtocolName> for Custom {}
);

let _cls = Custom::class();
let cls = Custom::class();
assert!(cls.conforms_to(NSCopyingObject::protocol().unwrap()));
}

#[test]
Expand Down Expand Up @@ -859,7 +867,7 @@ mod tests {
const NAME: &'static str = "TestDeclareClassMissingProtocolMethod";
}

unsafe impl Protocol<NSCopying> for Custom {
unsafe impl ConformsTo<NSCopyingObject> for Custom {
// Missing required method
}
);
Expand All @@ -878,7 +886,7 @@ mod tests {
const NAME: &'static str = "TestDeclareClassInvalidProtocolMethod";
}

unsafe impl Protocol<NSCopying> for Custom {
unsafe impl ConformsTo<NSCopyingObject> for Custom {
// Override with a bad return type
#[method(copyWithZone:)]
fn copy_with_zone(&self, _zone: *const NSZone) -> u8 {
Expand All @@ -904,7 +912,7 @@ mod tests {
const NAME: &'static str = "TestDeclareClassExtraProtocolMethod";
}

unsafe impl Protocol<NSCopying> for Custom {
unsafe impl ConformsTo<NSCopyingObject> for Custom {
#[method(copyWithZone:)]
#[allow(unreachable_code)]
fn copy_with_zone(&self, _zone: *const NSZone) -> *mut Self {
Expand Down
62 changes: 62 additions & 0 deletions crates/objc2/src/foundation/lock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use super::{NSObject, NSString};
use crate::rc::{Id, Owned, Shared};
use crate::{extern_class, extern_methods, extern_protocol, ClassType, ConformsTo, ProtocolType};

// TODO: Proper Send/Sync impls here

extern_protocol!(
pub struct NSLocking;

unsafe impl ProtocolType for NSLocking {
#[method(lock)]
pub unsafe fn lock(&self);

#[method(unlock)]
pub unsafe fn unlock(&self);
}
);

extern_class!(
#[derive(Debug)]
pub struct NSLock;

unsafe impl ClassType for NSLock {
type Super = NSObject;
}
);

unsafe impl ConformsTo<NSLocking> for NSLock {}

extern_methods!(
unsafe impl NSLock {
#[method_id(new)]
pub fn new() -> Id<Self, Owned>;

#[method(tryLock)]
pub unsafe fn try_lock(&self) -> bool;

#[method_id(name)]
pub fn name(&self) -> Option<Id<NSString, Shared>>;

#[method(setName:)]
pub fn set_name(&mut self, name: Option<&NSString>);
}
);

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn lock_unlock() {
let lock = NSLock::new();
let locking: &NSLocking = lock.as_protocol();
unsafe {
locking.lock();
assert!(!lock.try_lock());
locking.unlock();
assert!(lock.try_lock());
locking.unlock();
}
}
}
Loading

0 comments on commit bf28404

Please sign in to comment.