I'm actually doing a very similar thing atm, so the problem space is quite fresh in my mind. The one I'm linking isn't configurable, but details mean that it must be dynamically linked and the Rust bindings cannot control what version of the lib is linked, so it's quite desirable for the Rust bindings to support multiple lib versions. Also, while it could theoretically use bindgen at build time, I'd much rather not. (The headers are actually straightforward enough that it's almost practical to transform them with just a series of regex replaces…)
First and importantly, note that if you use the package.links
key (and you absolutely should if you ever export nonmangled symbols rather than only ever import them), your buildscript can be overridden. Anything you do should ideally be possible to specify declaratively in such an override, as when an override is used, the buildscript is not run at all. (If your buildscript would actually build anything, pointing to an externally previously built artifact.)
So for example, if I do decide to have the sys crate simultaneously support multiple versions, the package configuration for #define MYLIB_VERSION 0x0002'02'21
(16:8:8 BCD product.major.minor) might look like:
[target."*".mylib]
rustc-link-lib = ["mylib"]
rustc-link-search = ["/path/to/mylib/bin"]
rustc-cfg = [
'mylib_version_major="02"',
# skip prerelease minor versions 00...02
'mylib_version_minor="03"',
'mylib_version_minor="04"',
'mylib_version_minor="05"',
# ...
'mylib_version_minor="20"',
'mylib_version_minor="21"',
]
[target."*".mylib.metadata]
bin = "/path/to/mylib/bin"
doc = "/path/to/mylib/doc"
inc = "/path/to/mylib/inc"
version = "00020221"
Any items added in 2.02.17 would be gated by #[cfg(mylib_version_minor = "17")]
and an understanding that this is a compatibility bound, not an equality bound, just like cargo dependencies. For any packages downstream of mylib-sys to also be mylib version independent, they would need to regenerate the cfg from the DEP_MYLIB_VERSION
env var in their own buildscript.
A bit annoying to write out like that? Definitely; I'm strongly considering not doing this and instead tying lib version to package version (it gets checked by the lib at runtime to be compatible for ABI safety, thankfully) and telling the root bin to add a =
version constraint on the sys package instead (they need to handle providing packaging of the dylib anyway anyway for reasons).
Also fun is that what C libraries can consider API compatible does not mean Rust considers it compatible, even with the minimally translated bindings and the library providing ABI compatibility guarantees. Namely, adding fields to a struct
is source compatible in C (although the fields will stay uninitialized) and this can even be made ABI compatible if such a struct is only ever passed by pointer and with some indication of what declaration version it is (commonly via the first field being set to the size of the struct). So either -sys
libraries binding to libraries playing such tricks need to inject #[non_exhaustive]
on any such types or loudly disclaimer that it's following C API compatibility rules, not the Rust ones.