Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recommended way to wrap C lib initialization/destruction routine

Tags:

rust

I am writing a wrapper/FFI for a C library that requires a global initialization call in the main thread as well as one for destruction.

Here is how I am currently handling it:

struct App;

impl App {
    fn init() -> Self {
        unsafe { ffi::InitializeMyCLib(); }
        App
    }
}

impl Drop for App {
    fn drop(&mut self) {
        unsafe { ffi::DestroyMyCLib(); }
    }
}

which can be used like:

fn main() {
    let _init_ = App::init();
    // ...
}

This works fine, but it feels like a hack, tying these calls to the lifetime of an unnecessary struct. Having the destructor in a finally (Java) or at_exit (Ruby) block seems theoretically more appropriate.

Is there some more graceful way to do this in Rust?

EDIT

Would it be possible/safe to use this setup like so (using the lazy_static crate), instead of my second block above:

lazy_static! {
    static ref APP: App = App::new();
}

Would this reference be guaranteed to be initialized before any other code and destroyed on exit? Is it bad practice to use lazy_static in a library?

This would also make it easier to facilitate access to the FFI through this one struct, since I wouldn't have to bother passing around the reference to the instantiated struct (called _init_ in my original example).

This would also make it safer in some ways, since I could make the App struct default constructor private.

like image 556
Jacob Brown Avatar asked Apr 01 '16 14:04

Jacob Brown


2 Answers

I know of no way of enforcing that a method be called in the main thread beyond strongly-worded documentation. So, ignoring that requirement... :-)

Generally, I'd use std::sync::Once, which seems basically designed for this case:

A synchronization primitive which can be used to run a one-time global initialization. Useful for one-time initialization for FFI or related functionality. This type can only be constructed with the ONCE_INIT value.

Note that there's no provision for any cleanup; many times you just have to leak whatever the library has done. Usually if a library has a dedicated cleanup path, it has also been structured to store all that initialized data in a type that is then passed into subsequent functions as some kind of context or environment. This would map nicely to Rust types.

Warning

Your current code is not as protective as you hope it is. Since your App is an empty struct, an end-user can construct it without calling your method:

let _init_ = App;

We will use a zero-sized argument to prevent this. See also What's the Rust idiom to define a field pointing to a C opaque pointer? for the proper way to construct opaque types for FFI.

Altogether, I'd use something like this:

use std::sync::Once;

mod ffi {
    extern "C" {
        pub fn InitializeMyCLib();
        pub fn CoolMethod(arg: u8);
    }
}

static C_LIB_INITIALIZED: Once = Once::new();

#[derive(Copy, Clone)]
struct TheLibrary(());

impl TheLibrary {
    fn new() -> Self {
        C_LIB_INITIALIZED.call_once(|| unsafe {
            ffi::InitializeMyCLib();
        });
        TheLibrary(())
    }

    fn cool_method(&self, arg: u8) {
        unsafe { ffi::CoolMethod(arg) }
    }
}

fn main() {
    let lib = TheLibrary::new();
    lib.cool_method(42);
}
like image 63
Shepmaster Avatar answered Oct 21 '22 01:10

Shepmaster


I did some digging around to see how other FFI libs handle this situation. Here is what I am currently using (similar to @Shepmaster's answer and based loosely on the initialization routine of curl-rust):

fn initialize() {
    static INIT: Once = ONCE_INIT;
    INIT.call_once(|| unsafe {
        ffi::InitializeMyCLib();
        assert_eq!(libc::atexit(cleanup), 0);
    });

    extern fn cleanup() {
        unsafe { ffi::DestroyMyCLib(); }
    }
}

I then call this function inside the public constructors for my public structs.

like image 1
Jacob Brown Avatar answered Oct 21 '22 00:10

Jacob Brown