How do I make proper FFI bindings package for a C API that consists of multiple libraries?

Hi,

I am working on a Rust bindings package for a C API which is implemented by a dozen or so of interrelated libraries. Some of these libraries are required, some are optional, and some are mutually exclusive. The applications are expected to link the required libraries, plus one of the mutually exclusive, plus optional ones, if needed.

The functions defined in these libraries are declared in a common C header file (in fact, a few headers, but there's still no 1-to-1 relation between most libraries and headers). Some of the libraries do not expose any functions to the user and are only used internally by other components. They still have to be linked.

My initial approach was to make a single, fat sys package that both exposes the declarations of the functions (produced by bindgen from the C headers), and tells rustc which libraries to link. To choose between the mutually exclusive libraries, as well as to enable the optional ones, I tried to use cargo features. The build script would panic if conflicting features were selected.

However, as I learned the hard way, the cargo features are designed to be additive, which kind of contradicts my usage pattern.

So, what could be a better approach to design my API? Make a sys package for every C library, and add them one by one as dependencies to the applications? However, if the application fails to add one of the required libraries, the linker will fail with obscure errors.

Redesigning the underlying C API is not an option, unfortunately.

Generally the solution I see is to use features and have th mutually exclusive options be non default

1 Like

The only thing that Cargo has that deals with mutually-exclusive things is the links key in the manifest. Note that this is just metadata, it doesn't actually introduce a link to the specified library.

I'd make a sys package for every C library, and make sure to set the links field for mutually-exclusive libraries to the same value. For example links = "libfoo-feature-dns-resolver" or so.

Then, in your Rusty wrapper for the C API, you can create constructors where you pass in an implementation for one of them mutually exclusive features. This pushes selecting the mutually-exclusive option onto the user of your crate.

Keeping with my DNS resolver example, you'd have the following crates:

  • foo-sys
  • foo-dns-resolver1-sys
  • foo-dns-resolver2-sys
  • foo (deps=foo-sys)
  • foo-dns-resolver1 (deps=foo, foo-dns-resolver1-sys)
  • foo-dns-resolver2 (deps=foo, foo-dns-resolver2-sys)

In foo you'd have something like:

trait DnsResolver {}

// Or you put this on your constructor, or whatever, anything to make
// sure the user of your API *has* to pass this in somewhere
fn init<T: DnsResolver>(dns_resolver: T) {
}

In foo-dns-resolverX:

struct DnsResolverX;

impl foo::DnsResolver for DnsResolverX {}

User depends on foo and foo-dns-resolverX.

3 Likes

Yes, that's what I have now. However, multiple discussions suggest against doing that. Furthermore, there's the issue of publishing the crate: apparently, it won't be possible to package it for publishing — e.g. this page

To make mutually-exclusive features less fragile, you can choose one option arbitrarily if both are selected. For example, instead of cfg(feature = "B") you could use cfg(not(feature = "A")).

You can also use build.rs to filter features and select winners more carefully (it can set custom cfg flags).

Definitely don't rely on default-features, because they're a pain to unset.

You could support both cargo options (for discoverability and convenience) and your own env vars to override them (for advanced users who need to sort out messed up features).

Rust doesn't care about .h files, so I'd split sys crates by what libraries are linked. The bindings can be added by another crate that groups the sys crates together.

Thanks everybody!

@jethrogb

This seems to be the most appropriate approach, but it does involve creating a whole lot of packages... I think I will investigate that.

@kornel

In the C API that I'm dealing with, I can't really fall back to some default. The client app must be explicit about the features it needs, and an error must be reported in case of an inconsistent configuration. My current approach is to panic in my wrapper's build.rs if conflicting features are selected.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.