-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ability to UseAfterFree in master branch #1032
Comments
Also, I would note, that it isn't code which someone would write reading bevy tutorials but in case when user wants store value into static Mutex, rustc suggest make |
Thanks for identifying the problem (and a solution!) I definitely agree that we have a problem here. I'll make it my next priority to fix this (after wrapping up the schedule work). |
Alright I spent some time with this and tried a bunch of different options. The best solution I've found so far is completely safe:
Its basically a combination of the Bevy 0.3 approach and the Bevy master approach. The one big downside I've found so far is that it prevents To test different approaches I built a "minimal" reproduction of the Bevy abstractions (uploaded below) I included various experiments:
|
Ah wait that wasn't an accurate summary. The "minimal reproduction of the current Bevy master approach" I mentioned (e2e_with_param_transmute.rs) is actually a variant of @AngelicosPhosphoros's solution. It actually behaves pretty well:
The only real downside is that edit: also it requires |
So I think it comes down to: do we want |
Updated examples (and re-added a bevy_master example): |
One more example update (fixed the e2e_with_param_transmute example to actually work and modified main.rs to prove that system-owned values like Commands work) |
Another loss in functionality in the let hi = vec![1];
App::build()
.add_system(move || println!("{:?}", &hi))
.run(); This didn't work in Bevy 0.3 either, but it did work on Bevy master. |
It is not necessary. pub struct NewTypeOverBoxSystem(pub(crate) Box<dyn System>);
impl <template args> IntoSystem<'app, ... > for F
where
Out: Into<TicksToSkip>,
F: 'static + Fn(
$(command_buff_type!($command_buffer; &mut CommandBuffer),)?
$(Res<'app, $tres>,)*
$(ResMut<'app, $tresmut>,)*
$($tquery,)*
) -> Out,
$($tres: Storable,)*
$($tresmut: Storable,)*
$($tquery: query_trait::QueryTrait<'app>,)*{
// Safety: Caller must ensure that 'app is not static and result doesn't outlive 'app
unsafe fn make_system(self) -> NewTypeOverBoxSystem{
.... // Some validation here and accesses gathering
#[allow(unused_unsafe)]
let tick_func = move |command_buffer_arg: &mut CommandBuffer,
resource_storage: &ResourceStorage,
archetypes: &[&[&Archetype]]| -> TicksToSkip {
// Unsafe trick to make resulting System static.
// Caller of `make_system` should ensure that 'app is not static
#[allow(unused)]
let command_buffer_arg: &'app mut CommandBuffer = unsafe{
&mut *(command_buffer_arg as *mut _)
};
#[allow(unused)]
let resource_storage: &'app ResourceStorage = unsafe {
&*(resource_storage as *const _)
};
let archetypes: &[&[&'app Archetype]] = unsafe {
std::mem::transmute(archetypes)
};
$(
let $tres: Res<$tres> = resource_storage.get_resource();
)*
$(
let $tresmut: ResMut<$tresmut> = resource_storage.get_resource_mut();
)*
// This assert removes bounds checks for arch_index
// Also it validates input
assert_eq!(archetypes.len(), count_macro!($($tquery),*));
#[allow(unused)]
let arch_index = 0usize;
$(
let $tquery: $tquery = unsafe {
$tquery::from_archetypes(Archetype::convert_to_wrappers(archetypes[arch_index]))
};
#[allow(unused)]
let arch_index = arch_index + 1;
)*
self(
$(command_buff_arg!($command_buffer; command_buffer_arg),)?
$($tres,)*
$($tresmut,)*
$($tquery,)*
).into()
};
NewTypeOverBoxSystem(Box::new(FunctionSystem {
name_func: type_name::<Self>,
access_func,
tick_func,
}))
} I don't need to make System in my project public so I accept with function like: struct App {}
impl Drop for App {
fn drop(&mut self) { /* dropping systems */ }
}
impl App {
fn add_system<'a, T, Arg>(&'a mut self, system: T)
where
T: 'static + IntoSystem<'a, Arg>,
{
let system = unsafe { system.make_system() };
// store system in private fields so user cannot use it.
}
} Also I use newtype primarly to avoid leaking System trait out of ECS bounds.
So finally I have a function that accepts something convertible to System instead of System so all unsafety is hidden inside ECS. I think, it is not very big problem for Bevy to make this switch: // old way
App::build()
.add_system(my_func.system())
.add_system(MyManualSystem::new().system())
.run();
// new way
// Safe and maybe even more convenient
App::build()
.add_system(my_func)
.add_system(move || {}) // closure system
.add_system(MyManualSystem::new())
.run(); Especially, if you explicitly stated that Bevy is not stable. If you worry that monomorphied
|
Haha I just realized that the "e2e_with_param_transmute.rs" example doesn't have a lifetime on system. So I guess I sorted that out already then promptly forgot.
Thats literally the exact Bevy ECS master api, so I'm already well convinced that its "worth it" 😄 My biggest hangup with the approach you're suggesting is that it ties the lifetime to the "wrong thing" and severely limits contexts where you can create systems. Systems shouldn't need to be anchored to App/World/Resource lifetimes because they have no relationship to that state. They are essentially boxed functions with some extra internal state. By doing so, we are preventing all present and future scenarios that require non-app system construction. Ex: With your approach its unsafe to allow users to create Schedules / Stages outside of the context of an app, as the Schedule could have any lifetime. Almost everything added in this PR would be unsafe #1021 |
Bevy version
From Cargo.lock
Operating system & version
Windows 10 but I guess it is not relevant
What you did
This sample program. Notice
'static
lifetime ofRes
(ResMut or Query works too, I believe).What you expected to happen
This code should not compile because this is UB.
What actually happened
In my case it prints some garbage.
Additional information
I recently tried to implement own ECS system for my game and bevy was my inspiration. I implement it mostly for fun and I found same bug in my ECS during implementation.
We discussed it on Reddit and I found a solution.
Look at this playground link.
So I suggest:
'app
.'app
constraints to requested types for function.system
method in IntoSystem trait unsafe (because it is).add_system
method of App to accept IntoSystem trait implementation and use it likeAnd implement it like:
This would prevent passing function that accepts
Res<'static, T>
into App unless it is'static
itself but mutation of'static
variable available only in unsafe code so it is fine.Note: Code example above doesn't compile on Bevy 0.3.0 from crates.io.
The text was updated successfully, but these errors were encountered: