Conditionally use different types based on no_std

I'm pretty sure the answer is "declarative macro", but I haven't been able to fashion one.

I am creating a no_std crate with some opt-in std features. And, in that crate I have been using https://crates.io/crates/heapless datastructures (mostly Vec, but also Queue). Love it.

However, I'd like to be able to use std::vec::Vec when in the standard environment. I know the apis aren't identical, but I have been careful to only use methods with identical signatures. None of these types are exposed to the public and they are all type aliased. I have manually tested that everything works with either variant.

So, can I construct a macro that is "environment aware"?

Some code of the kind of think I'd like to do. The if_is_std!() macro uses the first for a std env and the second for no_std.

if_is_std!(
    use std::vec::Vec,
    use heapless::Vec
);

type Stream = if_is_std!(Vec<u8>, Vec<u8, 200>); 
// The heapless Vec requires a constant generic parameter

let mut stream = Stream::new();
stream.push(1).unwrap();
stream.push(2).unwrap();
#[cfg(feature = "std")]
use std::vec::Vec;

#[cfg(not(feature = "std"))]
use heapless::Vec;

Just make sure this changing Vec isn't visible in your public API, because Cargo features are meant to be additive, never a breaking change to enable one.

None of these types are exposed to the public and they are all type aliased.

Type aliases are exposed -- think of them just as nicknames. You need an actual wrapper type to really hide these details.

1 Like

no_std isn't a feature of the environment; it is a modifier you can apply to your crate, removing the default dependency of your crate on the std crate. A crate declaring no_std still works on a platform that has std.

In order to do something different depending on the platform, the convention is to declare a feature named "std" in your Cargo.toml, which users can then enable if they want (or disable if it's a default feature). You then use cfg(feature = "std") to control what code is compiled when that feature is enabled, as @cuviper showed.

Note that strictly speaking, vec isn't part of std; it's part of alloc. Some platforms have alloc but not std, and you may wish to offer corresponding features.

2 Likes

Well, that was simpler than I thought. Thanks!

@kpreid thanks for the explanation. I do have an optional std feature setup in my crate, but its hard to get away from thinking about "environment".

I hadn't realized that it's part of alloc. That would mean that standard vectors could be use on bare metal if a global allocator is setup? So, I should probably do the vec switching based on an additional "alloc" feature, shouldn't I?

Interesting. I guess what I meant is that they don't use the keyword pub.

In any case, these values are never returned by the public API. And my hope was that no one could do Stream::new() outside my crate.

Is that what you mean? Just don't return values outside the crate?

Oh, if your type alias is not pub, that's fine.

Scratch that, no it's not fine. This compiles even with a "private" alias:

pub fn foo() -> Stream {
    todo!()
}

type Stream = Vec<u8>;

Whereas this will error:

pub fn foo() -> Stream {
    todo!()
}

struct Stream(Vec<u8>);
error[E0446]: private type `Stream` in public interface

Good to know. I'll wrap my types. Thanks.

The type alias being public does determine whether it shows up in the documentation of functions/types that use it — if the alias is private, then the type it's an alias for shows up, but if the alias is public, then the alias's name does.

1 Like

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.