Skip to content

Commit

Permalink
Compare class, object and block encodings as equivalent
Browse files Browse the repository at this point in the history
This catches slightly fewer bugs than before, but is necessary to
properly support AnyClass in generic collections like NSArray.
  • Loading branch information
madsmtm committed Nov 25, 2024
1 parent 22f935a commit fefdcc1
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 9 deletions.
4 changes: 4 additions & 0 deletions crates/objc2-encode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 24 additions & 2 deletions crates/objc2-encode/src/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,18 @@ 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
/// in Objective-C compiler implementations.
///
/// 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)
}
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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]
Expand Down
14 changes: 13 additions & 1 deletion crates/objc2-encode/src/helper.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use core::ffi;
use core::fmt;
use core::mem;
use core::slice;
use core::write;

use crate::parse::verify_name;
Expand Down Expand Up @@ -71,7 +72,7 @@ pub(crate) fn compare_encodings<E1: EncodingType, E2: EncodingType>(
};

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
Expand Down Expand Up @@ -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<usize> {
match self {
// Under all the considered targets, `_Bool` is sized and aligned
Expand Down
17 changes: 11 additions & 6 deletions crates/objc2-encode/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item = &'a str>) -> 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<()> {
Expand All @@ -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();
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions crates/objc2/src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit fefdcc1

Please sign in to comment.