Skip to content

Commit 887672f

Browse files
BD103mockersf
andauthored
Improve ThinSlicePtr API (#21823)
# Objective - Although unsafe, `ThinSlicePtr::get()` doesn't signal that it doesn't perform bounds checking unless you are familiar with the API. - `ThinSlicePtr::get()` takes `self`, not `&self`, meaning it consumes the `ThinSlicePtr` each time `get()` is called. This does not match the behavior of [`slice.get()`](https://doc.rust-lang.org/stable/std/primitive.slice.html#method.get), which takes `&self`. - This small issue was hidden by the fact that `ThinSlicePtr` implements `Copy`. This isn't a large issue because it all compiles down to the same machine code, but there's no point to `get()` requiring `self`. - `ThinSlicePtr` could use better documentation, and could be improved a little in some areas. ## Solution - Rename `ThinSlicePtr::get()` to `get_unchecked()`, making the API mirror [`slice.get_unchecked()`](https://doc.rust-lang.org/stable/std/primitive.slice.html#method.get_unchecked). The old `get()` is kept as a deprecated method, to be removed in 1.19.0. - Make the new `slice.get_unchecked()` take `&self` rather than `self`. - Add more documentation and slightly re-arrange code, while maintaining original behavior. ## Testing - Any unintentional changes should be caught by the Rust Compiler and Miri! --------- Co-authored-by: François Mockers <francois.mockers@vleue.com>
1 parent f663241 commit 887672f

File tree

4 files changed

+82
-22
lines changed

4 files changed

+82
-22
lines changed

crates/bevy_ecs/src/query/fetch.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1653,7 +1653,7 @@ unsafe impl<T: Component> QueryData for &T {
16531653
// SAFETY: set_table was previously called
16541654
let table = unsafe { table.debug_checked_unwrap() };
16551655
// SAFETY: Caller ensures `table_row` is in range.
1656-
let item = unsafe { table.get(table_row.index()) };
1656+
let item = unsafe { table.get_unchecked(table_row.index()) };
16571657
item.deref()
16581658
},
16591659
|sparse_set| {
@@ -1841,13 +1841,14 @@ unsafe impl<'__w, T: Component> QueryData for Ref<'__w, T> {
18411841
unsafe { table.debug_checked_unwrap() };
18421842

18431843
// SAFETY: The caller ensures `table_row` is in range.
1844-
let component = unsafe { table_components.get(table_row.index()) };
1844+
let component = unsafe { table_components.get_unchecked(table_row.index()) };
18451845
// SAFETY: The caller ensures `table_row` is in range.
1846-
let added = unsafe { added_ticks.get(table_row.index()) };
1846+
let added = unsafe { added_ticks.get_unchecked(table_row.index()) };
18471847
// SAFETY: The caller ensures `table_row` is in range.
1848-
let changed = unsafe { changed_ticks.get(table_row.index()) };
1848+
let changed = unsafe { changed_ticks.get_unchecked(table_row.index()) };
18491849
// SAFETY: The caller ensures `table_row` is in range.
1850-
let caller = callers.map(|callers| unsafe { callers.get(table_row.index()) });
1850+
let caller =
1851+
callers.map(|callers| unsafe { callers.get_unchecked(table_row.index()) });
18511852

18521853
Ref {
18531854
value: component.deref(),
@@ -2053,13 +2054,14 @@ unsafe impl<'__w, T: Component<Mutability = Mutable>> QueryData for &'__w mut T
20532054
unsafe { table.debug_checked_unwrap() };
20542055

20552056
// SAFETY: The caller ensures `table_row` is in range.
2056-
let component = unsafe { table_components.get(table_row.index()) };
2057+
let component = unsafe { table_components.get_unchecked(table_row.index()) };
20572058
// SAFETY: The caller ensures `table_row` is in range.
2058-
let added = unsafe { added_ticks.get(table_row.index()) };
2059+
let added = unsafe { added_ticks.get_unchecked(table_row.index()) };
20592060
// SAFETY: The caller ensures `table_row` is in range.
2060-
let changed = unsafe { changed_ticks.get(table_row.index()) };
2061+
let changed = unsafe { changed_ticks.get_unchecked(table_row.index()) };
20612062
// SAFETY: The caller ensures `table_row` is in range.
2062-
let caller = callers.map(|callers| unsafe { callers.get(table_row.index()) });
2063+
let caller =
2064+
callers.map(|callers| unsafe { callers.get_unchecked(table_row.index()) });
20632065

20642066
Mut {
20652067
value: component.deref_mut(),

crates/bevy_ecs/src/query/filter.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -850,7 +850,7 @@ unsafe impl<T: Component> QueryFilter for Added<T> {
850850
// SAFETY: set_table was previously called
851851
let table = unsafe { table.debug_checked_unwrap() };
852852
// SAFETY: The caller ensures `table_row` is in range.
853-
let tick = unsafe { table.get(table_row.index()) };
853+
let tick = unsafe { table.get_unchecked(table_row.index()) };
854854

855855
tick.deref().is_newer_than(fetch.last_run, fetch.this_run)
856856
},
@@ -1078,7 +1078,7 @@ unsafe impl<T: Component> QueryFilter for Changed<T> {
10781078
// SAFETY: set_table was previously called
10791079
let table = unsafe { table.debug_checked_unwrap() };
10801080
// SAFETY: The caller ensures `table_row` is in range.
1081-
let tick = unsafe { table.get(table_row.index()) };
1081+
let tick = unsafe { table.get_unchecked(table_row.index()) };
10821082

10831083
tick.deref().is_newer_than(fetch.last_run, fetch.this_run)
10841084
},

crates/bevy_ptr/src/lib.rs

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,7 +1056,29 @@ impl<'a> OwningPtr<'a, Unaligned> {
10561056
}
10571057
}
10581058

1059-
/// Conceptually equivalent to `&'a [T]` but with length information cut out for performance reasons
1059+
/// Conceptually equivalent to `&'a [T]` but with length information cut out for performance
1060+
/// reasons.
1061+
///
1062+
/// Because this type does not store the length of the slice, it is unable to do any sort of bounds
1063+
/// checking. As such, only [`Self::get_unchecked()`] is available for indexing into the slice,
1064+
/// where the user is responsible for checking the bounds.
1065+
///
1066+
/// When compiled in debug mode (`#[cfg(debug_assertion)]`), this type will store the length of the
1067+
/// slice and perform bounds checking in [`Self::get_unchecked()`].
1068+
///
1069+
/// # Example
1070+
///
1071+
/// ```
1072+
/// # use core::mem::size_of;
1073+
/// # use bevy_ptr::ThinSlicePtr;
1074+
/// #
1075+
/// let slice: &[u32] = &[2, 4, 8];
1076+
/// let thin_slice = ThinSlicePtr::from(slice);
1077+
///
1078+
/// assert_eq!(*unsafe { thin_slice.get_unchecked(0) }, 2);
1079+
/// assert_eq!(*unsafe { thin_slice.get_unchecked(1) }, 4);
1080+
/// assert_eq!(*unsafe { thin_slice.get_unchecked(2) }, 8);
1081+
/// ```
10601082
pub struct ThinSlicePtr<'a, T> {
10611083
ptr: NonNull<T>,
10621084
#[cfg(debug_assertions)]
@@ -1065,18 +1087,32 @@ pub struct ThinSlicePtr<'a, T> {
10651087
}
10661088

10671089
impl<'a, T> ThinSlicePtr<'a, T> {
1068-
#[inline]
1069-
/// Indexes the slice without doing bounds checks
1090+
/// Indexes the slice without performing bounds checks.
10701091
///
10711092
/// # Safety
1093+
///
10721094
/// `index` must be in-bounds.
1073-
pub unsafe fn get(self, index: usize) -> &'a T {
1095+
#[inline]
1096+
pub unsafe fn get_unchecked(&self, index: usize) -> &'a T {
1097+
// We cannot use `debug_assert!` here because `self.len` does not exist when not in debug
1098+
// mode.
10741099
#[cfg(debug_assertions)]
1075-
debug_assert!(index < self.len);
1100+
assert!(index < self.len, "tried to index out-of-bounds of a slice");
10761101

1077-
let ptr = self.ptr.as_ptr();
1078-
// SAFETY: `index` is in-bounds so the resulting pointer is valid to dereference.
1079-
unsafe { &*ptr.add(index) }
1102+
// SAFETY: The caller guarantees `index` is in-bounds so that the resulting pointer is
1103+
// valid to dereference.
1104+
unsafe { &*self.ptr.add(index).as_ptr() }
1105+
}
1106+
1107+
/// Indexes the slice without performing bounds checks.
1108+
///
1109+
/// # Safety
1110+
///
1111+
/// `index` must be in-bounds.
1112+
#[deprecated(since = "0.18.0", note = "use get_unchecked() instead")]
1113+
pub unsafe fn get(self, index: usize) -> &'a T {
1114+
// SAFETY: The caller guarantees that `index` is in-bounds.
1115+
unsafe { self.get_unchecked(index) }
10801116
}
10811117
}
10821118

@@ -1091,10 +1127,11 @@ impl<'a, T> Copy for ThinSlicePtr<'a, T> {}
10911127
impl<'a, T> From<&'a [T]> for ThinSlicePtr<'a, T> {
10921128
#[inline]
10931129
fn from(slice: &'a [T]) -> Self {
1094-
let ptr = slice.as_ptr().cast_mut();
1130+
let ptr = slice.as_ptr().cast_mut().debug_ensure_aligned();
1131+
10951132
Self {
1096-
// SAFETY: a reference can never be null
1097-
ptr: unsafe { NonNull::new_unchecked(ptr.debug_ensure_aligned()) },
1133+
// SAFETY: A reference can never be null.
1134+
ptr: unsafe { NonNull::new_unchecked(ptr) },
10981135
#[cfg(debug_assertions)]
10991136
len: slice.len(),
11001137
_marker: PhantomData,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
title: Rename `ThinSlicePtr::get()` to `ThinSlicePtr::get_unchecked()`
3+
pull_requests: [21823]
4+
---
5+
6+
`ThinSlicePtr::get()` has been deprecated in favor of the new `ThinSlicePtr::get_unchecked()`
7+
method in order to more clearly signal that bounds checking is not performed. Beyond the name
8+
change, the only difference between these two methods is that `get_unchecked()` takes `&self` while
9+
`get()` takes `self`. In order to migrate, simply rename all usages of `get()` with
10+
`get_unchecked()`:
11+
12+
```rust
13+
let slice: &[u32] = &[2, 4, 8];
14+
let thin_slice = ThinSlicePtr::from(slice);
15+
16+
// 0.17
17+
let x = unsafe { thin_slice.get(0) };
18+
19+
// 0.18
20+
let x = unsafe { thin_slice.get_unchecked(0) };
21+
```

0 commit comments

Comments
 (0)