Unexpected failure to capture full struct with move ||

I've discovered some unexpected behavior when capturing structs with Copy members & wondering if this is expected (and maybe thinking either the docs could be improved or this might be marked as an inconsistency to be fixed in future editions).

The basic idea is that I've got a repr(C) struct containing some arbitrary data & a callback for doing some FFI callbacks and another callback for deallocating the data when we're done with it. I want to be able to save this in a Box<dyn Fn(..)> trait object and call it later, but to my dismay, the Drop() operation was being called on the object as soon as the constructing block returned, even though I thought the closure would be capturing via move, due to the use of the move keyword.

I think what is happening is that despite specifically requesting move capture, because the member types are Copy, only those fields are being captured, not the whole object.
I've found the workaround of let f = &f; which causes the whole object to be captured, but I'm wondering if there's any way to annotate the struct such that this could be avoided.

Any input would be appreciated, Below is a simplified example

use std::ffi::c_void;

#[repr("C")]
struct ContainsCopyableMembers {
  pub foreign_data: *mut c_void,
  pub destructor: Option<unsafe extern "C" fn(*mut c_void)>
}

impl Drop for ContainsCopyableMembers {
    fn drop(&mut self) {
        println!("Calling destructor....");
        if let Some(destructor) = self.destructor {
            unsafe {
                (destructor)(self.foreign_data);
            }
        }
    }
}

pub struct DynHolder {
    callable: Box<dyn Fn() -> u32>,
}
    
pub extern "C" fn delete_foreign_data(data: *mut c_void) {
    unsafe{Box::from_raw(data)}; // causes drop
}

fn main() {
    let fake_foreign_data : Box<u32> = Box::new(64);
    let cbh;
    {
        let cb = ContainsCopyableMembers{
            foreign_data : Box::into_raw(fake_foreign_data) as *mut c_void,
            destructor: Some(delete_foreign_data)
        };
        cbh = DynHolder{callable: Box::new(move || {
            // let cb = &cb; //Uncomment this line to get proper capture
            unsafe{*(cb.foreign_data as *const u32)}
        })};
    }
    println!("{}", (cbh.callable)());
}

(Playground)

Output:

Calling destructor....
4176920654

It's due to the capture disjoint fields capability, which is in effect on edition 2021 and later. (Try running your playground on edition 2018 to see the difference.) Here's an issue about it being unintuitive.

There's no annotation on the closure, so that's the typical way to get what you want on edition 2021+.

On the struct you could newtype the field to make it non-Copy I suppose.

Thanks! That's super helpful. I've left a comment on the related issue