How to handle Bindgen generating types aliases instead of callable functions

I'm trying to implement a -sys crate for a library I need in a project.

In the library headers, functions are defined like this :

typedef unsigned short (* SSV_InitLIB2) (const char * pcFichierSesam);

So, using bindgen, I got

pub type SSV_InitLIB2 = ::std::option::Option<
    unsafe extern "C" fn(pcFichierSesam: *const ::std::os::raw::c_char) -> ::std::os::raw::c_ushort,
>;

From there, I don't know how to manage the situation. I would like bindgen to generate something like this, how can I do ?

pub fn SSV_InitLIB2(pcFichierSesam: *const ::std::os::raw::c_char) -> ::std::os::raw::c_ushort;

Even in the C code you provided that is a typedef (C equivalent of type alias) so bindgen is faithful here. You would have to look at how that typedef is used in the C library to get any further.

1 Like

Yeah, true.

I edit my initial message : I don't have any other source files ... So I have to manage the situation with that.

Can I "configure" bindgen to make it generate functions instead of types ?

It doesn't make sense to translate a C typedef into a Rust fn, since the typedef in C represents a function type, not a function. A function type in Rust is the type that was generated.

In other words, in the C code there is no function called SSV_InitLIB2. It is probably used to define a function pointer in the C code. Is there a struct or another type in the C headers that uses SSV_InitLIB2?

2 Likes

I don't have access to the library source code, and the .h is given by the library developers and is the only thing I can base my generation on ...

I suspect there is, indeed, something twisted behind this situation, but I'm trying to deal with it :confused:

I was asking whether there are other references to SSV_InitLIB2 in the headers (the .h files), not the .c files. I'm trying to see how it is used in the headers.

Doh ... I've found some information in the lib documentation ... They suggest to load dynamically the library ... :person_facepalming:

Since 1.40.9.1, libraries can now be isolated on the same Workstation. This means that several different versions of FSV can now be installed on the same Workstation (e.g. FSV 1.40.9 and FSV 1.40.10).

Once all FSV libraries have been isolated, the SSV library must be loaded dynamically
by the healthcare software package.

# Under Windows
SESAM-Vitale Services are supported by an API (Application Programming Interface) library of dynamically linked functions. To implement a function of this type, the following preliminary operations must be carried out for a development environment using the C language.
1. Include the SESAM-Vitale Services function declaration file
function declaration file in the health software development project,
2. Load the library of these functions into memory (via the Windows function LoadLibrary, which returns a “handle”, i.e. a pointer to the library instance),
3. Declare a pointer to the function whose type is declared in the file
file included in step 1,
4. Set this pointer to the address of the function in the loaded library (via the Windows GetProcAddress function parameterized with the “handle” returned by the Windows LoadLibrary function). Two different methods are used to assign the pointer. The parameter passed to the GetProcAddress function designating the function can be :
- Either a character string containing the function name declared in the SESAM-Vitale Services function declaration file (see list below),
- Or the return of the MAKEINTRESSOURCE macro-instruction, to which the ordinal number corresponding to the function is passed. The list below shows how each SESAM-Vitale Services function matches its ordinal number

With an example :

#include «SSV.H»

void * hLibrary;
hLibrary = LoadLibrary («%ProgramFiles%\santesocial\ fsv\1.40.14\lib\ssvw32.dll»);

TFCTSSVALLOUER p_SSV_AllouerZoneMemoire;
p_SSV_InitLIB2 = (TFCTSSVALLOUER) GetProcAddress
(hLibrary, «SSV_InitLIB2»);

Or, with MacOS/Linux :

dlopen(<chemin complet + nom de la librairie>, RTLD_LAZY)
dlopen("/opt/santesocial/fsv/1.40.14/lib/libssv.so", RTLD_LAZY) ;

This is a terrible situation to be in, and a mistake we have resolved not to repeat at the company I work at. Just a warning: each time we pulled in a dependency we didn't have the source for we came to regret it.

Now we have a almost purely open source / source available / our own code stack, from the kernel and upwards (as well as all source for all microcontrollers). And we are working on phasing out the last bits we don't have source for. Then there is the question of the firmware for the industrial PCs we use (which is currently a mixed bag as I understand it but I'm not really involved in that).

I see, that would explain it.

Yeah, here it's the library managing crypted physical supports for the french health system ... So ... Not so much choice XD

Well, I'll maybe try with a dynamic linking ; they probably have a reason to force us to do things this way

It should work and you can expose nice Rust functions in your API. The only potential problem is that everyone using your library will have to install the native library.

I've been able to use a runtime loading, but now, I wonder how I could manage an automatic generation of the functions, to avoid manually doing what bindgen almost do.

Bindgen generate something like that :

