Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to wrap a native library with init/exit semantics

Tags:

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:

  1. Thin wrapper that requires the same close() calls as the C library;
  2. RAII style that has no exposed close(), only a Drop implementation;
  3. C# 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();
    }
}
like image 869
Anders Forsgren Avatar asked May 11 '16 20:05

Anders Forsgren


1 Answers

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)
        },
    }
}
like image 195
Shepmaster Avatar answered Sep 28 '22 03:09

Shepmaster