Best practice of extending a no_std crate

Hi!

I'm currently making a library that needs to work without the standard library but can provide more features by using it.

I see two solutions to this, and I'm split between which of them I like the best. I appreciate all insight you might have regarding this subject.

  1. Implement the set of the functionality working with no_std in a "core crate" and extend the functionality with a new crate depending on the original crate.

    • This might require the no_std crate to export things otherwise not required.
    • Should the crate using std re-export the no_std crate?
  2. Having an std [feature] in the crate and using #![cfg_attr(not(feature="std"), no_std)] instead of #![no_std] in the original crate. Then using conditional compilation to decide what should be compiled.

    • How will this work with the documentation generated at crates.io?
    • What does this mean for testing? cargo test --feature "std" should be added to the CI tool?
  3. Some better alternative that I haven't thought about.

Are there any other projects I can look to for inspiration?

To satisfy any curiosity, I'm currently implementing the Uavcan protocol in Rust.

This is usually done by having a default crate feature, "std". No_std users can then disable this feature to get the reduced functionality.

This "best-practice" was discussed for the libz blitz, but I can't find the links at the moment..

There's also a related keyword on crates.io. Looking at those crates should give some suggestions.

1 Like

Good tip about looking to the no_std crates on crates.io. I now see that serde uses the "std" feature, I think this is an excellent crate to use for inspiration.

Do you remember on which forum the discussion was? I will try to look it up tonight myself. Sounds like a perfect read as a discussion relating to libz blitz will probably contain comments from some of the most knowledgable people in the rust community.

Thanks!

1 Like

Serde is definitely worth emulating. It was one of the crates spearheading this discussion.

I don't recall the forum exactly, but it was either here, the internals.rust-lang forum, or a github RFC discussion.

As for your original questions on docs and testing impact: having it as a default-active feature means you won't need special config for those cases. Complete doc/testing is always done in the general case, and no_std users just deselect part of that.

2 Likes

I know it's mentioned in the api guidelines crate which currently resides in the nursery (GitHub repo).

Do not include words in the name of a Cargo feature that convey zero meaning, as in use-abc or with-abc. Name the feature abc directly.

This arises most commonly for crates that have an optional dependency on the Rust standard library. The canonical way to do this correctly is:

# In Cargo.toml

[features]
default = ["std"]
std = []
// In lib.rs
#![cfg_attr(not(feature = "std"), no_std)]

#[cfg(feature = "std")]
fn some_function_which_requires_std() -> HashMap<u32, String> {
  ...
}

Something you may want to do is create a "facade" module which acts somewhat like a union over core and std so you don't have to ugly #[cfg(feature = "std")] switches all over the place when importing things. Check out serde's lib.rs for a real life example.

5 Likes

Thanks! The API guidelines where exactly what I intended to reference!

Not having to do the conditional compilation switching (cfg) on std/core and std/alloc seems attractive. Should I be worried that the facade module will remove some of the explicitness of dependencies, and might be prone to the forgetting to remove no longer needed dependencies situation?

Perhaps to do the pub use in self::core, for then to use self::core::result::Result; in the different modules would be a nice compromise?

Anyway. thanks for pointing out the "facade" solution to the cfg switching mess!

Well it's just providing a union over core and std, so you won't have issues where you accidentally forget to remove an extern crate ... for other third-party libraries. They are just included like normal dependencies and don't belong in the facade.

I don't think it'll be a problem in the long run anyway, use statements don't generate any code and I'm sure the normal unused_import lint will help with any stray imports you forget. An extra unused #[cfg(feature = "std")] pub use std::foo::bar; in your facade wouldn't really do any harm anyway.

1 Like