mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 20:53:53 +00:00
56bcbb0975
# Objective Resolves #3824. `unsafe` code should be the exception, not the norm in Rust. It's obviously needed for various use cases as it's interfacing with platforms and essentially running the borrow checker at runtime in the ECS, but the touted benefits of Bevy is that we are able to heavily leverage Rust's safety, and we should be holding ourselves accountable to that by minimizing our unsafe footprint. ## Solution Deny `unsafe_code` workspace wide. Add explicit exceptions for the following crates, and forbid it in almost all of the others. * bevy_ecs - Obvious given how much unsafe is needed to achieve performant results * bevy_ptr - Works with raw pointers, even more low level than bevy_ecs. * bevy_render - due to needing to integrate with wgpu * bevy_window - due to needing to integrate with raw_window_handle * bevy_utils - Several unsafe utilities used by bevy_ecs. Ideally moved into bevy_ecs instead of made publicly usable. * bevy_reflect - Required for the unsafe type casting it's doing. * bevy_transform - for the parallel transform propagation * bevy_gizmos - For the SystemParam impls it has. * bevy_assets - To support reflection. Might not be required, not 100% sure yet. * bevy_mikktspace - due to being a conversion from a C library. Pending safe rewrite. * bevy_dynamic_plugin - Inherently unsafe due to the dynamic loading nature. Several uses of unsafe were rewritten, as they did not need to be using them: * bevy_text - a case of `Option::unchecked` could be rewritten as a normal for loop and match instead of an iterator. * bevy_color - the Pod/Zeroable implementations were replaceable with bytemuck's derive macros.
270 lines
9.7 KiB
Rust
270 lines
9.7 KiB
Rust
#![allow(unsafe_code)]
|
|
|
|
//! This example show how you can create components dynamically, spawn entities with those components
|
|
//! as well as query for entities with those components.
|
|
|
|
use std::{alloc::Layout, io::Write, ptr::NonNull};
|
|
|
|
use bevy::prelude::*;
|
|
use bevy::{
|
|
ecs::{
|
|
component::{ComponentDescriptor, ComponentId, ComponentInfo, StorageType},
|
|
query::{QueryBuilder, QueryData},
|
|
world::FilteredEntityMut,
|
|
},
|
|
ptr::{Aligned, OwningPtr},
|
|
utils::HashMap,
|
|
};
|
|
|
|
const PROMPT: &str = "
|
|
Commands:
|
|
comp, c Create new components
|
|
spawn, s Spawn entities
|
|
query, q Query for entities
|
|
Enter a command with no parameters for usage.";
|
|
|
|
const COMPONENT_PROMPT: &str = "
|
|
comp, c Create new components
|
|
Enter a comma separated list of type names optionally followed by a size in u64s.
|
|
e.g. CompA 3, CompB, CompC 2";
|
|
|
|
const ENTITY_PROMPT: &str = "
|
|
spawn, s Spawn entities
|
|
Enter a comma separated list of components optionally followed by values.
|
|
e.g. CompA 0 1 0, CompB, CompC 1";
|
|
|
|
const QUERY_PROMPT: &str = "
|
|
query, q Query for entities
|
|
Enter a query to fetch and update entities
|
|
Components with read or write access will be displayed with their values
|
|
Components with write access will have their fields incremented by one
|
|
|
|
Accesses: 'A' with, '&A' read, '&mut A' write
|
|
Operators: '||' or, ',' and, '?' optional
|
|
|
|
e.g. &A || &B, &mut C, D, ?E";
|
|
|
|
fn main() {
|
|
let mut world = World::new();
|
|
let mut lines = std::io::stdin().lines();
|
|
let mut component_names = HashMap::<String, ComponentId>::new();
|
|
let mut component_info = HashMap::<ComponentId, ComponentInfo>::new();
|
|
|
|
println!("{}", PROMPT);
|
|
loop {
|
|
print!("\n> ");
|
|
let _ = std::io::stdout().flush();
|
|
let Some(Ok(line)) = lines.next() else {
|
|
return;
|
|
};
|
|
|
|
if line.is_empty() {
|
|
return;
|
|
};
|
|
|
|
let Some((first, rest)) = line.trim().split_once(|c: char| c.is_whitespace()) else {
|
|
match &line.chars().next() {
|
|
Some('c') => println!("{}", COMPONENT_PROMPT),
|
|
Some('s') => println!("{}", ENTITY_PROMPT),
|
|
Some('q') => println!("{}", QUERY_PROMPT),
|
|
_ => println!("{}", PROMPT),
|
|
}
|
|
continue;
|
|
};
|
|
|
|
match &first[0..1] {
|
|
"c" => {
|
|
rest.split(',').for_each(|component| {
|
|
let mut component = component.split_whitespace();
|
|
let Some(name) = component.next() else {
|
|
return;
|
|
};
|
|
let size = match component.next().map(|s| s.parse::<usize>()) {
|
|
Some(Ok(size)) => size,
|
|
_ => 0,
|
|
};
|
|
// Register our new component to the world with a layout specified by it's size
|
|
// SAFETY: [u64] is Send + Sync
|
|
let id = world.init_component_with_descriptor(unsafe {
|
|
ComponentDescriptor::new_with_layout(
|
|
name.to_string(),
|
|
StorageType::Table,
|
|
Layout::array::<u64>(size).unwrap(),
|
|
None,
|
|
)
|
|
});
|
|
let Some(info) = world.components().get_info(id) else {
|
|
return;
|
|
};
|
|
component_names.insert(name.to_string(), id);
|
|
component_info.insert(id, info.clone());
|
|
println!("Component {} created with id: {:?}", name, id.index());
|
|
});
|
|
}
|
|
"s" => {
|
|
let mut to_insert_ids = Vec::new();
|
|
let mut to_insert_data = Vec::new();
|
|
rest.split(',').for_each(|component| {
|
|
let mut component = component.split_whitespace();
|
|
let Some(name) = component.next() else {
|
|
return;
|
|
};
|
|
|
|
// Get the id for the component with the given name
|
|
let Some(&id) = component_names.get(name) else {
|
|
println!("Component {} does not exist", name);
|
|
return;
|
|
};
|
|
|
|
// Calculate the length for the array based on the layout created for this component id
|
|
let info = world.components().get_info(id).unwrap();
|
|
let len = info.layout().size() / std::mem::size_of::<u64>();
|
|
let mut values: Vec<u64> = component
|
|
.take(len)
|
|
.filter_map(|value| value.parse::<u64>().ok())
|
|
.collect();
|
|
values.resize(len, 0);
|
|
|
|
// Collect the id and array to be inserted onto our entity
|
|
to_insert_ids.push(id);
|
|
to_insert_data.push(values);
|
|
});
|
|
|
|
let mut entity = world.spawn_empty();
|
|
|
|
// Construct an `OwningPtr` for each component in `to_insert_data`
|
|
let to_insert_ptr = to_owning_ptrs(&mut to_insert_data);
|
|
|
|
// SAFETY:
|
|
// - Component ids have been taken from the same world
|
|
// - Each array is created to the layout specified in the world
|
|
unsafe {
|
|
entity.insert_by_ids(&to_insert_ids, to_insert_ptr.into_iter());
|
|
}
|
|
|
|
println!("Entity spawned with id: {:?}", entity.id());
|
|
}
|
|
"q" => {
|
|
let mut builder = QueryBuilder::<FilteredEntityMut>::new(&mut world);
|
|
parse_query(rest, &mut builder, &component_names);
|
|
let mut query = builder.build();
|
|
|
|
query.iter_mut(&mut world).for_each(|filtered_entity| {
|
|
let terms = filtered_entity
|
|
.components()
|
|
.map(|id| {
|
|
let ptr = filtered_entity.get_by_id(id).unwrap();
|
|
let info = component_info.get(&id).unwrap();
|
|
let len = info.layout().size() / std::mem::size_of::<u64>();
|
|
|
|
// SAFETY:
|
|
// - All components are created with layout [u64]
|
|
// - len is calculated from the component descriptor
|
|
let data = unsafe {
|
|
std::slice::from_raw_parts_mut(
|
|
ptr.assert_unique().as_ptr().cast::<u64>(),
|
|
len,
|
|
)
|
|
};
|
|
|
|
// If we have write access, increment each value once
|
|
if filtered_entity.access().has_write(id) {
|
|
data.iter_mut().for_each(|data| {
|
|
*data += 1;
|
|
});
|
|
}
|
|
|
|
format!("{}: {:?}", info.name(), data[0..len].to_vec())
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(", ");
|
|
|
|
println!("{:?}: {}", filtered_entity.id(), terms);
|
|
});
|
|
}
|
|
_ => continue,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Constructs `OwningPtr` for each item in `components`
|
|
// By sharing the lifetime of `components` with the resulting ptrs we ensure we don't drop the data before use
|
|
fn to_owning_ptrs(components: &mut [Vec<u64>]) -> Vec<OwningPtr<Aligned>> {
|
|
components
|
|
.iter_mut()
|
|
.map(|data| {
|
|
let ptr = data.as_mut_ptr();
|
|
// SAFETY:
|
|
// - Pointers are guaranteed to be non-null
|
|
// - Memory pointed to won't be dropped until `components` is dropped
|
|
unsafe {
|
|
let non_null = NonNull::new_unchecked(ptr.cast());
|
|
OwningPtr::new(non_null)
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn parse_term<Q: QueryData>(
|
|
str: &str,
|
|
builder: &mut QueryBuilder<Q>,
|
|
components: &HashMap<String, ComponentId>,
|
|
) {
|
|
let mut matched = false;
|
|
let str = str.trim();
|
|
match str.chars().next() {
|
|
// Optional term
|
|
Some('?') => {
|
|
builder.optional(|b| parse_term(&str[1..], b, components));
|
|
matched = true;
|
|
}
|
|
// Reference term
|
|
Some('&') => {
|
|
let mut parts = str.split_whitespace();
|
|
let first = parts.next().unwrap();
|
|
if first == "&mut" {
|
|
if let Some(str) = parts.next() {
|
|
if let Some(&id) = components.get(str) {
|
|
builder.mut_id(id);
|
|
matched = true;
|
|
}
|
|
};
|
|
} else if let Some(&id) = components.get(&first[1..]) {
|
|
builder.ref_id(id);
|
|
matched = true;
|
|
}
|
|
}
|
|
// With term
|
|
Some(_) => {
|
|
if let Some(&id) = components.get(str) {
|
|
builder.with_id(id);
|
|
matched = true;
|
|
}
|
|
}
|
|
None => {}
|
|
};
|
|
|
|
if !matched {
|
|
println!("Unable to find component: {}", str);
|
|
}
|
|
}
|
|
|
|
fn parse_query<Q: QueryData>(
|
|
str: &str,
|
|
builder: &mut QueryBuilder<Q>,
|
|
components: &HashMap<String, ComponentId>,
|
|
) {
|
|
let str = str.split(',');
|
|
str.for_each(|term| {
|
|
let sub_terms: Vec<_> = term.split("||").collect();
|
|
if sub_terms.len() == 1 {
|
|
parse_term(sub_terms[0], builder, components);
|
|
} else {
|
|
builder.or(|b| {
|
|
sub_terms
|
|
.iter()
|
|
.for_each(|term| parse_term(term, b, components));
|
|
});
|
|
}
|
|
});
|
|
}
|