andrewcsmith / bela-rs

Safe Rust wrapper for the Bela.io API for realtime audio and sensor feedback

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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 than task. 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 strduped.

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 when T: Unpin). However, you cannot use mem::replace on data wrapped inside a Pin<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

  1. The task must be Send, as it is executed on another thread
  2. The task must be 'static as it may be called arbitrarily late due to execution on another thread (same reasoning behind the 'static constraint on std::thread::spawn)
  3. 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.