Skip to content

Commit 629cfab

Browse files
committed
Improve safety for CommandQueue internals (#7039)
# Objective - Safety comments for the `CommandQueue` type are quite sparse and very imprecise. Sometimes, they are right for the wrong reasons or use circular reasoning. ## Solution - Document previously-implicit safety invariants. - Rewrite safety comments to actually reflect the specific invariants of each operation. - Use `OwningPtr` instead of raw pointers, to encode an invariant in the type system instead of via comments. - Use typed pointer methods when possible to increase reliability. --- ## Changelog + Added the function `OwningPtr::read_unaligned`.
1 parent ddfafab commit 629cfab

File tree

2 files changed

+63
-40
lines changed

2 files changed

+63
-40
lines changed

crates/bevy_ecs/src/system/commands/command_queue.rs

Lines changed: 54 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
1-
use std::mem::{ManuallyDrop, MaybeUninit};
1+
use std::{mem::MaybeUninit, ptr::NonNull};
2+
3+
use bevy_ptr::{OwningPtr, Unaligned};
24

35
use super::Command;
46
use crate::world::World;
57

68
struct CommandMeta {
9+
/// Offset from the start of `CommandQueue.bytes` at which the corresponding command is stored.
710
offset: usize,
8-
func: unsafe fn(value: *mut MaybeUninit<u8>, world: &mut World),
11+
/// SAFETY: The `value` must point to a value of type `T: Command`,
12+
/// where `T` is some specific type that was used to produce this metadata.
13+
apply_command: unsafe fn(value: OwningPtr<Unaligned>, world: &mut World),
914
}
1015

1116
/// A queue of [`Command`]s
1217
//
13-
// NOTE: [`CommandQueue`] is implemented via a `Vec<MaybeUninit<u8>>` over a `Vec<Box<dyn Command>>`
18+
// NOTE: [`CommandQueue`] is implemented via a `Vec<MaybeUninit<u8>>` instead of a `Vec<Box<dyn Command>>`
1419
// as an optimization. Since commands are used frequently in systems as a way to spawn
1520
// entities/components/resources, and it's not currently possible to parallelize these
1621
// due to mutable [`World`] access, maximizing performance for [`CommandQueue`] is
1722
// preferred to simplicity of implementation.
1823
#[derive(Default)]
1924
pub struct CommandQueue {
25+
/// Densely stores the data for all commands in the queue.
2026
bytes: Vec<MaybeUninit<u8>>,
27+
/// Metadata for each command stored in the queue.
28+
/// SAFETY: Each entry must have a corresponding value stored in `bytes`,
29+
/// stored at offset `CommandMeta.offset` and with an underlying type matching `CommandMeta.apply_command`.
2130
metas: Vec<CommandMeta>,
2231
}
2332

@@ -34,45 +43,45 @@ impl CommandQueue {
3443
where
3544
C: Command,
3645
{
37-
/// SAFETY: This function is only every called when the `command` bytes is the associated
38-
/// [`Commands`] `T` type. Also this only reads the data via `read_unaligned` so unaligned
39-
/// accesses are safe.
40-
unsafe fn write_command<T: Command>(command: *mut MaybeUninit<u8>, world: &mut World) {
41-
let command = command.cast::<T>().read_unaligned();
42-
command.write(world);
43-
}
44-
45-
let size = std::mem::size_of::<C>();
4646
let old_len = self.bytes.len();
4747

48+
// SAFETY: After adding the metadata, we correctly write the corresponding `command`
49+
// of type `C` into `self.bytes`. Zero-sized commands do not get written into the buffer,
50+
// so we'll just use a dangling pointer, which is valid for zero-sized types.
4851
self.metas.push(CommandMeta {
4952
offset: old_len,
50-
func: write_command::<C>,
53+
apply_command: |command, world| {
54+
// SAFETY: According to the invariants of `CommandMeta.apply_command`,
55+
// `command` must point to a value of type `C`.
56+
let command: C = unsafe { command.read_unaligned() };
57+
command.write(world);
58+
},
5159
});
5260

53-
// Use `ManuallyDrop` to forget `command` right away, avoiding
54-
// any use of it after the `ptr::copy_nonoverlapping`.
55-
let command = ManuallyDrop::new(command);
56-
61+
let size = std::mem::size_of::<C>();
5762
if size > 0 {
63+
// Ensure that the buffer has enough space at the end to fit a value of type `C`.
64+
// Since `C` is non-zero sized, this also guarantees that the buffer is non-null.
5865
self.bytes.reserve(size);
5966

60-
// SAFETY: The internal `bytes` vector has enough storage for the
61-
// command (see the call the `reserve` above), the vector has
62-
// its length set appropriately and can contain any kind of bytes.
63-
// In case we're writing a ZST and the `Vec` hasn't allocated yet
64-
// then `as_mut_ptr` will be a dangling (non null) pointer, and
65-
// thus valid for ZST writes.
66-
// Also `command` is forgotten so that when `apply` is called
67-
// later, a double `drop` does not occur.
68-
unsafe {
69-
std::ptr::copy_nonoverlapping(
70-
&*command as *const C as *const MaybeUninit<u8>,
71-
self.bytes.as_mut_ptr().add(old_len),
72-
size,
73-
);
74-
self.bytes.set_len(old_len + size);
75-
}
67+
// SAFETY: The buffer must be at least as long as `old_len`, so this operation
68+
// will not overflow the pointer's original allocation.
69+
let ptr: *mut C = unsafe { self.bytes.as_mut_ptr().add(old_len).cast() };
70+
71+
// Transfer ownership of the command into the buffer.
72+
// SAFETY: `ptr` must be non-null, since it is within a non-null buffer.
73+
// The call to `reserve()` ensures that the buffer has enough space to fit a value of type `C`,
74+
// and it is valid to write any bit pattern since the underlying buffer is of type `MaybeUninit<u8>`.
75+
unsafe { ptr.write_unaligned(command) };
76+
77+
// Grow the vector to include the command we just wrote.
78+
// SAFETY: Due to the call to `.reserve(size)` above,
79+
// this is guaranteed to fit in the vector's capacity.
80+
unsafe { self.bytes.set_len(old_len + size) };
81+
} else {
82+
// Instead of writing zero-sized types into the buffer, we'll just use a dangling pointer.
83+
// We must forget the command so it doesn't get double-dropped when the queue gets applied.
84+
std::mem::forget(command);
7685
}
7786
}
7887

@@ -83,17 +92,22 @@ impl CommandQueue {
8392
// flush the previously queued entities
8493
world.flush();
8594

86-
// SAFETY: In the iteration below, `meta.func` will safely consume and drop each pushed command.
87-
// This operation is so that we can reuse the bytes `Vec<u8>`'s internal storage and prevent
88-
// unnecessary allocations.
95+
// Reset the buffer, so it can be reused after this function ends.
96+
// In the loop below, ownership of each command will be transferred into user code.
97+
// SAFETY: `set_len(0)` is always valid.
8998
unsafe { self.bytes.set_len(0) };
9099

91100
for meta in self.metas.drain(..) {
92-
// SAFETY: The implementation of `write_command` is safe for the according Command type.
93-
// It's ok to read from `bytes.as_mut_ptr()` because we just wrote to it in `push`.
94-
// The bytes are safely cast to their original type, safely read, and then dropped.
101+
// SAFETY: `CommandQueue` guarantees that each metadata must have a corresponding value stored in `self.bytes`,
102+
// so this addition will not overflow its original allocation.
103+
let cmd = unsafe { self.bytes.as_mut_ptr().add(meta.offset) };
104+
// SAFETY: It is safe to transfer ownership out of `self.bytes`, since the call to `set_len(0)` above
105+
// gaurantees that nothing stored in the buffer will get observed after this function ends.
106+
// `cmd` points to a valid address of a stored command, so it must be non-null.
107+
let cmd = unsafe { OwningPtr::new(NonNull::new_unchecked(cmd.cast())) };
108+
// SAFETY: The underlying type of `cmd` matches the type expected by `meta.apply_command`.
95109
unsafe {
96-
(meta.func)(self.bytes.as_mut_ptr().add(meta.offset), world);
110+
(meta.apply_command)(cmd, world);
97111
}
98112
}
99113
}

crates/bevy_ptr/src/lib.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,15 @@ impl<'a, A: IsAligned> OwningPtr<'a, A> {
334334
unsafe { PtrMut::new(self.0) }
335335
}
336336
}
337+
impl<'a> OwningPtr<'a, Unaligned> {
338+
/// Consumes the [`OwningPtr`] to obtain ownership of the underlying data of type `T`.
339+
///
340+
/// # Safety
341+
/// - `T` must be the erased pointee type for this [`OwningPtr`].
342+
pub unsafe fn read_unaligned<T>(self) -> T {
343+
self.as_ptr().cast::<T>().read_unaligned()
344+
}
345+
}
337346

338347
/// Conceptually equivalent to `&'a [T]` but with length information cut out for performance reasons
339348
pub struct ThinSlicePtr<'a, T> {

0 commit comments

Comments
 (0)