6

I would like to pass a FnOnce closure to an object to be used later, but I would like to avoid any heap allocation. I can avoid heap allocation by keeping the closure on the stack. But the problem is that I can't pass a reference to the object because the FnOnce call_once consumes the closure. So I need to pass an owned pointer (e.g. Box) without heap allocation.

Is this possible? What I'd like to do is this:

fn main() {
    let mut scheduler = NoHeapScheduler();

    // allocate the task on the stack
    let task = move ||;

    // somehow pass ownership of the closure, while keeping it allocated on the stack.
    scheduler.add_task(StaticBox::new(task));

    schedule.run();
}

As far as I know this should be safe as long as the scheduler doesn't outlive the task. Is there any way to make this happen?

awelkie
  • 2,422
  • 1
  • 22
  • 32
  • 1
    https://stackoverflow.com/questions/30411594/moving-a-boxed-function – Josh Lee Nov 30 '16 at 20:20
  • It looks like what I really want is the `&move` pointer as specified in [this RFC](https://github.com/rust-lang/rfcs/pull/1646) – awelkie Dec 18 '16 at 18:51

4 Answers4

4

Can I create an owned pointer to a stack object?

No. This is non-sensical actually, since by definition a stack object is owned by the stack, so it cannot also be owned by something else.

So I need to pass an owned pointer (e.g. Box) without heap allocation.

There are other owned pointers than Box.

At the moment, I know of none without a heap allocation, but there is little reason it cannot be done.

I envision a InlineFnOnceBox<S: Default, R, A> used as InlineFnOnceBox<[u8; 48], (), ()> in this case, which would contain both the array itself, used as backing storage, plus a virtual pointer to the FnOnce<A -> R> v-table for the type instantiated.

It requires some care (and unsafe code) to instantiate, but otherwise seems feasible.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • I wonder about the nonsensical part. The object can remain on the stack, but not ["live"](https://doc.rust-lang.org/nomicon/references.html#liveness), as in `let mut foo = 1; { let foo_ref = &mut foo; *foo_ref = 2 }; foo = 3`. Inside the inner block an owning reference to the stack object is created, and `foo` is no longer live. Once the reference is dropped, `foo` is live again. If we don't need `foo` later, it should be safe to pass the reference to the scheduler, as long as Rust could prove that the scheduler's use of the reference wouldn't outlive the object. – user4815162342 Dec 01 '16 at 10:28
  • @user4815162342: there is no notion of *owning reference* in Rust. An owner is in control of the lifetime of the object, while a reference only *borrows* the object and guarantees that the object will be alive (with all its invariants in place) after the borrow ends. – Matthieu M. Dec 01 '16 at 10:30
  • Point taken, but I suspect the OP was referring to *mutable reference*. – user4815162342 Dec 01 '16 at 10:38
  • 1
    @user4815162342: Given that he is talking about `FnOnce`, a mutable reference is not sufficient. – Matthieu M. Dec 01 '16 at 11:41
3

Can I create an owned pointer to a stack object?

No, but you can simply move the stack object into your scheduler. Your scheduler will increase in size with every closure you schedule, but it will be completely self contained an can even be moved around.

The basic idea is that your Scheduler becomes a kind of singly linked list:

pub trait Scheduler: Sized {
    fn run(self);
}
pub struct NoHeapScheduler<F: FnOnce(), T: Scheduler> {
    inner: T,
    f: F,
}
impl<F: FnOnce(), T: Scheduler> Scheduler for NoHeapScheduler<F, T> {
    fn run(self) {
        self.inner.run();
        (self.f)()
    }
}

The Scheduler trait is here to break the recursion chain in the NoHeapScheduler (Otherwise we'd need a feature like variadic generics).

To terminate the chain we also implement Scheduler for some no-op type, e.g. ():

impl Scheduler for () {
    fn run(self) {}
}

Now the only thing left is a way to add new closures.

impl<F: FnOnce(), T: Scheduler> NoHeapScheduler<F, T> {
    fn add_task<F2: FnOnce()>(self, f: F2) -> NoHeapScheduler<F2, Self> {
        NoHeapScheduler {
            inner: self,
            f: f,
        }
    }
}

This method moves The current scheduler into a new scheduler and adds the scheduled closure.

You can use this function like so:

let scheduler = scheduler.add_task(task);

Fully working example in the playground

oli_obk
  • 28,729
  • 6
  • 82
  • 98
  • This is clever. It seems like it might not be very ergonomic for some tasks (like running tasks out of order), but I'll give it a shot. – awelkie Dec 01 '16 at 12:14
  • 2
    @awelkie: You can write any algorithm you could write on a linked list. So if you can write an algorithm for linked list reordering, then you can write it for this setup. But it's all happening at compile-time. Once you go into runtime-decisions it might end up requiring some allocations, I'm not sure. Open another question when you hit that ;) – oli_obk Dec 01 '16 at 12:19
2

As stated, the answer to the question is "no".

If you pass ownership of the closure, you have to by definition move it into the owner (otherwise what you've got is a reference). You can do that if you've only got one callback using a generic type:

pub struct NoHeapScheduler<F:FnOnce()> {
    f: Option<F>,
}

impl<F:FnOnce()> NoHeapScheduler<F> {
    pub fn add_task(&mut self, f: F) {
        self.f = Some(f);
    }
    pub fn run(&mut self) {
        let f = self.f.take().unwrap();
        f()
    }
}

fn main() {
    let mut scheduler = NoHeapScheduler{ f: None };

    let task = move || {};

    scheduler.add_task(task);

    scheduler.run();
}

Playground

However you'd run into problems adding more than one closure, since they all have different types.

If you're willing to allow allocations and an unstable feature on the nightly compiler, you could use FnBox. This is like FnOnce but works with Box:

#![feature(fnbox)]
use std::boxed::FnBox;

pub struct NoHeapScheduler {
    v: Vec<Box<FnBox()>>,
}

impl NoHeapScheduler {
    pub fn add_task(&mut self, f: Box<FnBox()>) {
        self.v.push(f);
    }
    pub fn run(&mut self) {
        for f in self.v.drain(0..) {
            f();
        }
    }
}

fn main() {
    let mut scheduler = NoHeapScheduler{ v: Vec::new() };

    let task = move || {println!("Hello,"); };
    let other_task = move || {println!("world!"); };

    scheduler.add_task(Box::new(task));
    scheduler.add_task(Box::new(other_task));

    scheduler.run();
}

Playground

Chris Emerson
  • 13,041
  • 3
  • 44
  • 66
0

I can use an Option to do this. I can keep the Option on the stack and pass a mutable reference around, and then when I'm ready to consume the closure I can use Option::take to take ownership of the closure and consume it.

To handle different implementations of FnOnce, I can lift this out into a trait and use trait objects:

trait Callable {
    fn call(&mut self);
}

impl<F: FnOnce()> Callable for Option<F> {
    fn call(&mut self) {
        if let Some(func) = self.take() {
            func();
        }
    }
}

Then I can pass around trait objects that live on the stack but can be consumed by having the reference.

awelkie
  • 2,422
  • 1
  • 22
  • 32
  • Can you show what `add_task` would look like? I understand how to implement `add_task()` that accepts `&mut Callable`, but it only works for closures created before the creation of the scheduler. [Playground](https://play.rust-lang.org/?gist=26177963f40b7337bcab93c547fe2493&version=stable&backtrace=0) – user4815162342 Dec 03 '16 at 00:02
  • OK, I think I got it: the variables holding the options must be declared above the `scheduler` initialization, otherwise the scheduler's scope technically doesn't contain the options' scope. The option variables can even be uninitialized at the time the scope is created. (The scheduler's array itself uses an `Option` to enable the tasks to reside in the stack-allocated array of `Callable` trait objects.) [Playground link](https://play.rust-lang.org/?gist=703d95a7b9dfff9338cba8f7dd649889&version=stable&backtrace=0) – user4815162342 Dec 03 '16 at 00:45
  • I ended up just passing all of the tasks as a list to the scheduler. So it would be something like `scheduler.run(&[&mut Some(move ||) as &mut Callable, ...]);` – awelkie Dec 03 '16 at 02:41