Search code examples
rustmoveownershipdowncast

How to convert RefCell<Box<dyn std::io::Write>> to owned File?


I want to move a File datatype from one struct into another and then drop the original struct.

I have a data structure with a logger of type RefCell<Box<dyn std::io::Write>>

pub struct WriterLogger {
    pub logger: RefCell<Box<dyn std::io::Write>>,
}

I also have a data structure with a logger that contains the concrete File type.

pub struct FileLogger {
    pub logger: RefCell<File>,
}

As a convenience, I want to allow users to convert a WriterLogger into a FileLogger if possible. I do that by casting to Box<dyn Any> and checking the type.

impl WriterLogger  {
    fn to_file_logger(self) -> Option<FileLogger> {
        let logger: Box<dyn Any> = Box::new(self.logger);

        // Attempt to downcast to concrete type
        if let Some(file_logger) = logger.downcast_ref::<RefCell<Box<File>>>() {

            // file_logger type is &RefCell<Box<File>>
            // I want to drop file_logger, and take ownership
            // of Box<File>
            let file = file_logger.into_inner();

            Some(FileLogger {
                logger: RefCell::new(*file),
            })
        } else {
            None
        }
    }
}

However, I can't figure out how move the concreate File from the WriterLogger to the new FileLogger structure. I get this error on the line where I call file_logger.into_inner()...

cannot move out of *file_logger which is behind a shared reference move occurs because *file_logger has type std::cell::RefCell<std::boxed::Box<std::fs::File>>, which does not implement the Copy trait

Which makes sense since I do have shared references. But what I want to do is just take the value and then drop everything. If I was working with an Option<File> I could call take() but I really don't want to change the data structure to have an Option in it. Is there any other way to do this?


Solution

  • Simply, you can't do this with the types you have. Not because of ownership problems but because this isn't how Any works.

    You could only do this if you had a RefCell<Box<dyn Any>>. All you're doing here is wrapping your RefCell in a Box<dyn Any>. The only type you can extract this as is RefCell<Box<dyn std::io::Write>>.

    If you do this with a RefCell<Box<dyn Any>> then you lose direct access to the Write trait.

    What if we make our own trait that combines Any with Write?

    trait AnyWrite: Any + std::io::Write {}
    
    impl<T: Any + std::io::Write> AnyWrite for T {}
    

    Now you could use RefCell<Box<dyn AnyWrite>> instead.

    Oh, but the downcast methods of Any require exactly a Box<dyn Any>! So this won't work either. We need a way to convert our type into a Box<dyn Any> temporarily so we can downcast it.

    We can do this by adding a suitable method to our AnyWrite trait:

    trait AnyWrite: std::io::Write {
        fn into_boxed_any(self: Box<Self>) -> Box<dyn Any>;
    }
    
    impl<T: std::io::Write + Any> AnyWrite for T {
        fn into_boxed_any(self: Box<Self>) -> Box<dyn Any> {
            Box::new(*self)
        }
    }
    

    Now, we can perform the downcast like so:

    impl WriterLogger {
        fn to_file_logger(self) -> Option<FileLogger> {
            self.logger
                .into_inner()
                .into_boxed_any()
                .downcast()
                .ok()
                .map(|file| FileLogger {
                    logger: RefCell::new(*file),
                })
        }
    }
    

    (Playground)

    A few notes about this approach:

    • I'd suggest not having your struct fields pub here. For POD or "record" types it makes sense but here it leaks a lot of implementation detail that's unnecessary.
    • If you fix the pub issue above, you can make AnyWrite a module-private type, keeping it a secret implementation detail of WriterLogger.

    If you wanted a good exercise from here, see if you can figure out how to have to_file_logger return Result<FileLogger, WriterLogger> -- that is, if the downcast fails, return back the original WriterLogger so the caller can do something else with it. Once you do that it would be trivial to impl TryFrom<WriterLogger> for FileLogger and then you can use the standard .try_into() to attempt the conversion, too!