pub mod BINDINGS {
    pub type SSV_InitLIB2 = ::std::option::Option<
        unsafe extern "C" fn(pcFichierSesam: *const ::std::os::raw::c_char) -> ::std::os::raw::c_ushort,
    >;

    pub type SSV_LireConfig = ::std::option::Option<
        unsafe extern "C" fn(
            pZDataOut: *mut *mut ::std::os::raw::c_void,
            psTailleDataOut: *mut usize,
        ) -> ::std::os::raw::c_ushort,
    >;
}

And functions, in my code, can be implemented like that (without optimisation, but that's not the question ;))

pub unsafe fn ssv_init_lib2(pcFichierSesam: *const ::std::os::raw::c_char) -> Result<::std::os::raw::c_ushort, Error> {
    let library = unsafe { libloading::Library::new("/my/library/path.so")? };
    let func_ptr: Symbol<'_, BINDINGS::SSV_InitLIB2> = unsafe { library.get("SSV_InitLIB2".as_bytes()) }?;
    let func = match *func_ptr {
        Some(f) => f,
        None => return Err(Error::SymbolMissing("SSV_InitLIB2")),
    };
    Ok(unsafe { func(pcFichierSesam) })
}

pub unsafe fn ssv_lire_config(pZDataOut: *mut *mut ::std::os::raw::c_void, psTailleDataOut: *mut usize) -> Result<::std::os::raw::c_ushort, Error> {
    let library = unsafe { libloading::Library::new("/my/library/path.so")? };
    let func_ptr: Symbol<'_, BINDINGS::SSV_LireConfig> = unsafe { library.get("SSV_LireConfig".as_bytes()) }?;
    let func = match *func_ptr {
        Some(f) => f,
        None => return Err(Error::SymbolMissing("SSV_LireConfig")),
    };

    Ok(unsafe { func(pZDataOut, psTailleDataOut) })
}

Could I generate these functions with somes macros, without having to explicitly define the parameters types ? I don't find how ...

I wonder if I could not write a custom formatter for bindgen, to generate the functions ?

I don't think there is a super straightforward way to do that as of right now, I experimented around with it a bit and this is what I came up with, using a modified bindgen (super hacky for now, not at all something that should be contributed to bindgen in it's current state): GitHub - JustusFluegel/test-bindgen-dynlib-typedefs

Especially see the build.rs there for the bindgen options as well as the last commit on the bindgen submodule - Alternatively, although more tedious, you could use standard bindgen with a similar build.rs configuration and a override for each type which sets it as a function declaration directly, as such it will be detected by the dynlib logic already present in bindgen:

// this could be inside a imported header, this file with the overrides beeing a wrapper header file around it
typedef unsigned short (* SSV_InitLIB2) (const char * pcFichierSesam);

/**
* <div rustbindgen replaces="SSV_InitLib2"></div>
*/
unsigned short SSV_InitLIB2_override(const char * pcFichierSesam);

maybe that helps, although I know neither are perfect solutions :confused:

I didn't find solution for not having to manually write something for each of my functions. Disappointing but that's it.

So, now :

  1. I generate "normal" bindings with bindgen (I've tried things with the default_alias_style bindgen config, to get Struct instead of Type in the generated bindings, but I didn't found a nice way to use the result, so ...)

  2. I load the bindings in my lib.rs

pub mod BINDINGS {
    include!(concat!(env!("OUT_DIR"), "/bindings_1.40.13.rs"));
}

// We need to keep the this use statement to get `ssv_function` macro working well
use BINDINGS::*;
  1. I have a struct, letting centralize the library loading and derived functions
