Manage various versions of a C library loaded at runtime

This topic follows a previous one where I was fighting with the bindings generation for an extern C library loaded at runtime.

I want to offer the opportunity to handle various versions ... at runtime.

I have multiple bindings_vX.X.X.rs files generated at build time.
And I've a struct loading and storing the library and the version number, which should expose the Rust version of the functions in the C library.

I wonder how I can use "I don't know what" (traits ? other things ?) to let the user do something like :

let library_v1 = MyLibrary::new("/path/to/library/v1", SupportedVersions::V1);
let library_v2 = MyLibrary::new("/path/to/library/v2", SupportedVersions::V2);

// V1 & V2 functions doesn't always have the same signatures
let result: u16 = library_v1.do_something("arg1", 12);
let result: String = library_v2.do_something(12, "arg1", false);

Any idea ?

I can think of 3 approaches:

  1. a Individual library struct for each library version
  2. prefixes/suffixes on each function version which changed in a newer version
  3. use some kind of generics:

I think option 3 is probably the nicest, so let me draft a quick example on how you could go about it:

// avoid people implementing the LibVersion trait outside your crate
mod sealed { pub trait Sealed {} }
pub trait LibVersion: sealed::Sealed { }

struct Version1;
impl sealed::Sealed for Version1 {}
impl LibVersion for Version1 {}

struct Version2;
impl sealed::Sealed for Version2 {}
impl LibVersion for Version2 {}

struct MyLibrary<Version: LibVersion> { 
 // ...
}

// think of a better name plz
pub(crate) trait SharedFeaturesVersion1And2 {}
impl SharedFeaturesVersion1And2 for Version1 {}
impl SharedFeaturesVersion1And2 for Version2 {}

// signature only in version 1
impl MyLibrary<Version1> {
    fn foo(arg1: bool) { /* ...*/ }
}

// signature only in version 2
impl MyLibrary<Version2> {
    fn foo(arg1: bool, arg2: const * c_str) { /* ...*/ }
}

// signature shared between version 1 and 2
impl<T: SharedFeaturesVersion1And2> MyLibrary<T> {
    fn bar(arg1: bool, arg2: const * c_str, arg3: u16) { /* ...*/ }
}


// usage:

let my_lib_v1 = MyLibrary::<Version1>::new(...);

let my_lib_v2 = MyLibrary::<Version2>::new(...);

// different functions
my_lib_v1.foo(...);
my_lib_v2.foo(...,...);

// same function called
my_lib_v1.bar(...,...,...);
my_lib_v2.bar(...,...,...);

this approach is similar to the typestate pattern often used for builders, etc.

1 Like

Thanks for your answer, it helps me so much :tada: !!!

Here is the full example, with details showing how/where I've implemented the ::new constructor initializing the library :

use std::marker::PhantomData;

mod sealed { pub trait Sealed {} }

struct FakeLibrary {
    value: i32,
}
impl FakeLibrary {
    fn add_ten(&self) -> i32 {
        self.value + 10
    }
}

trait MyLibCommon {
    fn new(init_value: i32) -> Result<Self, String> where Self: Sized;
    fn get_version_code(&self) -> &'static str;
}
trait MyLibVersion: sealed::Sealed {
    fn get_version_code() -> &'static str;
}

struct V1;
impl sealed::Sealed for V1 {}
impl MyLibVersion for V1 {
    fn get_version_code() -> &'static str {
        "v1"
    }
}

struct V2;
impl sealed::Sealed for V2 {}
impl MyLibVersion for V2 {
    fn get_version_code() -> &'static str {
        "v2"
    }
}

struct MyLib<Version: MyLibVersion> {
    library: FakeLibrary,
    _phantom: PhantomData<Version>,
}

impl<Version: MyLibVersion> MyLibCommon for MyLib<Version> {
    fn new(init_value: i32) -> Result<Self, String> {
        Ok(MyLib {
            library: FakeLibrary { value: init_value },
            _phantom: PhantomData,
        })
    }
    fn get_version_code(&self) -> &'static str {
        Version::get_version_code()
    }
}

impl MyLib<V1> {
    fn add_twenty_v1(&self) -> i32 {
        self.library.add_ten() + 10
    }
}

impl MyLib<V2> {
    fn add_twenty_v2(&self) -> String {
        let result = self.library.add_ten() + 10;
        result.to_string()
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn init_mylib_v1_v2() {
        let _lib_v1 = MyLib::<V1> {
            library: FakeLibrary { value: 12 },
            _phantom: PhantomData,
        };
        let _lib_v2 = MyLib::<V2> {
            library: FakeLibrary { value: 18 },
            _phantom: PhantomData,
        };
    }

    #[test]
    fn get_version_code() {
        let lib_v1 = MyLib::<V1> {
            library: FakeLibrary { value: 12 },
            _phantom: PhantomData,
        };
        let lib_v2 = MyLib::<V2> {
            library: FakeLibrary { value: 18 },
            _phantom: PhantomData,
        };

        assert_eq!(lib_v1.get_version_code(), "v1");
        assert_eq!(lib_v2.get_version_code(), "v2");
    }

    #[test]
    fn use_new_contructor() {
        let lib_v1 = MyLib::<V1>::new(18).unwrap();
        let lib_v2 = MyLib::<V2>::new(72).unwrap();
    }

    #[test]
    fn run_library() {
        let lib_v1 = MyLib::<V1>::new(18).unwrap();
        let lib_v2 = MyLib::<V2>::new(72).unwrap();

        assert_eq!(lib_v1.library.add_ten(), 28);
        assert_eq!(lib_v2.library.add_ten(), 82);
    }

    #[test]
    fn run_library_v1() {
        let lib_v1 = MyLib::<V1>::new(18).unwrap();
        assert_eq!(lib_v1.add_twenty_v1(), 38);
    }

    #[test]
    fn run_library_v2() {
        let lib_v2 = MyLib::<V2>::new(72).unwrap();
        assert_eq!(lib_v2.add_twenty_v2(), "92");
    }
}