DEV Community

Cover image for [Advanced Rust] 2.8. API Design Principles of Flexibility Pt.4 - Problems with Explicit Destructors and Three Solutions
SomeB1oody
SomeB1oody

Posted on

[Advanced Rust] 2.8. API Design Principles of Flexibility Pt.4 - Problems with Explicit Destructors and Three Solutions

2.8.1. Problems with Explicit Destructors

Problems arise when you add an explicit destructor:

  • When a type implements Drop, you cannot move any of its fields out inside the destructor. That is because after the explicit destructor runs, drop() will still be called, and it takes &mut self, which requires all parts of self to remain in place.
  • Drop takes &mut self rather than self, so Drop cannot simply call the explicit destructor and ignore its result, because Drop does not own self

Based on the example from the previous article, if we add both a Drop implementation and a close method:

use std::os::fd::{FromRawFd, IntoRawFd};
use std::fs::{File as StdFile, OpenOptions, metadata};
use std::io::Error;

/// A type that represents a file handle
struct File {
    /// File name
    name: String,
    /// File descriptor
    fd: i32,
}

impl File {
    /// A constructor that opens a file and returns a `File` instance
    fn open(name: &str) -> Result<File, Error> {
        // Open the file with read and write permissions using `OpenOptions`
        let file: StdFile = OpenOptions::new()
            .read(true)
            .write(true)
            .open(name)?;

        // Obtain the file descriptor
        let fd: i32 = file.into_raw_fd();

        // Return a `File` instance
        Ok(File {
            name: name.to_string(),
            fd,
        })
    }

    /// An explicit destructor that closes the file and returns any error
    fn close(self) -> Result<(), Error> {
        // Convert the fd back into a `File` using `FromRawFd`
        let file: std::fs::File = unsafe {
            std::fs::File::from_raw_fd(self.fd)
        };

        // Flush file data to disk
        file.sync_all()?;

        // Truncate the file to 0 bytes
        file.set_len(0)?;

        // Flush the file again
        file.sync_all()?;

        // Drop the file instance; it will close the file automatically
        drop(file);

        // Return success
        Ok(())
    }
}

// Implement `Drop`
impl Drop for File {
    fn drop(&mut self) {
        let _ = self.close(); // call `close` while dropping
        println!("File dropped");
    }
}

fn main() {
    // Create a file named "test.txt" and write some content to it
    std::fs::write("test.txt", "Hello, world!").unwrap();

    // Open the file and obtain a `File` instance
    let file: File = File::open("test.txt").unwrap();

    // Print the file name and fd
    println!("File name: {}, fd: {}", file.name, file.fd);

    // Close the file and handle any error
    match file.close() {
        Ok(()) => println!("File closed successfully"),
        Err(e) => println!("Error closing file: {}", e),
    }

    // Check the file size after closing
    let metadata = metadata("test.txt").unwrap();
    println!("File size: {} bytes", metadata.len());
}
Enter fullscreen mode Exit fullscreen mode

Output:

error[E0507]: cannot move out of `*self` which is behind a mutable reference
  --> src/main.rs:59:17
   |
59 |         let _ = self.close(); // call `close` while dropping
   |                 ^^^^ ------- `*self` moved due to this method call
   |                 |
   |                 move occurs because `*self` has type `File`, which does not implement the `Copy` trait
   |
note: `File::close` takes ownership of the receiver `self`, which moves `*self`
  --> src/main.rs:33:14
   |