mpl SSVLibrary {
    pub fn new(version: SUPPORTED_FSV_VERSIONS) -> Result<Self, Error> {
        let library_path = Self::get_library_path(&version);
        let library = unsafe { libloading::Library::new(library_path)? };
        Ok(SSVLibrary { version, library })
    }

    pub fn library(&self) -> &libloading::Library {
        &self.library
    }

    // ...
  1. I made a macro to easily generate functions, in the struct, which get the symbol, exposing standard Rust type or libc ones ; it lower a little the boilerplate code to use the library functions
// The macro itself
/// Macro to generate a function that implements a call to an external function in BINDINGS
macro_rules! ssv_function {
    ($binding:ty, $func_name:ident, {$($arg_name:ident: $arg_type:ty),*}) => {
        /// # Safety
        /// This function is unsafe because it calls an external function through FFI.
        /// The caller must ensure that the provided arguments are valid and that the
        /// external function is safe to call.
        pub unsafe fn $func_name(&self, $($arg_name: $arg_type),*) -> Result<u16, Error> {
            let func_struct: libloading::Symbol<'_, $binding> =
                unsafe { self.library.get(stringify!($binding).as_bytes())? };
            let func = match *func_struct {
                Some(func) => func,
                None => return Err(Error::SymbolMissing(stringify!($binding))),
            };
            Ok(func($($arg_name),*))
        }
    };
}


// Its usage, in the struct

     ssv_function!(SSV_InitLIB2, ssv_init_lib2, {
        pcFichierSesam: *const i8
    });

    ssv_function!(SSV_LireConfig, ssv_lire_config, {
        pZDataOut: *mut *mut libc::c_void,
        psTailleDataOut: *mut usize
    });

    // ...

It works well. I'm not 100% sure about the safety of the implementation, but ... For now I think it shoul be enough

(I'm still having a problem, now : to handle various bindings versions ... but I'll create a new topic about it)

If that works for you, that's great, go for it! - although I think it's probably a similar amount of effort to just use the overrides in the c header files and use the existing bindgen infrastructure with .dynamic_library_name("foo") which integrates well with libloading, for your example the required wrapper header file would be something along the lines of

// file: wrapper.h
// the provided header file
#include <header.h>

// the name for the items below don't actually matter as long as they don't clash with anything else,
// since they override symbols from above and as such they take the names of them in the output

/** <div rustbindgen replaces="SSV_InitLIB2"></div> */
unsigned short SSV_InitLib2_override(const char* pcFichierSesam);

/** <div rustbindgen replaces="SSV_LireConfig"></div> */
unsigned short SSV_LireConfig_override(void** pZDataOut, size_t* psTailleDataOut);

especially since if you stay in the c-world you can just copy the signature, remove the typedef keyword as well as a (,) and * and you have your override - might even be pretty straightforward to automate in another build step before generating the bindings in build.rs, although I haven't experimented around with that a lot.

and then just a build.rs like so would work:

// file: build.rs

use std::{env, path::PathBuf};

fn main() {
    println!("cargo::rerun-if-changed=header.h");
    println!("cargo::rerun-if-changed=wrapper.h");

    // if you automate the override generation include the wrapper generation here

    let bindings = bindgen::Builder::default()
        .header("wrapper.h")
        .dynamic_library_name("MyLibName")
        // set this if all items are always in the dynlib and individual
        // items aren't allowed to be missing
        .dynamic_link_require_all(false)
        .generate()
        .expect("Unable to generate bindings");

    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindigns");
}

which would result in output that could be used like so:

// file: src/sample.rs
// asuming a mod bindings; exists where the bindings are included

// MyLibName is the name you used in the build.rs in the `dynamic_library_name` option
use crate::bindings::MyLibName;

fn sample() {
    // this could be in a impl on MyLibName as well, for example a new_by_version
    // or something
    let loaded_lib = unsafe { MyLibName::new(library_path_from_version(version)) };
    // depending on if you enable the dynamic_link_require_all option in build.rs
    // the entire library loading with new returns a result or each individual
    // function returns a result with a error value if it's missing
    
    // assuming you set it to true, include this:
    // let loaded_lib = loaded_lib.expect("All methods to be found in loaded library");

    // this panics if you set dynamic_link_require_all to false and it its not found in the library
    let some_fn_result = unsafe { loaded_lib.SSV_InitLib2(c"foo".as_ptr()) };

    // if you want to check it manually, use:
    if let Ok(SSV_InitLib2) = loaded_lib.SSV_InitLib2 {
        let some_fn_result = unsafe { (SSV_InitLib2)(c"foo".as_ptr()) };
    } else {
        // this is the symbol missing case
    }
}

Yeah, for sure

I didn't choose this way, because we're multiple maintainers, so "manual" modification of the wrappers feel not very safe in this case, and the library have significant changes every X months and is published with ... 3 variants of the headers (one for each mac/win/linux targets ...) so, it's very messy ... XD #nightmares

Well you wouldn't modify the headers, you would add external overrides and keep the original header file - but I see where you are coming from, as i said if that works for you that's great, go for it :slight_smile:

If you are fine with using a modified version of rust-bindgen (which in my mind is a build tool, so idk how important keeping up with the current version of bindgen is for you but for me as long as the "old" modified version (currently master, but might get out of date in a while) still works for everything I need it it's fine for me), take a look at the example I shared above which should probably work just fine without any manual declaration for each function at all.

( Although it's probably thinkable to just open a issue and get it integrated into upstream as well, I don't think the changes needed to support your usecase are really very huge, I just don't understand c syntax well enough to think that i haven't missed an edgecase and I don't have your headers to test it on so I am not that comfortable with finishing it up and creating a pr for it )

1 Like

Yeah, true, your comments are really inspiring ! I keep this in mind, for the day where I would spend time again on the subject :slight_smile: