Skip to content

spsc: fix split lifetimes - #29

Open
matoushybl wants to merge 6 commits into
cbiffle:mainfrom
matoushybl:fix/spsc-split-lifetimes
Open

matoushybl wants to merge 6 commits into
cbiffle:mainfrom
matoushybl:fix/spsc-split-lifetimes

Conversation

@matoushybl

Copy link
Copy Markdown

In the current implementation, it was not possible to split the queue in a function where the queue was passed as a mutable reference. For example when wrapping Pusher and Popper in custom types. This failed with an "queue dropped here while still borrowed, borrow might be used here, when queue is dropped and runs the Drop code for type spsc::Queue". This commit attempts to solve it by introducing an explicit lifetime of the &mut capture when splitting, which is shorter than the lifetime of the underlying data.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=17cad726a74a1be401924d2686e757ab

@matoushybl
matoushybl force-pushed the fix/spsc-split-lifetimes branch from 493be9d to 6091af6 Compare January 31, 2025 08:14
@matoushybl
matoushybl marked this pull request as draft January 31, 2025 08:19
@matoushybl
matoushybl force-pushed the fix/spsc-split-lifetimes branch from 6091af6 to 72319c5 Compare January 31, 2025 08:30
@matoushybl
matoushybl marked this pull request as ready for review January 31, 2025 10:43
cbiffle and others added 5 commits May 4, 2026 17:45
The only code changes were:

- Marking a now-unsafe attribute as unsafe
- Removing the old Captures hack.

This bumps the MSRV for the edition support.
In the current implementation, it was not possible to split the queue in
a function where the queue was passed as a mutable reference. For
example when wrapping Pusher and Popper in custom types. This failed
with an "`queue` dropped here while still borrowed, borrow might be used
here, when `queue` is dropped and runs the `Drop` code for type
`spsc::Queue`". This commit attempts to solve it by introducing an
explicit lifetime of the &mut capture when splitting, which is shorter
than the lifetime of the underlying data.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=17cad726a74a1be401924d2686e757ab
@cbiffle
cbiffle force-pushed the fix/spsc-split-lifetimes branch from 72319c5 to 1d8330e Compare May 9, 2026 17:27

@cbiffle cbiffle left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Hey, sorry I missed this initially. This is a good catch.

The tests in the PR don't actually build, but I'm fixing them and I'll push the fixes to your branch.

This is a major breaking change and would require a 2.0.0 lilos. However, the original API here seems broken, since it was explicitly intended to allow split in this case, and it didn't (due to the problem you've found). So I'm kind of inclined to start preparing for a 2.0 release, but it may have to wait a bit.

(Edit: See my comment below, I'm not sure the API change is necessary to unstick this use case.)

Comment thread testsuite/src/spsc.rs Outdated
fn compile_test_split_lifetime() {
fn split<'split, 'storage>(
queue: &mut Queue<'storage, ()>,
) -> (Pusher<'storage, ()>, Popper<'storage, ()>) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The 'split lifetime is missing from Pusher/Popper here, so the test doesn't compile (I'm fixing this)

Comment thread testsuite/src/spsc.rs Outdated
queue.split()
}

let mut storage: [MaybeUninit<u8>; 5] = [MaybeUninit::uninit(); 5];

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

u8 should be () here for consistency with the function signature above (fixing this)

@cbiffle

cbiffle commented May 9, 2026

Copy link
Copy Markdown
Owner

I've gotten the code in your playground example to work, and I think it comes down to the signature of the split routine.

You had:

    fn split<'storage>(q: &'storage mut Queue<'storage>) -> (Pusher<'storage>, Popper<'storage>) {

This constrains the queue's lifetime to match the storage's lifetime. It's actually over-constrained compared to lilos's split routine, which is (with an implicit lifetime filled in for clarity):

impl<'storage> Queue<'storage> {
    pub fn split(&'q mut self) -> (Pusher<'q, T>, Popper<'q, T>);
}

or as a standalone function, that would be equivalent to

fn split<'q, 'storage, T>(&'q mut Queue<'storage, T>) -> (Pusher<'q, T>, Popper<'q, T>);

This avoids connecting the lifetime of the queue itself ('q here) with the lifetime of the backing storage ('storage), which is what was causing the compiler to complain about your split function.

Here is a modified version of your playground example where I've rewritten the split routine as follows:

    fn split<'q>(q: &'q mut Queue<'_>) -> (Pusher<'q>, Popper<'q>) {
        q.split()
    }

This works.

I know it's been a bit since you filed this issue, but if you still have the code you were writing that motivated this PR, can you try rewriting any routines that split queues using this technique and let me know if it works?

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