Understanding FnBox in ch20 (web server) project in TRPL

#1

I’m working through the final project in TRPL

I’m at the part where it’s gone from spinning up a thread per request to shipping closures across a channel that are getting pulled by workers in a thread pool and there’s this line about how we can’t quite yet run the closures, due to things in a Box needing to be of a known size:

To call a FnOnce closure that is stored in a Box<T> (which is what our Job type alias is), the closure needs to move itself out of the Box<T> because the closure takes ownership of self when we call it.

In this line what is self? The closure that is trying to grab Job's off the channel is not in a method so it doesn’t appear to be an instance of the worker.

impl Worker {
    pub fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let job = receiver.lock().unwrap().recv().unwrap();

            println!("worker {} got job; executing.", id);

            (*job)();
        });
        Worker { id, thread }
    }
}

The original creator of the closure to be run is from the main fn:

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for inc in listener.incoming() {
        //println!("... NEW REQUEST");
        let stream = inc.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }
}

The execute method of the threadpool itself is the “nearest” self that I see to the closure here:

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);
        self.sender.send(job).unwrap();
    }

But it doesn’t appear to actually close over self(threadpool) itself.

So I’m not sure how to read the above sentence because I can’t track what it’s talking about.

When I brought this up with a colleague, they pointed out that it’s probably the ‘self’ of this trait method: https://doc.rust-lang.org/std/ops/trait.FnOnce.html#tymethod.call_once and that while as a user of rust from this closure sugar, I am not implementing call_once nor explicitly writing (*job.call_once()), the method is getting called when running (*job)().

The other bit is how the solution to this problem seems to point to how the closure needs to be changed from an unknown size to a known size, but the exact means of making it of a known size doesn’t quite hit the mark for me. We change it from a Box<FnOnce...> to a Box<FnBox...>.

I’m struggling to see here how this level of indirection makes the closure of a known size now. Am I overlooking something mentioned earlier in the book? Otherwise where can I go to read more about how the closure needs to be of a known size and how using aBox is not enough, where earlier you could use Boxes to resolve this kind of an issue?

1 Like
#2

Yes, it’s that self.

The crucial difference is that FnBox::call_box takes a self: Box<Self> as the receiver, not a “naked” self. This means that the inner type does not need to be moved out.

This is a rather unfortunate limitation of the current type system, and (I believe) will likely change at some point to allow Box<FnOnce> to just work (in fact, I may have seen some recent chatter about this on Rust’s github, but can’t find the link). In the meantime, people either use FnBox (if they’re ok using nightly) or invent their own variant of this.

1 Like
#3
  • in TRPL,

    type Job = Box<dyn(FnOnce() + Send + 'static)>;
    
  • dyn Trait is a trait object, meaning that Box<dyn Trait> is

    1. a pointer to the object’s data (in this case, the environment captured by the closure) which, by the way, happens to be living in the heap (because Box);

    2. a pointer to an array of functions: the trait’s (now virtual) methods for that Trait.

  • for the trait in question, FnOnce(), there is only one method:

    ![feature(fn_traits)] // this is subject to change
    
    trait FnOnce<()> {
      type Output = ();
    
      extern "rust-call" // just ignore this
      fn call_once (
          self: Self,
          args: (), // no args in this case
      ) -> Self::Output;
    }
    

    The method takes self: Self, that is, without indirection, thus needing to know the size of the data at compile time. That is, when you call (*job)(), it is sugar for FnOnce::call_once(*job, ()), and this requires code to read the data pointed to by job. How many bytes? Well, it depends on the size of the environment captured by the closure *job, which could be anything. That’s why, at this point, the compiler can’t possibly issue code for such a (dynamic) read, and complains about unSizedness.

    We say that the method is not object safe (and remember, all this comes from taking Self by value).

Then, why does the following trick work?

- The trick:

trait FnBox : FnOnce() {
  fn call_box (
    self: Box<Self>, // look, indirection! => object safe
    // args: (),
  ) -> Self::Output;
}

impl<Env> FnBox for Env
where
  Env : FnOnce(),
{
  fn call_box (
    self: Box<Env>,
    // args: (),
  ) -> <Env as FnOnce()>::Output
  {
    // we can move this env around,
    // since we know its static type and thus size!
    let env: Env = *self;
    <Env as FnOnce()>::call_once(env, ())
  }
}

fn it_works (job: Box<dyn FnBox>)
{
  // unsugared job.call_box()
  <(dyn FnBox) as FnBox>::call_box(job);
}

This works thanks to 2 things:

  • if a method method (e.g., call_box) of a trait Trait (e.g., FnBox) is object-safe (no Self by value), then <(dyn Trait) as Trait>::method is well-defined (thanks to indirections) and thus usable.

  • call_box's implementation for each <Env : FnOnce()> is well defined too! Don’t let the generic fool you: Env is some concrete type … every “time” (at every monomorphization).

    So here is where, before abstracting stuff into dyn Fns, Rust can write down the code for each closure relative to moving “it” (i.e., the captured environment) into the stack (the amount of bytes for a given specific environment is statically knowable and thus known).

    • Example:

      use ::std::mem::{size_of_val, size_of};
      type Data = (); // untyped data
      
      let x = 42_u8;
      let env_x: _ // anonymous type, but let's call it EnvU8Print
        = move || println!("{}", x);
      assert_eq!(
          size_of::<u8>(),
          size_of_val(&env_x),
      );
      let env_x_in_heap = Box::new(env_x);
      // up until now, the type/env of the closure was concrete
      // (although anonymous). Now, for the coercion to a `dyn FnBox`,
      // we "erase" the type/env static info but store the address of
      // `<EnvU8Print as FnBox>::call_box` "in the Box's vtable":
      let some_env_and_vtable: Box<dyn FnBox> = env_x_in_heap;
      assert_eq!(
        size_of::<(Box<Data>, &'static [fn(Box<Data>); 1])>,
        size_of_val(&some_env_and_vtable), // fat pointer
      );
      some_env_and_vtable.call_box();
      // this becomes something along these lines:
      let (boxed_data, at_methods) = some_env_and_vtable;
      (at_methods[0])(boxed_data)
      // with at_methods[0] = <EnvU8Print as FnBox>::call_box
      

    Since the function pointer to the corresponding “read-into-stack” method is in the dyn FnBox's vtable and is thus dynamically accessible from a Box<dyn FnBox>: the compiler is now able to “move the closure into the stack” by dynamically calling that (virtual) method.