Should create_auxiliary_task require !Unpin?
andrewcsmith opened this issue · comments
This declaration is almost certainly unsafe, because it creates a pointer to something that might (but hopefully won't) move. Maybe it would be better to use the !Unpin
/ Pin
API?
Examine the PinBox<T>
constructors to see whether that might be an alternative. Otherwise, it would have to be an immobile stack-allocated object.
I'm currently looking into the safety of bela-rs
in general and I think there are a couple other issues with create_auxiliary_task
. For example, I believe the signature
pub unsafe fn create_auxiliary_task<'b, 'c, A: 'b>(
task: &'c mut A,
priority: i32,
name: &'static str,
) -> CreatedTask<'b>
where
A: Auxiliary
implies that the reference task
has a lifetime 'c
independent of the lifetime 'b
of the CreatedTask
, but then multiple calls to create_auxiliary_task
function can be made with the same task object (just checked, and this works with the current code!).
I'm not even sure lifetimes other than 'static
are really valid for 'b
, since there is no Drop
for CreatedTask
and it doesn't appear the Bela API even supports removing a task once created.
Yeah, good call! This issue was basically a note-to-self that there were huge borrowck gaps. Check this out -- seems that it would be fairly trivial for them to extract this logic to allow deleting just one aux task, so that might be worth asking for.
But, til then, I think it's just important that the CreatedTask
lasts longer than task
. That is: 'b: 'c
. I don't think it needs to be 'static
, necessarily, since we can reason about the scope. The issue there is that each CreatedTask
will leak memory with no way to recover, but that seems like something that's also a potential issue with the bela api in general.
Also it's been probably 3-4 years since I've written any real Rust code so I may be totally fuzzy. I've still got a Bela though so I'll try and hack at it this weekend. Thanks for checking this.
Edit: I remember now, the whole point of this issue is that currently any aux tasks have to basically be stack-allocated. Requiring !Unpin
should actually let us move closures to the heap, I think. !Unpin
was created more recently than my more serious Rust days so I'll have to do more research.
But, til then, I think it's just important that the
CreatedTask
lasts longer thantask
. That is:'b: 'c
. I don't think it needs to be'static
, necessarily, since we can reason about the scope.
I don't think that is correct. task
has to exist as long as or longer than CreatedTask
because internally CreatedTask
effectively holds a mutable reference. BTW, from looking at the Bela side of things, I believe name
doesn't need to be 'static
, as it is strdup
ed.
The issue there is that each
CreatedTask
will leak memory with no way to recover, but that seems like something that's also a potential issue with the bela api in general.
Agreed
Edit: I remember now, the whole point of this issue is that currently any aux tasks have to basically be stack-allocated. Requiring
!Unpin
should actually let us move closures to the heap, I think.!Unpin
was created more recently than my more serious Rust days so I'll have to do more research.
!Unpin
alone doesn't really do anything AFAIK, it only changes how a reference behind a Pin<T>
acts:
Unpin
has no consequence at all for non-pinned data. In particular,mem::replace
happily moves!Unpin
data (it works for any&mut T
, not just whenT: Unpin
). However, you cannot usemem::replace
on data wrapped inside aPin<P<T>>
because you cannot get the&mut T
you need for that, and that is what makes this system work.
I discussed this over in the Rust Discord, and the consensus is that
- The task must be
Send
, as it is executed on another thread - The task must be
'static
as it may be called arbitrarily late due to execution on another thread (same reasoning behind the'static
constraint onstd::thread::spawn
) - Keeping a mutable reference around is almost impossible (but would be necessary since otherwise it can alias), so it would be best to accept a task by value (and
Box
it internally or accept a boxed task)
I currently have the following (not pushed yet) which also gets rid of the custom trait that has to be implemented, since closures already carry around their context:
/// Create an auxiliary task that runs on a lower-priority thread
/// `name` must be globally unique across all Xenomai processes!
pub fn create_auxiliary_task<Auxiliary>(
task: Box<Auxiliary>,
priority: i32,
name: &str,
) -> CreatedTask
where
Auxiliary: FnMut() + Send + 'static,
{
// TODO: Bela API does not currently offer an API to stop and unregister a task,
// so we can only leak the task. Otherwise, we could Box::into_raw here, store the
// raw pointer in `CreatedTask` and drop it after unregistering & joining the thread
// using Box::from_raw.
let task_ptr = Box::leak(task) as *mut _ as *mut _;
extern "C" fn auxiliary_task_trampoline<Auxiliary>(aux_ptr: *mut std::os::raw::c_void)
where
Auxiliary: FnMut() + Send,
{
let task_ptr = unsafe { &mut *(aux_ptr as *mut Auxiliary) };
task_ptr();
}
let aux_task = unsafe {
bela_sys::Bela_createAuxiliaryTask(
Some(auxiliary_task_trampoline::<Auxiliary>),
priority,
name.as_bytes().as_ptr(),
task_ptr,
)
};
CreatedTask(aux_task)
}
There should be no need to pin anything here, as the boxing already produces a heap allocated pointer that won't be moved out of, as no one else has access.
That's fantastic, and thank you. Interested to hear what you might be working on with this.