How to wrap your own library using init / exit semantics

I wrapped around the C library, which creates a device that you must explicitly close.

Writing the original FFI functions was easy, but how can I make it ergonomic Rust in a higher-level shell?

In particular, should I do this RAII style and use only Drop to ensure that the closure is called when it goes out of scope, instead of exposing the close() method to the caller? Which path is the most idiomatic in Rust?

There are basically 3 options:

  • A thin shell that requires the same close() calls as the C library;
  • RAII style, which does not have open close() , only Drop implementation;
  • C # dispose() is a style implementation that tracks a closed state and allows both forms of closure.

The last form is as follows:

 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(); } } 
+5
source share
1 answer

Idiom, I believe that you are just implementing Drop . I do not know any standard types of libraries that implement a template that allows the user to manually manage the resource (method call) and automatically (by deleting).

This even leads to some strange cases. For example, closing a file with a function like fclose can generate errors. However, the Rust destructor cannot return an error code to the user. This means that such errors are swallowed .

This leads to the fact that you can support them. Your close method can return a Result , and then you can ignore this result in Drop .


As Jsor points out , you probably want your close method to accept a type by value. I also realized that you can use a NULL value to indicate whether this value was 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 error that could be fixed when closing the device, you can 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) }, } } 
+4
source

All Articles