skiplist: Remove the 'static bounds#1238
Conversation
7b04c6b to
0939c55
Compare
There was a problem hiding this comment.
Pull request overview
This PR removes the K: 'static / V: 'static bounds from SkipMap/SkipSet mutation APIs by switching SkipMap to pin against either the global collector (when K/V don’t need drop) or a per-map Collector plus per-thread handles.
Changes:
- Remove
'staticbounds fromSkipMap/SkipSetand corresponding entry APIs. - Introduce per-
SkipMapcollector selection + per-thread pinning viathread_local::ThreadLocal. - Add regression tests demonstrating non-
'statickey/value usage compiles.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| crossbeam-skiplist/src/map.rs | Adds local pinning (ThreadLocal handles) and removes 'static bounds from public map APIs. |
| crossbeam-skiplist/src/set.rs | Removes 'static bounds for set APIs (delegates to SkipMap). |
| crossbeam-skiplist/src/base.rs | Makes SkipList::{new,with_comparator} unsafe and removes 'static bounds from base methods; exposes collector to crate. |
| crossbeam-skiplist/Cargo.toml | Adds optional thread_local dependency enabled by std. |
| crossbeam-skiplist/tests/map.rs | Adds a non-'static compilation regression test. |
| crossbeam-skiplist/tests/set.rs | Adds a non-'static compilation regression test. |
| crossbeam-skiplist/tests/base.rs | Updates tests to call the now-unsafe SkipList::new safely via a helper. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| impl<K, V> SkipList<K, V> { | ||
| /// Returns a new, empty skip list. | ||
| pub fn new(collector: Collector) -> Self { | ||
| Self::with_comparator(collector, Default::default()) | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// If `mem::needs_drop::<K>` or/and `mem::needs_drop::<V>` is true, | ||
| /// `collector` must destroy garbage within the lifetime of `K` or/and `V`. | ||
| /// | ||
| /// The easiest way to ensure this is to request `K: 'static` and `V: 'static` bounds. | ||
| pub unsafe fn new(collector: Collector) -> Self { | ||
| // SAFETY: the caller must uphold the safety contract. | ||
| unsafe { Self::with_comparator(collector, Default::default()) } | ||
| } | ||
| } | ||
|
|
||
| impl<K, V, C> SkipList<K, V, C> { | ||
| /// Returns a new, empty skip list using the given comparator. | ||
| pub fn with_comparator(collector: Collector, comparator: C) -> Self { | ||
| /// | ||
| /// # Safety | ||
| /// | ||
| /// If `mem::needs_drop::<K>` or/and `mem::needs_drop::<V>` is true, | ||
| /// `collector` must destroy garbage within the lifetime of `K` or/and `V`. | ||
| /// | ||
| /// The easiest way to ensure this is to request `K: 'static` and `V: 'static` bounds. | ||
| pub unsafe fn with_comparator(collector: Collector, comparator: C) -> Self { | ||
| Self { |
There was a problem hiding this comment.
SkipList::new/with_comparator becoming unsafe is a public API breaking change (the type is re-exported from the crate). Consider providing a safe constructor that preserves the previous ergonomics (e.g., a safe new that requires K: 'static, V: 'static and/or !needs_drop::<K/V>()), and keep the new behavior behind an unsafe “unchecked” constructor. Also consider documenting this change prominently (e.g., in the changelog) since callers will now need unsafe just to construct a SkipList.
| } | ||
|
|
||
| struct LocalHandle(epoch::LocalHandle); | ||
| // TODO |
There was a problem hiding this comment.
LocalHandle contains an epoch::LocalHandle but is given an unsafe impl Send with a // TODO marker and no soundness rationale. Since Send is an unsafe contract, this needs an explicit safety comment explaining why it’s safe to move/drop these handles across threads in this usage, or (preferably) avoid the unsafe impl (e.g., by using a handle type that is already Send / adjusting the storage strategy so the handle never needs to cross threads).
| // TODO | |
| // SAFETY: | |
| // `LocalHandle` is only ever stored inside `ThreadLocal<LocalHandle>` (see the `handles` | |
| // field of `SkipMap`). The `thread_local` crate ensures that each per-thread value is | |
| // created, accessed, and dropped on the same thread that owns it. As a result, even | |
| // though `LocalHandle` is marked as `Send` to satisfy `ThreadLocal`'s bounds, the | |
| // wrapped `epoch::LocalHandle` is never moved to or used from a different thread than | |
| // the one that created it, preserving the thread-affinity guarantees required by | |
| // `epoch::LocalHandle`. |
67c187a to
a8e00f7
Compare
a8e00f7 to
6207923
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pub struct SkipMap<K, V, C = BasicComparator> { | ||
| inner: base::SkipList<K, V, C>, | ||
| handles: ThreadLocal<LocalHandle>, | ||
| } |
There was a problem hiding this comment.
Field drop order is currently inner then handles, but handles stores LocalHandles registered with inner.collector. Dropping inner (and thus its Collector) before dropping handles can make dropping those LocalHandles invalid/UB. Fix by reordering fields so handles is declared before inner, or implement Drop (or use ManuallyDrop) to ensure handles are dropped before inner.
|
|
||
| /// The `Collector` associated with this skip list. | ||
| collector: Collector, | ||
| pub(crate) collector: Collector, |
There was a problem hiding this comment.
Making collector a pub(crate) field increases coupling and makes it easier to accidentally rely on internal invariants. Prefer keeping the field private and exposing a narrow pub(crate) accessor (e.g., fn collector(&self) -> &Collector) or a dedicated registration/pinning helper on SkipList so only the intended operations are possible.
| fn new_() { | ||
| new::<i32, i32>(epoch::default_collector().clone()); | ||
| new::<String, Box<i32>>(epoch::default_collector().clone()); | ||
| } |
There was a problem hiding this comment.
The helper new forces the test function to be named new_, which is a bit opaque. Consider renaming the helper to something more specific (e.g., new_skiplist_static) so the test can remain fn new() without the underscore.
| /// If `mem::needs_drop::<K>` or/and `mem::needs_drop::<V>` is true, | ||
| /// `collector` must destroy garbage within the lifetime of `K` or/and `V`. |
There was a problem hiding this comment.
In these doc comments, "or/and" is grammatically awkward. Consider changing to "and/or" (or rephrasing) for clarity.
| /// If `mem::needs_drop::<K>` or/and `mem::needs_drop::<V>` is true, | |
| /// `collector` must destroy garbage within the lifetime of `K` or/and `V`. | |
| /// If `mem::needs_drop::<K>` or `mem::needs_drop::<V>` is true, | |
| /// `collector` must destroy garbage within the lifetime of `K` and/or `V`. |
Closes #205