Skip to content

Conversation

@luukvanderduim
Copy link
Collaborator

@luukvanderduim luukvanderduim commented Sep 8, 2025

To encode in the APIs that functions should not be called with a null-reference and to prevent this from happening by accident, we introduce NonNullObjectRef.

To prevent repeating ourselves, this refactor changes the ObjectRef types to:

enum ObjectRef<'o> {
    Null,
    NonNull(NonNullObjectRef<'o>),
}

enum NonNullObjectRef<'o> {
    Borrowed { name: UniqueName<'o>, path: ObjectPath<'o> },
    Owned { name: OwnedUniqueName, path: OwnedObjectPath }
}

struct ObjectRefOwned(ObjectRef<'static>)

Which offers an ObjectRef and ObjectRefOwned that can be deserialized from any reference, including null-referenes. Good for receiving references 'bus-side'.

NonNullObjectRef is particularly well suited where we want to accept object references.

Addresses: #296

We'd like to encode in our APIs that some functions should not be
called with a null-reference. This required us to rethink how we
organize our references.

We landed on:

```rust
enum ObjectRef<'o> {
    Null,
    NonNull(NonNullObjectRef<'o>),
}

enum NonNullObjectRef<'o> {
    Borrowed { name: UniqueName<'o>, path: ObjectPath<'o> },
    Owned { name: OwnedUniqueName, path: OwnedObjectPath }
}

struct ObjectRefOwned(ObjectRef<'static>)
```

Which offers us an `ObjectRef` and `ObjectRefOwned` that can be
deserialized from any reference, including null-referenes. Good for
receiving references.

`NonNullObjectRef` is particularly well suited where we want to
accept object references.
@codecov
Copy link

codecov bot commented Sep 9, 2025

Codecov Report

❌ Patch coverage is 67.34007% with 97 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.79%. Comparing base (f486755) to head (0d82a2e).

Files with missing lines Patch % Lines
atspi-common/src/object_ref.rs 78.12% 56 Missing ⚠️
atspi-proxies/src/accessible.rs 0.00% 29 Missing ⚠️
atspi-connection/src/p2p.rs 0.00% 12 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #301      +/-   ##
==========================================
- Coverage   75.17%   74.79%   -0.38%     
==========================================
  Files          44       44              
  Lines        4374     4586     +212     
==========================================
+ Hits         3288     3430     +142     
- Misses       1086     1156      +70     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@luukvanderduim luukvanderduim marked this pull request as ready for review September 13, 2025 21:04
@luukvanderduim
Copy link
Collaborator Author

The fixes to the text-under-click example will come in a separate PR.

Fixing it involves a bit of "forensics". I need to recreate the flow as it was when the example ran when we still had null references that could be made into proxies without problems. Performing calls would error, very different from what we have now.

On the bright side the macros I had before are now gone, in favor of mostly this pattern:

		// Skip null reference and convert to NonNullObjectRef.
		let Ok(non_null_ref) = NonNullObjectRef::try_from(app.clone()) else { continue };

@luukvanderduim luukvanderduim force-pushed the non-null-object-refs branch 2 times, most recently from 186fc6b to 0d82a2e Compare September 18, 2025 16:50
Copy link
Member

@TTWNO TTWNO left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work so far. Sorry about my limited free time.

/// In AT-SPI2, objects in the applications' UI object tree are uniquely identified
/// using an application's bus name and object path. "(so)"
///
/// Emitted by `RemoveAccessible` and `Available`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Link the events!


/// Returns the name of the object reference.
#[must_use]
#[allow(clippy::match_same_arms)] // Arms differ by lifetime
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name the two arms' name variable differently in the two cases, and then you don't need the clippy allow.


/// Returns the path of the object reference.
#[must_use]
#[allow(clippy::match_same_arms)] // Arms differ by lifetime
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise as above.

// because PartialEq does not consider it either.
//
// This to uphold the contract that if a == b, then a.hash() == b.hash() must hold true.
impl Hash for NonNullObjectRef<'_> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good observation that derive-ing would have missed!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you haven't already, please add tests to ensure that this remains the case.

}

#[test]
fn non_null_hash_and_object_coherence() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good! There is a test after all :D

let mut peers = Vec::with_capacity(accessible_applications.len());

for app in accessible_applications {
if app.is_null() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be:

let app = match app.is_null() {
    true => continue,
    // etc.
};

IMO easier to reason about.

for child in root.get_children().await?.iter() {
let proxy = child.clone().into_accessible_proxy(conn).await?;
for child in root.get_children().await?.into_iter() {
let Ok(child) = NonNullObjectRef::try_from(child) else { continue };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be the same as the other case; show a unified way to deal with null-refs.

let x = match child.is_null() {
    false => continue,
    _ => // etc.
};


for app in apps.iter() {
let proxy = app.clone().into_accessible_proxy(conn).await?;
for app in apps.into_iter().flat_map(|or| NonNullObjectRef::try_from(or).ok()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought.... this pattern is very common: continue if null, otherwise try to convert to proxy. That should probably just be a method?

@TTWNO
Copy link
Member

TTWNO commented Sep 30, 2025

If you are looking at other ways to do the "if null continue, else convert to proxy" thing., you could also try Option<NonNullObjectRef>::from(ObjectRef).map(...).await? or something like that 🤷

C-Loftus and others added 11 commits September 30, 2025 10:38
Example watched mouse events but it doesn't actually use them anywhere
This commit adds local git hooks based on those distributed with [zbus](https://github.com/dbus2/zbus).
This adds a "cargo machete" pass to check for unused dependencies to the pre-commit script.

Considering `.git/hooks` is not part of the distribution, contributors are advised to configure git to enable these hooks for this repository:

```sh
git config core.hooksPath .githooks
```

Thank you @C-Loftus for bringing this up and @jonathanhood for the tidy helper script.

Addresses: #298
The event enum had a mistake in the `DBusMatchRule` impl.

Fixes: #306
- `GetUri` -> `GetURI`
- `Nanchors` -> `NAnchors`
Make `event_stream` to parse signals with prefix "org.a11y.atspi."
only. Otherwise it will error on non-atspi signals this connection
expects. (`NameOwnerChanged` of "org.freedesktop.DBus")

Fixes: #307
- Bump versions
- Update lockfile
- Downgrade rayon to keep MSRV
and.. remove bad lifetime requirements on return in methods `name()` /
`path()`. We no more need to opt out of clippy.
Rustc can only guarantee the static arm if the impl is static too.
But this comes with the disadvantage that a second 'static impl
cannot have same name methods.

I had to reinstate the opt-out for match-on-same-arm pedantic clippy
warning.

fixes doctests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants