Calling FnOnce inside a custom DST

Is there any way to call a FnOnce inside a custom DST in a box? Unsafe or unstable is okay.

struct TaskNode<T: FnOnce() + ?Sized> {
    next: Option<Box<TaskNode<dyn FnOnce()>>>,
    task: T,
}

fn run_tasks(mut head: Option<Box<TaskNode<dyn FnOnce()>>>) {
    while let Some(node) = head {
        head = node.next;
        // How to actually do this??
        (node.task)();
    }
}

AFAIK you need an internal feature (unsized_fn_params). And when I tried to use the feature anyway, I got an ICE. I'm not sure how to get around that.

2 Likes

there's an unstable feature unsized_locals, which allows you to "unbox" the DST, but on stable, you need to use a different trait for the type erasure, the vtable of FnOnce simply cannot do.

EDIT:

unsized_locals is removed, usized_fn_params still exists, but it doesn't work, see previous post by @quinedot

here's an example to use a custom trait for the type erasure:

trait ErasedTask {
	fn destructure(self: Box<Self>) -> (Option<Box<dyn ErasedTask>>, Box<dyn FnOnce()>);
}

struct TaskNode<T> {
	next: Option<Box<dyn ErasedTask>>,
	task: T,
}

impl<T: FnOnce() + 'static> ErasedTask for TaskNode<T> {
	fn destructure(self: Box<Self>) -> (Option<Box<dyn ErasedTask>>, Box<dyn FnOnce()>) {
		(self.next, Box::new(self.task))
	}
}


impl<T: FnOnce() + 'static> TaskNode<T> {
	fn new_erased(task: T) -> Box<dyn ErasedTask> {
		Box::new(Self {
			next: None,
			task,
		})
	}
}

fn run_tasks(mut head: Option<Box<dyn ErasedTask>>) {
	while let Some(node) = head {
		let (next, task) = node.destructure();
		head = next;
		task();
	}
}

fn main() {
	let node = TaskNode::new_erased(|| println!("hello"));
	run_tasks(Some(node));
}

alternatively, don't "re-erase" the task closure but call it directly, which is more efficient:

trait ErasedTask {
	fn run_task_then_return_next(self: Box<Self>) -> Option<Box<dyn ErasedTask>>;
}

impl<T: FnOnce() + 'static> ErasedTask for TaskNode<T> {
	fn run_task_then_return_next(self: Box<Self>) -> Option<Box<dyn ErasedTask>> {
		(self.task)();
		self.next
	}
}

fn run_tasks(mut head: Option<Box<dyn ErasedTask>>) {
	while let Some(node) = head {
		head = node.run_task_then_return_next();
	}
}
4 Likes

Nice! I started working on a way to "manually" move the dyn FnOnce() tail into a Box on stable, but these are much better :sweat_smile:.

2 Likes

Oh that's clever!
Ty both

the problem with dyn FnOnce() is the vtable is complete unusable for user defined type: you cannot implement/delegate it, you cannot consume it either. so including a dyn FnOnce tail in a custom DST doesn't make much sense.

1 Like

In light of your solution, I agree, and the below nit is moot for this topic IMO.

The metadata for a UserDef<dyn FnOnce()> is the same as that for a Box<dyn FnOnce()>, so "all" you have to do[1] is move the type-erased value into a new Box-compatible allocation and use Box::from_raw, correctly juggling all destructor calls.

Moving the type-erased value is still somewhat questionable, I haven't thought that through in depth. But I believe this has all been possible on stable for quite awhile now, since any time one has been able to soundly deduce how to decompose and recompose dyn pointers and had ManuallyDrop available. wrapping_byte_add in particular removes any chance of not being able to deal with the wide pointers at runtime.

(I did finish a POC, but don't recommend using it in light of your solution, so didn't bother posting it.)


  1. without unstable, apparently incomplete features â†Šī¸Ž

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.