From fefdcc1366c554d50602d3ca2879822b7f018a19 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Mon, 25 Nov 2024 14:46:01 +0100 Subject: [PATCH] Compare class, object and block encodings as equivalent This catches slightly fewer bugs than before, but is necessary to properly support AnyClass in generic collections like NSArray. --- crates/objc2-encode/CHANGELOG.md | 4 ++++ crates/objc2-encode/src/encoding.rs | 26 ++++++++++++++++++++++++-- crates/objc2-encode/src/helper.rs | 14 +++++++++++++- crates/objc2-encode/src/parse.rs | 17 +++++++++++------ crates/objc2/src/runtime/mod.rs | 7 +++++++ 5 files changed, 59 insertions(+), 9 deletions(-) diff --git a/crates/objc2-encode/CHANGELOG.md b/crates/objc2-encode/CHANGELOG.md index 98b49d0e2..5246f5fa1 100644 --- a/crates/objc2-encode/CHANGELOG.md +++ b/crates/objc2-encode/CHANGELOG.md @@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Added `Encoding::size`, which computes the size of the encoded type for the current target. +### Changed +* Equivalence comparisons now consider `Encoding::Class`, `Encoding::Object` + and `Encoding::Block` as equivalent. + ## 4.0.3 - 2024-05-21 diff --git a/crates/objc2-encode/src/encoding.rs b/crates/objc2-encode/src/encoding.rs index 1c5c7f7e7..60920b8bb 100644 --- a/crates/objc2-encode/src/encoding.rs +++ b/crates/objc2-encode/src/encoding.rs @@ -212,6 +212,7 @@ impl Encoding { /// - Structs or unions with no fields/members are considered to represent /// "opqaue" types, and will therefore be equivalent to all other /// structs / unions. + /// - [`Object`], [`Block`] and [`Class`] compare as equivalent. /// /// The comparison may be changed in the future to e.g. ignore struct /// names or similar changes that may be required because of limitations @@ -219,6 +220,10 @@ impl Encoding { /// /// For example, you should not rely on two equivalent encodings to have /// the same size or ABI - that is provided on a best-effort basis. + /// + /// [`Object`]: Self::Object + /// [`Block`]: Self::Block + /// [`Class`]: Self::Class pub fn equivalent_to(&self, other: &Self) -> bool { compare_encodings(self, other, NestingLevel::new(), false) } @@ -395,18 +400,24 @@ mod tests { fn block() { Encoding::Block; + ~Encoding::Class; + ~Encoding::Object; + !Encoding::Unknown; "@?"; } fn object() { Encoding::Object; - !Encoding::Block; + ~Encoding::Block; + ~Encoding::Class; + !Encoding::Sel; "@"; ~"@\"AnyClassName\""; ~"@\"\""; // Empty class name + ~"@?"; + ~"#"; !"@\"MyClassName"; !"@MyClassName\""; - !"@?"; } fn unknown() { @@ -645,6 +656,17 @@ mod tests { Encoding::Array(42, &Encoding::Pointer(&Encoding::None)); "[42^]"; } + + fn class() { + Encoding::Class; + ~Encoding::Object; + ~Encoding::Block; + !Encoding::Sel; + "#"; + ~"@?"; + ~"@"; + !"a"; + } } #[test] diff --git a/crates/objc2-encode/src/helper.rs b/crates/objc2-encode/src/helper.rs index 16cf4ffc4..1ef04d52e 100644 --- a/crates/objc2-encode/src/helper.rs +++ b/crates/objc2-encode/src/helper.rs @@ -1,6 +1,7 @@ use core::ffi; use core::fmt; use core::mem; +use core::slice; use core::write; use crate::parse::verify_name; @@ -71,7 +72,7 @@ pub(crate) fn compare_encodings( }; match (enc1.helper(), enc2.helper()) { - (Primitive(p1), Primitive(p2)) => p1 == p2, + (Primitive(p1), Primitive(p2)) => p1.equivalents().contains(&p2), (BitField(size1, Some((offset1, type1))), BitField(size2, Some((offset2, type2)))) => { size1 == size2 && offset1 == offset2 @@ -174,6 +175,17 @@ impl Primitive { } } + /// Classes, blocks and objects can all be used in places where `id` is + /// expected (i.e. where the encoding is for objects). + /// + /// So to support those use-cases, we compare them as equivalent. + pub(crate) fn equivalents(&self) -> &[Self] { + match self { + Self::Block | Self::Object | Self::Class => &[Self::Block, Self::Object, Self::Class], + _ => slice::from_ref(self), + } + } + pub(crate) const fn size(self) -> Option { match self { // Under all the considered targets, `_Bool` is sized and aligned diff --git a/crates/objc2-encode/src/parse.rs b/crates/objc2-encode/src/parse.rs index cf01b1ee0..ce4272de4 100644 --- a/crates/objc2-encode/src/parse.rs +++ b/crates/objc2-encode/src/parse.rs @@ -238,11 +238,16 @@ impl Parser<'_> { } } - fn expect_str(&mut self, s: &str) -> Option<()> { - for b in s.as_bytes() { - self.expect_byte(*b)?; + fn expect_one_of_str<'a>(&mut self, strings: impl IntoIterator) -> Option<()> { + for s in strings { + if self.remaining().starts_with(s) { + for b in s.as_bytes() { + self.expect_byte(*b).unwrap(); + } + return Some(()); + } } - Some(()) + None } fn expect_u64(&mut self, int: u64) -> Option<()> { @@ -264,7 +269,7 @@ impl Parser<'_> { pub(crate) fn expect_encoding(&mut self, enc: &Encoding, level: NestingLevel) -> Option<()> { match enc.helper() { Helper::Primitive(primitive) => { - self.expect_str(primitive.to_str())?; + self.expect_one_of_str(primitive.equivalents().iter().map(|p| p.to_str()))?; if primitive == Primitive::Object && self.try_peek() == Some(b'"') { self.advance(); @@ -295,7 +300,7 @@ impl Parser<'_> { } Helper::Container(kind, name, items) => { self.expect_byte(kind.start_byte())?; - self.expect_str(name)?; + self.expect_one_of_str([name])?; if let Some(level) = level.container_include_fields() { self.expect_byte(b'=')?; // Parse as equal if the container is empty diff --git a/crates/objc2/src/runtime/mod.rs b/crates/objc2/src/runtime/mod.rs index 74820440a..20e30d648 100644 --- a/crates/objc2/src/runtime/mod.rs +++ b/crates/objc2/src/runtime/mod.rs @@ -1627,6 +1627,13 @@ mod tests { assert_eq!(result, 3); } + #[test] + fn class_self() { + let cls = NSObject::class(); + let result: &'static AnyClass = unsafe { msg_send![cls, self] }; + assert_eq!(cls, result); + } + #[test] fn test_subprotocols() { let sub_proto = test_utils::custom_subprotocol();