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 ofselfto remain in place. -
Droptakes&mut selfrather thanself, soDropcannot simply call the explicit destructor and ignore its result, becauseDropdoes not ownself
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());
}
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
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>,
}
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,
}),
})
}
- Because this code only uses
Filein 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",
))
}
}
- After receiving the parameter, we first use pattern matching to access the field values
- If the
innerfield isNone, 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
}
}
- After receiving the parameter, we first use pattern matching to get the field values
- If the
innerfield isNone, 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
}
- The original type is
Option<InnerFile>. After calling.as_ref(), it becomesOption<&InnerFile> - Once it becomes
Option<&InnerFile>, the value extracted byunwrapis a reference rather than an owned value -
file.inneris anOption<InnerFile>. Accessing theOptionvalue directly would require moving ownership or pattern matching (for example throughtake()orunwrap()), which would destroy the inner value of theOption, soas_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());
}
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>,
}
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),
})
}
- The
openmethod's parameter does not involve theFilestruct, so the parameter part does not need to change - The
openmethod's return value involvesFile, so each field needs to be wrapped inSome
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",
))
}
}
- The parameter must first be pattern-matched, and we use
std::mem::taketo 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
}
}
- The parameter must first be pattern-matched, and we use
std::mem::taketo 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
}
- 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,
unwrapextracts a reference rather than an owned value - Accessing the
Optionvalue directly requires moving ownership or pattern matching (for example throughtake()orunwrap()), which destroys the inner value of theOption, soas_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());
}
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>,
}
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),
})
}
- The
openmethod's parameter does not involve theFilestruct, so the parameter part does not need to change - The
openmethod's return value involvesFile, so each field must be passed withManuallyDrop::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(())
}
- Use
std::mem::replaceto replace thenamefield with an empty string and store the original value inname - Use
std::mem::replaceto replace thefdfield with an invalid value (-1) and store the original value infd - 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);
}
}
- Use
ManuallyDrop::taketo take the values ofnameandfd, and check whether they are an empty string or an invalid value - If the
fdfield is not the invalid value (-1), or thenamefield 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 ofnameandfdhappen 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
}
- 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());
}
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)