I made a wrapper around a C library which creates a device that you must explicitly close.
Writing the raw FFI functions was easy, but how do I make it ergonomic Rust in a higher level wrapper?
Specifically, should I be doing it the RAII style and only using Drop
to ensure the close is called when it goes out of scope, instead of exposing the close()
method to the caller? Which way is the most idiomatic in Rust?
There are basically 3 options:
close()
calls as the C library;close()
, only a Drop
implementation;dispose()
-style implementation that tracks the closed state and allows both forms of closing.The last form looks like this:
pub enum NativeDevice {} // Opaque pointer to C struct
fn ffi_open_native_device() -> *mut NativeDevice { unimplemented!() }
fn ffi_close_native_device(_: *mut NativeDevice) {}
fn ffi_foo(_: *mut NativeDevice, _: u32) -> u32 { unimplemented!() }
pub struct Device {
native_device: *mut NativeDevice,
closed: bool,
}
impl Device {
pub fn new() -> Device {
Device {
native_device: ffi_open_native_device(),
closed: false,
}
}
pub fn foo(&self, arg: u32) -> u32 {
ffi_foo(self.native_device, arg)
}
pub fn close(&mut self) {
if !self.closed {
ffi_close_native_device(self.native_device);
self.closed = true;
}
}
}
impl Drop for Device {
fn drop(&mut self) {
self.close();
}
}
Idiomatically, I believe you would just implement Drop
. I am unaware of any standard library types that implement the pattern of allowing the user to dispose a resource manually (calling a method) and automatically (by dropping).
This even leads to some strange cases. For example, closing a file via a function like fclose
can generate errors. However, a Rust destructor can not return a failure code to the user. This means that errors like that are swallowed.
This leads to the reason that you may want to support both. Your close
method could return a Result
and then you could ignore that result in Drop
.
As Jsor points out, you'd probably want your close
method to accept the type by value. I also realized you could use a NULL
value to indicate if the value had been closed or not.
use std::ptr;
enum NativeDevice {} // Opaque pointer to C struct
fn ffi_open_native_device() -> *mut NativeDevice {
0x1 as *mut NativeDevice
}
fn ffi_close_native_device(_: *mut NativeDevice) -> u8 {
println!("Close was called");
0
}
struct Device {
native_device: *mut NativeDevice,
}
impl Device {
fn new() -> Device {
let dev = ffi_open_native_device();
assert!(!dev.is_null());
Device {
native_device: dev,
}
}
fn close(mut self) -> Result<(), &'static str> {
if self.native_device.is_null() { return Ok(()) }
let result = ffi_close_native_device(self.native_device);
self.native_device = ptr::null_mut();
// Important to indicate that the device has already been cleaned up
match result {
0 => Ok(()),
_ => Err("Something wen't boom"),
}
}
}
impl Drop for Device {
fn drop(&mut self) {
if self.native_device.is_null() { return }
let _ = ffi_close_native_device(self.native_device);
// Ignoring failure to close here!
}
}
fn main() {
let _implicit = Device::new();
let explicit = Device::new();
explicit.close().expect("Couldn't close it");
}
If you had some kind of recoverable error that might occur when closing the device, you could return the object back to the user to try again:
enum Error {
RecoverableError(Device),
UnknownError,
}
fn close(mut self) -> Result<(), Error> {
if self.native_device.is_null() {
return Ok(());
}
let result = ffi_close_native_device(self.native_device);
match result {
0 => {
self.native_device = ptr::null_mut();
// Important to indicate that the device has already been cleaned up
Ok(())
},
1 => Err(Error::RecoverableError(self)),
_ => {
self.native_device = ptr::null_mut();
// Important to indicate that the device has already been cleaned up
Err(Error::UnknownError)
},
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With