2016-05-11 11 views
5

Ho creato un wrapper attorno a una libreria C che crea un dispositivo che è necessario chiudere esplicitamente.Come eseguire il wrapping di una libreria nativa con semantica init/exit

Scrivere le funzioni FFI non elaborate era semplice, ma come posso renderlo ergonomico in un wrapper di livello superiore?

In particolare, dovrei farlo nello stile RAII e utilizzare solo Drop per garantire che la chiusura venga chiamata quando esce dall'ambito, invece di esporre il metodo close() al chiamante? Qual è il modo più idiomatico in Rust?

ci sono fondamentalmente 3 opzioni:

  1. involucro sottile che richiede gli stessi close() chiamate come la libreria C;
  2. stile RAII che non ha esposto close(), solo un'implementazione Drop;
  3. C# dispose() implementazione in stile che tiene traccia dello stato chiuso e consente entrambe le forme di chiusura.

L'ultima forma assomiglia a questo:

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(); 
    } 
} 
+0

Opzione numero 2. I mai visto l'opzione 1 o 3 usata in ruggine. Nel tuo esempio, cosa succede quando 'close' viene chiamato più di una volta? – malbarbo

+0

Ben avvistato, l'if-closed dovrebbe ovviamente andare nel metodo close() e non nel drop(). Modificato. –

risposta

4

idiomaticamente, credo si sarebbe solo implementare Drop. Non sono a conoscenza di alcun tipo di libreria standard che implementa lo schema di consentire all'utente di disporre manualmente una risorsa (chiamando un metodo) e automaticamente (facendo cadere).

Questo porta persino a casi strani. Ad esempio, la chiusura di un file tramite una funzione come fclose può generare errori. Tuttavia, un distruttore Rust non può restituire un codice di errore all'utente. Ciò significa che errors like that are swallowed.

Questo porta al motivo che è possibile che supporti entrambi. Il tuo metodo close potrebbe restituire un Result e quindi potresti ignorare quel risultato in Drop.


Come Jsor points out, che vorreste probabilmente il metodo close per accettare il tipo per valore. Mi sono anche reso conto che potresti utilizzare un valore NULL per indicare se il valore era stato chiuso o meno.

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"); 
} 

Se tu avessi un qualche tipo di errore recuperabile che potrebbero verificarsi durante la chiusura del dispositivo, è possibile restituire l'oggetto indietro all'utente di provare di nuovo:

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) 
     }, 
    } 
} 
+1

Per il caso di segnalazione degli errori, potresti avere ancora più semantica Rustic avendo 'fn close (self) -> Result' invece di' fn close (& mut self) -> Result'. Dove il 'Errore' restituisce il risultato forse contiene' self'so puoi fare ulteriore maneggevolezza. Il vero problema qui è use-after-resource-release. – LinearZoetrope

+0

più vicino a quest'ultimo. – LinearZoetrope

+0

@Jsor qualcosa come quell'aggiornamento? – Shepmaster

Problemi correlati