Skip to content

Commit f8191be

Browse files
Fix memory leak in dynamic ECS example (#11461)
# Objective - Fix a memory leak in the dynamic ECS example mentioned in #11459 ## Solution - Rather than allocate the memory manually instead store a collection of `Vec` that will be dropped after it is used. --- I must have misinterpreted `OwningPtr`s semantics when initially writing this example. I believe we should be able to provide better APIs here for inserting dynamic components that don't require the user to wrangle so much unsafety. We have no other examples using `insert_by_ids` and our only tests show it being used for 1 or 2 values with nested calls to `OwningPtr::make` despite the function taking an iterator. Rust's type system is quite restrictive here but we could at least let `OwningPtr::new` take non u8 `NonNull`. I also agree with #11459 that we should generally be trying to simplify and clarify this example.
1 parent 7d69d31 commit f8191be

File tree

1 file changed

+39
-14
lines changed

1 file changed

+39
-14
lines changed

examples/ecs/dynamic.rs

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use bevy::{
1010
query::{QueryBuilder, QueryData},
1111
world::FilteredEntityMut,
1212
},
13-
ptr::OwningPtr,
13+
ptr::{Aligned, OwningPtr},
1414
utils::HashMap,
1515
};
1616

@@ -81,6 +81,7 @@ fn main() {
8181
Some(Ok(size)) => size,
8282
_ => 0,
8383
};
84+
// Register our new component to the world with a layout specified by it's size
8485
// SAFETY: [u64] is Send + Sync
8586
let id = world.init_component_with_descriptor(unsafe {
8687
ComponentDescriptor::new_with_layout(
@@ -100,44 +101,45 @@ fn main() {
100101
}
101102
"s" => {
102103
let mut to_insert_ids = Vec::new();
103-
let mut to_insert_ptr = Vec::new();
104+
let mut to_insert_data = Vec::new();
104105
rest.split(',').for_each(|component| {
105106
let mut component = component.split_whitespace();
106107
let Some(name) = component.next() else {
107108
return;
108109
};
110+
111+
// Get the id for the component with the given name
109112
let Some(&id) = component_names.get(name) else {
110113
println!("Component {} does not exist", name);
111114
return;
112115
};
116+
117+
// Calculate the length for the array based on the layout created for this component id
113118
let info = world.components().get_info(id).unwrap();
114119
let len = info.layout().size() / std::mem::size_of::<u64>();
115120
let mut values: Vec<u64> = component
116121
.take(len)
117122
.filter_map(|value| value.parse::<u64>().ok())
118123
.collect();
124+
values.resize(len, 0);
119125

120-
// SAFETY:
121-
// - All components will be interpreted as [u64]
122-
// - len and layout are taken directly from the component descriptor
123-
let ptr = unsafe {
124-
let data = std::alloc::alloc_zeroed(info.layout()).cast::<u64>();
125-
data.copy_from(values.as_mut_ptr(), values.len());
126-
let non_null = NonNull::new_unchecked(data.cast());
127-
OwningPtr::new(non_null)
128-
};
129-
126+
// Collect the id and array to be inserted onto our entity
130127
to_insert_ids.push(id);
131-
to_insert_ptr.push(ptr);
128+
to_insert_data.push(values);
132129
});
133130

134131
let mut entity = world.spawn_empty();
132+
133+
// Construct an `OwningPtr` for each component in `to_insert_data`
134+
let to_insert_ptr = to_owning_ptrs(&mut to_insert_data);
135+
135136
// SAFETY:
136137
// - Component ids have been taken from the same world
137-
// - The pointer with the correct layout
138+
// - Each array is created to the layout specified in the world
138139
unsafe {
139140
entity.insert_by_ids(&to_insert_ids, to_insert_ptr.into_iter());
140141
}
142+
141143
println!("Entity spawned with id: {:?}", entity.id());
142144
}
143145
"q" => {
@@ -162,6 +164,8 @@ fn main() {
162164
len,
163165
)
164166
};
167+
168+
// If we have write access, increment each value once
165169
if filtered_entity.access().has_write(id) {
166170
data.iter_mut().for_each(|data| {
167171
*data += 1;
@@ -181,6 +185,24 @@ fn main() {
181185
}
182186
}
183187

188+
// Constructs `OwningPtr` for each item in `components`
189+
// By sharing the lifetime of `components` with the resulting ptrs we ensure we don't drop the data before use
190+
fn to_owning_ptrs(components: &mut [Vec<u64>]) -> Vec<OwningPtr<Aligned>> {
191+
components
192+
.iter_mut()
193+
.map(|data| {
194+
let ptr = data.as_mut_ptr();
195+
// SAFETY:
196+
// - Pointers are guaranteed to be non-null
197+
// - Memory pointed to won't be dropped until `components` is dropped
198+
unsafe {
199+
let non_null = NonNull::new_unchecked(ptr.cast());
200+
OwningPtr::new(non_null)
201+
}
202+
})
203+
.collect()
204+
}
205+
184206
fn parse_term<Q: QueryData>(
185207
str: &str,
186208
builder: &mut QueryBuilder<Q>,
@@ -189,10 +211,12 @@ fn parse_term<Q: QueryData>(
189211
let mut matched = false;
190212
let str = str.trim();
191213
match str.chars().next() {
214+
// Optional term
192215
Some('?') => {
193216
builder.optional(|b| parse_term(&str[1..], b, components));
194217
matched = true;
195218
}
219+
// Reference term
196220
Some('&') => {
197221
let mut parts = str.split_whitespace();
198222
let first = parts.next().unwrap();
@@ -208,6 +232,7 @@ fn parse_term<Q: QueryData>(
208232
matched = true;
209233
}
210234
}
235+
// With term
211236
Some(_) => {
212237
if let Some(&id) = components.get(str) {
213238
builder.with_id(id);

0 commit comments

Comments
 (0)