33 |     fn close(self) -> Result<(), Error> {
   |              ^^^^
note: if `File` implemented `Clone`, you could clone the value
  --> src/main.rs:6:1
   |
 6 | struct File {
   | ^^^^^^^^^^^ consider implementing `Clone` for this type
...
59 |         let _ = self.close(); // call `close` while dropping
   |                 ---- you could clone this value
Enter fullscreen mode Exit fullscreen mode

The error message shows that we cannot move a value out of *self because it sits behind &mut self.

2.8.2. Solutions

First, it is important to note that there is no perfect solution; we can only try our best to compensate.


Solution 1: Wrap the Struct in Option<T> and Add Another Layer of Struct

We can turn the outer layer into a new type that wraps Option<T>, so that the Option<T> internally holds a type containing all the fields.

At that point, we need two destructors, one outer and one inner. In both destructors, we use Option::take to get ownership of the data and remove the value.

Because the inner type does not implement Drop, you can take ownership of all fields.

The downside is that every method you want to provide on the outer type now has to include code to access the fields on the inner type through the Option<T> wrapper.

We modify the earlier example as follows:

Step 1: Change the File definition and add a wrapper layer

First, we need to move the two fields into another struct and wrap that struct in Option<T> as a field of File.

/// A type that represents a file handle
struct InnerFile {
    /// File name
    name: String,
    /// File descriptor
    fd: i32,
}

/// A wrapper around `InnerFile`
struct File {
    /// Wrap `InnerFile` in `Option<T>`
    inner: Option<InnerFile>,
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Update the methods on File

There are two methods on File, and we need to add code to access the inner fields through the Option<T> wrapper.

First, the open method:

/// A constructor that opens a file and returns a `File` instance
fn open(name: &str) -> Result<File, Error> {
    // Open the file with read and write permissions using `OpenOptions`
    let file: StdFile = OpenOptions::new()
        .read(true)
        .write(true)
        .open(name)?;

    // Obtain the file descriptor
    let fd: i32 = file.into_raw_fd();

    // Return a `File` instance
    Ok(File {
        inner: Some(InnerFile {
            name: name.to_string(),
            fd,
        }),
    })
}
Enter fullscreen mode Exit fullscreen mode
  • Because this code only uses File in the return value, only the return value needs to change

Next, the close method:

/// An explicit destructor that closes the file and returns any error
fn close(mut self) -> Result<(), Error> { // remember to make `self` mutable, otherwise `take` will not work
    // Use pattern matching to extract the field values
    if let Some(inner) = self.inner.take() {
        let name = inner.name;
        let fd = inner.fd;
        println!("Closing file: {} with fd: {}", name, fd);

        // Convert the fd back into a `File` using `FromRawFd`
        let file: std::fs::File = unsafe {
            std::fs::File::from_raw_fd(fd)
        };

        // Flush file data to disk
        file.sync_all()?;

        // Truncate the file to 0 bytes
        file.set_len(0)?;

        // Flush the file again
        file.sync_all()?;

        // Drop the file instance; it will close the file automatically
        drop(file);

        // Return success
        Ok(())
    } else {
        // If `inner` is `None`, the file has already been closed or dropped, so return an error
        Err(Error::new(
            std::io::ErrorKind::Other,
            "File is already closed",
        ))
    }
}
Enter fullscreen mode Exit fullscreen mode
  • After receiving the parameter, we first use pattern matching to access the field values
  • If the inner field is None, that is, if pattern matching fails, we need to return an error ourselves

Step 3: Update the Drop implementation

Drop::drop needs to be changed:

fn drop(&mut self) {
    // Use pattern matching to get the field values
    if let Some(inner) = self.inner.take() {
        let name = inner.name;
        let fd = inner.fd;
        println!("Dropping file: {} (fd: {})", name, fd);

        // Convert the fd back into a `File` using `FromRawFd`
        let file: std::fs::File = unsafe {
            std::fs::File::from_raw_fd(fd)
        };

        // Drop the `File` instance
        drop(file);
    } else {
        // If the `inner` field is `None`, the file has already been dropped or closed; do nothing
    }
}
Enter fullscreen mode Exit fullscreen mode
  • After receiving the parameter, we first use pattern matching to get the field values
  • If the inner field is None, the file has already been dropped or closed, so we do nothing

Step 4: Slightly Adjust main

The parts of main that need to access field values must be updated:

fn main() {
    // ...unchanged above, omitted

    // Print the file name and fd (this needs to change)
    println!("File name: {}, fd: {}",
        file.inner.as_ref().unwrap().name,
        file.inner.as_ref().unwrap().fd
    );

    // ...unchanged below, omitted
}
Enter fullscreen mode Exit fullscreen mode
  • The original type is Option<InnerFile>. After calling .as_ref(), it becomes Option<&InnerFile>
  • Once it becomes Option<&InnerFile>, the value extracted by unwrap is a reference rather than an owned value
  • file.inner is an Option<InnerFile>. Accessing the Option value directly would require moving ownership or pattern matching (for example through take() or unwrap()), which would destroy the inner value of the Option, so as_ref() is needed

Full Code

use std::os::fd::{FromRawFd, IntoRawFd};
use std::fs::{File as StdFile, OpenOptions, metadata};
use std::io::Error;

/// A type that represents a file handle
struct InnerFile {
    /// File name
    name: String,
    /// File descriptor
    fd: i32,
}

/// A wrapper around `InnerFile`
struct File {
    /// Wrap `InnerFile` in `Option<T>`
    inner: Option<InnerFile>,
}

impl File {
    /// A constructor that opens a file and returns a `File` instance
    fn open(name: &str) -> Result<File, Error> {
        // Open the file with read and write permissions using `OpenOptions`
        let file: StdFile = OpenOptions::new()
            .read(true)
            .write(true)
            .open(name)?;

        // Obtain the file descriptor
        let fd: i32 = file.into_raw_fd();

        // Return a `File` instance
        Ok(File {
            inner: Some(InnerFile {
                name: name.to_string(),
                fd,
            }),
        })
    }

    /// An explicit destructor that closes the file and returns any error
    fn close(mut self) -> Result<(), Error> {
        // Use pattern matching and `std::mem::take` to extract the `name` field value
        if let Some(inner) = self.inner.take() {
            let name = inner.name;
            let fd = inner.fd;
            println!("Closing file: {} with fd: {}", name, fd);

            // Convert the fd back into a `File` using `FromRawFd`
            let file: std::fs::File = unsafe {
                std::fs::File::from_raw_fd(fd)
            };

            // Flush file data to disk
            file.sync_all()?;

            // Truncate the file to 0 bytes
            file.set_len(0)?;

            // Flush the file again
            file.sync_all()?;

            // Drop the file instance; it will close the file automatically
            drop(file);

            // Return success
            Ok(())
        } else {
            // If the `inner` field is `None`, the file has already been closed or dropped, so return an error
            Err(Error::new(
                std::io::ErrorKind::Other,
                "File is already closed",
            ))
        }
    }
}

// Implement `Drop` for code that runs when the value leaves scope
impl Drop for File {
    fn drop(&mut self) {
        // Use pattern matching to get the field values
        if let Some(inner) = self.inner.take() {
            let name = inner.name;
            let fd = inner.fd;
            println!("Dropping file: {} (fd: {})", name, fd);

            // Convert the fd back into a `File` using `FromRawFd`
            let file: std::fs::File = unsafe {
                std::fs::File::from_raw_fd(fd)
            };

            // Drop the file instance
            drop(file);
        } else {
            // If the `inner` field is `None`, the file has already been dropped or closed; do nothing
        }
    }
}

fn main() {
    // Create a file named "test.txt" and write some content to it
    std::fs::write("test.txt", "Hello, world!").unwrap();

    // Open the file and obtain a `File` instance
    let file: File = File::open("test.txt").unwrap();

    // Print the file name and fd (this needs to change)
    println!("File name: {}, fd: {}",
        file.inner.as_ref().unwrap().name,
        file.inner.as_ref().unwrap().fd
    );

    // Close the file and handle any error
    match file.close() {
        Ok(()) => println!("File closed successfully"),
        Err(e) => println!("Error closing file: {}", e),
    }

    // Check the file size after closing
    let metadata = metadata("test.txt").unwrap();
    println!("File size: {} bytes", metadata.len());
}
Enter fullscreen mode Exit fullscreen mode

Solution 2: Wrap Each Field in Option<T>

We can also keep the struct unchanged, but wrap each field in Option<T>. When ownership is needed, use Option::take; when a reference is needed, use .as_ref() and .unwrap().

This works very well if the type has a reasonable empty value.

The downside is that if you have to wrap almost every field in Option and then match and unwrap those fields on every access, the code becomes very verbose.

We modify the earlier example as follows:

Step 1: Change the File definition

Add one layer of Option<T> to each field:

/// A type that represents a file handle
struct File {
    /// File name
    name: Option<String>,
    /// File descriptor
    fd: Option<i32>,
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Update the methods on File

There are two methods on File, and we need to add code to access the fields through the Option<T> wrapper.

First, the open method:

/// A constructor that opens a file and returns a `File` instance
fn open(name: &str) -> Result<File, Error> {
    // Open the file with read and write permissions using `OpenOptions`
    let file: StdFile = OpenOptions::new()
        .read(true)
        .write(true)
        .open(name)?;

    // Obtain the file descriptor
    let fd: i32 = file.into_raw_fd();

    // Return a `File` instance
    Ok(File {
        name: Some(name.to_string()),
        fd: Some(fd),
    })
}
Enter fullscreen mode Exit fullscreen mode
  • The open method's parameter does not involve the File struct, so the parameter part does not need to change
  • The open method's return value involves File, so each field needs to be wrapped in Some

Next, the close method:

/// An explicit destructor that closes the file and returns any error
fn close(mut self) -> Result<(), Error> {
    // Pattern-match and use `std::mem::take` to take out the `name` field value
    if let Some(name) = std::mem::take(&mut self.name) {
        // Pattern-match and use `std::mem::take` to take out the `fd` field value
        if let Some(fd) = std::mem::take(&mut self.fd) {
            // Print
            println!("Closing file: {} with fd: {}", name, fd);

            // Convert the fd back into a `File` using `FromRawFd`
            let file: std::fs::File = unsafe {
                std::fs::File::from_raw_fd(fd)
            };

            // Flush file data to disk
            file.sync_all()?;

            // Truncate the file to 0 bytes
            file.set_len(0)?;

            // Flush the file again
            file.sync_all()?;

            // Drop the file instance; it will close the file automatically
            drop(file);

            // Return success
            Ok(())
        } else {
            // If the `fd` field is `None`, the file has already been closed or dropped, so return an error
            Err(Error::new(
                std::io::ErrorKind::Other,
                "File descriptor already dropped or taken",
            ))
        }
    } else {
        // If the `name` field is `None`, the file has already been closed or dropped, so return an error
        Err(Error::new(
            std::io::ErrorKind::Other,
            "File name already dropped or taken",
        ))
    }
}
Enter fullscreen mode Exit fullscreen mode
  • The parameter must first be pattern-matched, and we use std::mem::take to take out the value inside it
  • If any field is None, it means the file has already been closed or dropped, so an error is returned

Step 3: Update the Drop implementation

fn drop(&mut self) {
    // Use pattern matching to get the field values
    if let Some(name) = self.name.take() {
        if let Some(fd) = self.fd.take() {
            println!("Dropping file: {} (fd: {})", name, fd);

            // Convert the fd back into a `File`
            let file: std::fs::File = unsafe {
                std::fs::File::from_raw_fd(fd)
            };

            // Drop the file instance
            drop(file);
        } else {
            // If the `fd` field is `None`, the file has already been closed or dropped; do nothing
        }
    } else {
        // If the `name` field is `None`, the file has already been closed or dropped; do nothing
    }
}
Enter fullscreen mode Exit fullscreen mode
  • The parameter must first be pattern-matched, and we use std::mem::take to take out the value inside it
  • If any field is None, it means the file has already been closed or dropped; do nothing

Step 4: Slightly Adjust main

fn main() {
    // ...unchanged above, omitted

    // Print the file name and fd (this needs to change)
    println!("File name: {}, fd: {}",
         file.name.as_ref().unwrap(),
         file.fd.as_ref().unwrap()
    );

    // ...unchanged below, omitted
}
Enter fullscreen mode Exit fullscreen mode
  • The original type is wrapped in Option<T>, so calling .as_ref() gives you a reference to the value inside
  • Once it becomes a reference, unwrap extracts a reference rather than an owned value
  • Accessing the Option value directly requires moving ownership or pattern matching (for example through take() or unwrap()), which destroys the inner value of the Option, so as_ref() is needed

Full Code

use std::os::fd::{FromRawFd, IntoRawFd};
use std::fs::{File as StdFile, OpenOptions, metadata};
use std::io::Error;

/// A type that represents a file handle
struct File {
    /// File name
    name: Option<String>,
    /// File descriptor
    fd: Option<i32>,
}

impl File {
    /// A constructor that opens a file and returns a `File` instance
    fn open(name: &str) -> Result<File, Error> {
        // Open the file with read and write permissions using `OpenOptions`
        let file: StdFile = OpenOptions::new()
            .read(true)
            .write(true)
            .open(name)?;

        // Obtain the file descriptor
        let fd: i32 = file.into_raw_fd();

        // Return a `File` instance
        Ok(File {
            name: Some(name.to_string()),
            fd: Some(fd),
        })
    }

    /// An explicit destructor that closes the file and returns any error
    fn close(mut self) -> Result<(), Error> {
        // Pattern-match and use `std::mem::take` to take out the value inside `name`
        if let Some(name) = std::mem::take(&mut self.name) {
            // Pattern-match and use `std::mem::take` to take out the value inside `fd`
            if let Some(fd) = std::mem::take(&mut self.fd) {
                // Print
                println!("Closing file: {} with fd: {}", name, fd);

                // Convert the fd back into a `File` using `FromRawFd`
                let file: std::fs::File = unsafe {
                    std::fs::File::from_raw_fd(fd)
                };

                // Flush file data to disk
                file.sync_all()?;

                // Truncate the file to 0 bytes
                file.set_len(0)?;

                // Flush the file again
                file.sync_all()?;

                // Drop the file instance; it will close the file automatically
                drop(file);

                // Return success
                Ok(())
            } else {
                // If the `fd` field is `None`, the file has already been closed or dropped, so return an error
                Err(Error::new(
                    std::io::ErrorKind::Other,
                    "File descriptor already dropped or taken",
                ))
            }
        } else {
            // If the `name` field is `None`, the file has already been closed or dropped, so return an error
            Err(Error::new(
                std::io::ErrorKind::Other,
                "File name already dropped or taken",
            ))
        }
    }
}

// Implement `Drop` for code that runs when the value leaves scope
impl Drop for File {
    fn drop(&mut self) {
        // Use pattern matching to get the field values
        if let Some(name) = self.name.take() {
            if let Some(fd) = self.fd.take() {
                println!("Dropping file: {} (fd: {})", name, fd);

                // Convert the fd back into a `File`
                let file: std::fs::File = unsafe {
                    std::fs::File::from_raw_fd(fd)
                };

                // Drop the file instance
                drop(file);
            } else {
                // If the `fd` field is `None`, the file has already been closed or dropped; do nothing
            }
        } else {
            // If the `name` field is `None`, the file has already been closed or dropped; do nothing
        }
    }
}

fn main() {
    // Create a file named "test.txt" and write some content to it
    std::fs::write("test.txt", "Hello, world!").unwrap();

    // Open the file and obtain a `File` instance
    let file: File = File::open("test.txt").unwrap();

    // Print the file name and fd (this needs to change)
    println!("File name: {}, fd: {}",
         file.name.as_ref().unwrap(),
         file.fd.as_ref().unwrap()
    );

    // Close the file and handle any error
    match file.close() {
        Ok(()) => println!("File closed successfully"),
        Err(e) => println!("Error closing file: {}", e),
    }

    // Check the file size after closing
    let metadata = metadata("test.txt").unwrap();
    println!("File size: {} bytes", metadata.len());
}
Enter fullscreen mode Exit fullscreen mode

Solution 3: Store Data in ManuallyDrop

If data is stored in ManuallyDrop, it dereferences to the inner type, so there is no need to use unwrap anymore.

When destroying values inside drop, you can use ManuallyDrop::take to gain ownership.

The downside is that ManuallyDrop::take is unsafe, so it must be placed inside an unsafe block.

We modify the earlier example as follows:

Step 1: Change the File definition

Add a ManuallyDrop wrapper to each field:

/// A type that represents a file handle
struct File {
    /// File name
    name: ManuallyDrop<String>,
    /// File descriptor
    fd: ManuallyDrop<i32>,
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Update the methods on File

There are two methods on File, and we need to add code to access the fields through the wrapper.

First, the open method:

/// A constructor that opens a file and returns a `File` instance
fn open(name: &str) -> Result<File, Error> {
    // Open the file with read and write permissions using `OpenOptions`
    let file: StdFile = OpenOptions::new()
        .read(true)
        .write(true)
        .open(name)?;

    // Obtain the file descriptor
    let fd: i32 = file.into_raw_fd();

    // Return a `File` instance
    Ok(File {
        name: ManuallyDrop::new(name.to_string()),
        fd: ManuallyDrop::new(fd),
    })
}
Enter fullscreen mode Exit fullscreen mode
  • The open method's parameter does not involve the File struct, so the parameter part does not need to change
  • The open method's return value involves File, so each field must be passed with ManuallyDrop::new

Next, the close method:

/// An explicit destructor that closes the file and returns any error
fn close(mut self) -> Result<(), Error> {
    // Use `std::mem::replace` to replace the `name` field with an empty string, and keep the original value in `name`
    let name =
        std::mem::replace(&mut self.name, ManuallyDrop::new("".to_string()));

    // Use `std::mem::replace` to replace the `fd` field with an invalid value (-1), and keep the original value in `fd`
    let fd =
        std::mem::replace(&mut self.fd, ManuallyDrop::new(-1));

    // Print
    println!("Closing file: {:?} with fd: {:?}", name, fd);

    // Convert the fd back into a `File` using `FromRawFd`
    let file: std::fs::File = unsafe {
        std::fs::File::from_raw_fd(*fd) // `fd` must be dereferenced first
    };

    // Flush file data to disk
    file.sync_all()?;

    // Truncate the file to 0 bytes
    file.set_len(0)?;

    // Flush the file again
    file.sync_all()?;

    // Drop the file instance; it will close the file automatically
    drop(file);

    // Return success
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode
  • Use std::mem::replace to replace the name field with an empty string and store the original value in name
  • Use std::mem::replace to replace the fd field with an invalid value (-1) and store the original value in fd
  • In std::fs::File::from_raw_fd(*fd), the argument must be dereferenced first, so write *fd

Step 3: Update the Drop implementation

fn drop(&mut self) {
    // Use `ManuallyDrop::take` to take the `name` field value and check whether it is an empty string
    let name = unsafe { ManuallyDrop::take(&mut self.name) };

    // Use `ManuallyDrop::take` to take the `fd` field value and check whether it is an invalid value
    let fd = unsafe { ManuallyDrop::take(&mut self.fd) };

    // Print
    println!("Dropping file: {:?} (fd: {:?})", name, fd);

    // If the `fd` field is not the invalid value, the file has not been closed or dropped yet, so perform the drop operation
    if fd != -1 || !name.is_empty() {
        let file = unsafe { std::fs::File::from_raw_fd(fd) };
        // Drop it
        drop(file);
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Use ManuallyDrop::take to take the values of name and fd, and check whether they are an empty string or an invalid value
  • If the fd field is not the invalid value (-1), or the name field is not empty, then the file has not been closed or dropped yet, so a drop operation is needed
  • In fact, you do not need both conditions (fd != -1 || !name.is_empty()); one is enough, because the value changes of name and fd happen together, and if one is invalid it means the whole struct has not yet been cleaned up

Step 4: Slightly Adjust main

fn main() {
    // ...unchanged above, omitted

    // Print the file name and fd (this needs to change)
    println!("File name: {}, fd: {}", *file.name, *file.fd);

    // ...unchanged below, omitted
}
Enter fullscreen mode Exit fullscreen mode
  • Use dereferencing to print the values

Full Code

use std::os::fd::{FromRawFd, IntoRawFd};
use std::fs::{File as StdFile, OpenOptions, metadata};
use std::io::Error;
use std::mem::ManuallyDrop;

/// A type that represents a file handle
struct File {
    /// File name
    name: ManuallyDrop<String>,
    /// File descriptor
    fd: ManuallyDrop<i32>,
}

impl File {
    /// A constructor that opens a file and returns a `File` instance
    fn open(name: &str) -> Result<File, Error> {
        // Open the file with read and write permissions using `OpenOptions`
        let file: StdFile = OpenOptions::new()
            .read(true)
            .write(true)
            .open(name)?;

        // Obtain the file descriptor
        let fd: i32 = file.into_raw_fd();

        // Return a `File` instance
        Ok(File {
            name: ManuallyDrop::new(name.to_string()),
            fd: ManuallyDrop::new(fd),
        })
    }

    /// An explicit destructor that closes the file and returns any error
    fn close(mut self) -> Result<(), Error> {
        // Use `std::mem::replace` to replace the `name` field with an empty string, and keep the original value in `name`
        let name =
            std::mem::replace(&mut self.name, ManuallyDrop::new("".to_string()));

        // Use `std::mem::replace` to replace the `fd` field with an invalid value (-1), and keep the original value in `fd`
        let fd =
            std::mem::replace(&mut self.fd, ManuallyDrop::new(-1));

        // Print
        println!("Closing file: {:?} with fd: {:?}", name, fd);

        // Convert the fd back into a `File` using `FromRawFd`
        let file: std::fs::File = unsafe {
            std::fs::File::from_raw_fd(*fd) // `fd` must be dereferenced first
        };

        // Flush file data to disk
        file.sync_all()?;

        // Truncate the file to 0 bytes
        file.set_len(0)?;

        // Flush the file again
        file.sync_all()?;

        // Drop the file instance; it will close the file automatically
        drop(file);

        // Return success
        Ok(())
    }
}

// Implement `Drop` for code that runs when the value leaves scope
impl Drop for File {
    fn drop(&mut self) {
        // Use `ManuallyDrop::take` to take the `name` field value and check whether it is an empty string
        let name = unsafe { ManuallyDrop::take(&mut self.name) };

        // Use `ManuallyDrop::take` to take the `fd` field value and check whether it is an invalid value
        let fd = unsafe { ManuallyDrop::take(&mut self.fd) };

        // Print
        println!("Dropping file: {:?} (fd: {:?})", name, fd);

        // If the `fd` field is not the invalid value, the file has not been closed or dropped yet, so perform the drop operation
        if fd != -1 || !name.is_empty() {
            let file = unsafe { std::fs::File::from_raw_fd(fd) };
            // Drop it
            drop(file);
        }
    }
}

fn main() {
    // Create a file named "test.txt" and write some content to it
    std::fs::write("test.txt", "Hello, world!").unwrap();

    // Open the file and obtain a `File` instance
    let file: File = File::open("test.txt").unwrap();

    // Print the file name and fd (this needs to change)
    println!("File name: {}, fd: {}", *file.name, *file.fd);

    // Close the file and handle any error
    match file.close() {
        Ok(()) => println!("File closed successfully"),
        Err(e) => println!("Error closing file: {}", e),
    }

    // Check the file size after closing
    let metadata = metadata("test.txt").unwrap();
    println!("File size: {} bytes", metadata.len());
}
Enter fullscreen mode Exit fullscreen mode

Choosing Between the Three Solutions

Which of these three solutions you choose depends on the actual situation, and usually the second one is the best. But if there are so many fields that unwrap becomes too noisy, you need to consider other options.

If the code is simple enough that you can easily verify its safety, then the third ManuallyDrop solution is also a very good choice.

Top comments (0)