Splitting crate into shared library and frontend: what are the best practices?

What are the best practices for splitting a crate between a C interface shared library and a Rust frontend? I currently have a workspace setup like this:

  • crate-shared: exposes a stable extern "C" API and has crate-type = ["cdylib"]. This crate is meant to be used by either Rust or C.
  • crate-frontend: depends on the shared crate with crate-shared = { path = "../crate-shared" }. It is meant to be

The goal is that crate-frontend could be distributed via crates.io, but the dynamic output of crate-shared will be shipped with a system package manager (possibly with a feature flag to statically link). I am just trying to get a local toolchain working.

The problem is that most cargo commands wind up saying use of undeclared crate or module wherever I use crate_shared::x in crate-frontend. Adding -C prefer-dynamic doesn't seem to have an effect, and I am not sure whether that would affect crate-shared's ability to be a standalone dynamic library.

It seems like the issue is that Cargo is looking for a Rust library when compiling crate-frontend, rather than just using crate-shared like a header file. I am not doing anything specific in the crates, just extern "C" in -shared but nothing that emits -L/-l to crate-frontend or #[link...].

What am I missing here, or is there a better way to do this easily?

when use a package with only cdylib crate type targets, you should get a warning like this:

warning: The package foo provides no linkable target. The compiler might raise an error while compiling bar. Consider adding 'dylib' or 'rlib' to key crate-type in foo's Cargo.toml. This warning might turn into a hard error in the future.

rustc can only use libraries compiled with rust metadata as extern crate dependencies.

the short and easy solution is add rlib or just lib to the library package, like this:

[lib]
crate-type = ["cdylib", "lib"]

this way, rust program can use it as dependency and statically link against it, while it can also be built as a standalone shared library.

Thanks for confirming the behavior and for the suggestion. Is there any way to get the second crate to use the shared library without linking? That is,

Adding undefined extern function stubs to crate-frontend would work, but I am hoping for some way to do this automatically.

well, in that case, your library crate is essentially treated as it were an ffi library. so you need declarations of "binding" for the library. I don't know if there's automatic way to generate those bindings from the rust source. if you export your APIs to a C header file, you might use bindgen, but it feels weird to go from rust to C to rust again.

it's really an uncommon use case. maybe checkout https://cxx.rs for some inspiration?

I thought about doing cbindgen->bindgen to get the API, but it seemed like there should be a more straightforward way. I would have thought that packaging crates that can be used as either (stable C) dynamics or statics would have been more common.

Thanks for the help, I'll take a look.

You may be interested in https://github.com/rust-lang/rfcs/pull/3435

Alternatively you could look into the dylib crate type which allows a Rust crate to dynamically link to a statically known crate. Turning on and off is a bit tricky and requires an additional crate, although you won't need to redeclare your interface (check out for example how bevy does it, although note that they do this for speeding up compile times). I'm not sure if dylibs can be loaded from C though (they are .so/.dll/.dylib so they probably can).

Appreciate the link, that would resolve the header options and I'll bring up the use case here. It would at least hopefully take care of the interfaces issue

Somebody pointed me at Tracking Issue for RFC 3028: Allow "artifact dependencies" on bin, cdylib, and staticlib crates · Issue #9096 · rust-lang/cargo · GitHub which seems like it fixes at least part of this problem too.

It's unfortunate that there doesn't seem to be a good way to do this at this time, but the best bet for now does seem to be sending everything through cbindgen then bindgen

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.