Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Addressing ergonomics around Rust and embedded/no_std development #26

Closed
10 tasks
Susurrus opened this issue May 25, 2017 · 15 comments
Closed
10 tasks

Addressing ergonomics around Rust and embedded/no_std development #26

Susurrus opened this issue May 25, 2017 · 15 comments

Comments

@Susurrus
Copy link

Susurrus commented May 25, 2017

This issue is a place to track broadening the Rust embedded ecosystem by tracking various papercuts that will hit users, especially new ones (both to embedded and Rust), when they start to dive it. Rust has been good at attracting new users and many don't come with a systems background. The broad availability of SBCs and microcontrollers that Rust can target and can be used for cool hobbyist or IoT projects will likely pull in many new users to the embedded development side of Rust. We should coddle these users into full-fledged embedded devs and increase our numbers!

Anyways, on to the list:

  • The current convention for crates that have both a no_std version with less functionality and a std version is to have a feature and enable it by default. We should decide on a consistent name and get this written up as a convention somewhere for devs to reference (tracked here). Once that's finished we could also scan crates.io for crates that use other names and file tickets against them to encourage the ecosystem to standardize [NO TRACKING ISSUES]
  • Crates that are compiled (using std by default) do not have core available to them and have to explicitly import them via extern crate core. Additionally, when compiling as no_std, this then results in a compilation error. This combination increases the friction for a user trying to use just core instead of std where possible. I suggest that core be automatically included for all projects (except if #![no_core] is specified). [NO TRACKING ISSUES]
  • Users will default to using the std version of something because of how the docs are arranged. Knowing about core and caring about no_std use is almost always not a primary thought of the crate developer. Convincing them to support no_std is now more onerous as they have to go in and adjust all the use paths to core. This is especially problematic given the point above. It would be great if this could be ameliorated by #![no_std] automatically mapping those std paths to core. A partial solution could be injecting use core as std. [NO TRACKING ISSUES]
  • For no_std crates, including hybrid dependencies (mixed std/no_std) doesn't expose only the no_std functionality by default. As discussed in Point 1 here many imports then look like byteorder = { version = "1", default-features = false }. So for users coming from std, there's extra work even declaring crate dependencies. [NO TRACKING ISSUES]
  • Core is missing functionality that would be appropriate to expose in core (not involve allocations or os-specific functionality). Right now that's things like signum(), sqrt(), and the like. It's a big surprise to users that they can't use sqrt() without the standard library! Other proposed functionality is OsStr and PathBuf and some of the UTF-16 processing, though for the first two it's a little unclear how those might be implemented. There are likely others and an audit of std would likely be enlightening. [tracking issue 1, tracking issue 2]
  • Some crates don't expose no_std even if they could. ref_slice is one such example where the author wasn't aware til an issue was posted. There's likely some work here that can be done to alert crate authors, though the most appropriate time seems to be when publishing to crates.io or when using clippy [tracking issue]
  • There's no way to enforce saying that a crate should not be compiled with std. no_std just specifies that the crate code doesn't need it, but std could still be pulled in by a dependency. This combos with Point 4 to make it the default that a user could try to implement a no_std crate and ship it only to have downstream users complain that it's not no_std because he forgot to include a dependency properly. There should be some sort of compilation check for this (it's part of The Rust Way(TM)). Maybe a clippy lint here would be a good place to start. [NO TRACKING ISSUES]
  • There isn't much in the way of Best Practices for embedded with Rust. This org is working to change that along with @brson's rust-api-guidelines repo, but having resources explicitly talking about this would be helpful. I've proposed a cargo checklist command that could be helpful here for new crate authors (tracking issue).
  • Users aren't aware of useful crates for no_std. This is a combination of exposure and the niche of no_std developers. Increasing users' exposure to no_std crates so they a) know they're available and b) think about making their crates suport no_std could be a good way to start here. I've suggested a badge on crates.io for no_std-compatible crates] to compliment the no_std keyword and no_std categories that are already there. Everyone loves badges!
  • cargo test by default doesn't expose std for no_std crates. This makes it harder to debug and work with tests for users used to doing println!(). I believe std should still be exposed within the test code itself, but it might also be worth compiling the crate as std so that these debugging statements can be inserted. This should only apply to the crate itself and not upstream dependencies. [NO TRACKING ISSUES].
@japaric
Copy link
Member

japaric commented May 25, 2017

@Susurrus Thanks for the write up!

Some comments

  1. This is rust-lang/rfcs material. Sounds good to me, but it seems non
    backwards compatible: For example a no_std crate with a module named core
    in its root when compiled with the "std" feature enabled.

  2. Same comment as above.

  3. I wish cargo add accepted a --no-default-features flag. A magic solution
    here would to disable the "std" feature of all crates when the top crate
    contains #![no_std]. Again rust-lang/rfcs material and possibly non
    backward compatible.

  4. Some float operations, like abs, are available on core via the unstable
    Float trait. I think the only reason that the trait is unstable is because
    those methods should be inherent to float rather require a trait import.
    However, the current implementation doesn't support splitting inherent
    implementations on primitive types across crates (it would be easy to violate
    coherence if that was allowed). For the other float operations, like sqrt,
    is because they come from libc, at least when you use std, and core should
    not depend on libc. This can be solved by having a Rust implementation of
    libm in core (core can't have dependencies). Though that's a lot of work
    and nontrivial (you have to make sure your implementations have acceptable
    rounding errors). For OsStr, PathBuf, etc. I don't think they should be in
    core as they can live in other crates on top of core. IIRC, the reason that's
    not the case is because of coherence. TL;DR I agree no_std floats should have
    operations like sqrt and abs as inherent implementations; however, that's not
    easy to implement.

  5. 👍. Sounds hard though.

  6. I don't see how this can happen. If you try to publish a crate A that depends
    on crate B with default-features = true and that has a "std" feature then
    publication would fail because compilation would fail. Now if crate A depends
    on crate B with default-features = false and you are building a crate C that
    depends on both A and B but you forgot to set default-features = false for
    your dependency B then compilation of A would fail because Cargo features
    are additive and A's dependency B would also get compiled with the feature
    "std". However, this is not the fault of the author of crate A.

  7. FWIW, there's a category for this.

  1. If I'm reading this correctly cargo test should not only add implicitly
    inject a dependency on test but also dependency on std. I'm 👎 on this
    as this would impede testing via no_std testing frameworks like
    utest, which are useful if you want to run tests on no_std targets
    like microcontrollers

@jbowens
Copy link

jbowens commented Nov 30, 2017

These are on point. I've run up against many of the same places of friction.

@pftbest
Copy link

pftbest commented Dec 1, 2017

I have a question about #![no_std] attribute. Why do we need to have it in the source code? Why not just a compiler flag? I think there are many crates like ref_slice that doesn't need anything from std, and would have happily compiled for no_std target. But instead you need to ask each crate's author to modify their source code. This feels counterproductive. Also having std/core renaming makes matters even worse, not only do you need to add no_std attribute, but you also have to rename the core back to std. So why it was renamed in the first place?

Small example:
Three weeks ago I needed to parse a MessagePack format on a Cortex-M4. There is a great library on crates.io called rmp but, as you might have guessed, it doesn't have no_std feature. This is what I had to do to make it work on my MCU:

  1. Add modified num-traits crate which doesn't have floating points (rmp doesn't need them anyway)
  2. Provide my own traits like std::io::Read and std::error::Error
  3. Add std::io::Cursor struct
  4. Add part of byteorder crate that depends on Cursor

As you can see, I didn't have to do any real changes to rmp crate itself, the only thing that it needs is a Cursor and a bunch of io traits. On the other hand, rewriting rmp so it doesn't use Cursor and byteorder, would require a lot of effort and probably make the code much more complex and less maintainable.

If I had some easy way to provide a custom std based on core + Cursor, I could have just added rmp as a regular dependency in my Cargo.toml. And If you include alloc and collections in such hypothetical modular std, then I'm sure it will be possible to compile hundreds more crates from crates.io without any modifications.

@Kixunil
Copy link

Kixunil commented Dec 1, 2017

@pftbest instead of writing custom IO traits, please consider using genio crate. It has no_std support and addresses some other problems with std::io. Let me know if it's missing something you need.

@pftbest
Copy link

pftbest commented Dec 1, 2017

@Kixunil
My goal was to make modifications as small as possible, so it would be easy for me to update the crate in the future if I need to.

If I understand correctly, genio uses a different approach to error handling than std::io, so it would require some heavy modifications to both rmp and byteorder. Which I don't have the time to do at the moment, sorry.

I did consider using core_io crate, it even has its own byteorder fork, but I had a problem running tests, because they use Vec and Cursor from full std. I needed to implement my custom io::Write for std Vec. I do see that this will not be a problem in genio, which is nice.

@Kixunil
Copy link

Kixunil commented Jan 1, 2018

Thank you for feedback!

I'm considering to implement byteorder-like trait for genio. Regarding rmp, maybe we could convince the author to implement it on top of genio as genio wants to be compatible with std::io. (Anything that impls std::io should be able to impl genio.)

@pftbest
Copy link

pftbest commented Jan 1, 2018

@Kixunil

Maybe it would be better to add genio support in byteorder crate itself. If such popular crate would gain support for genio then it would be easier to convince rmp and other libraries to switch.

@Kixunil
Copy link

Kixunil commented Jan 1, 2018

@pftbest Since genio still needs some work, I doubt 1.0 crate will want to depend on 0.1 crate.

I looked at genio and realized it already has native implementation of byteorder-like methods, if you turn on byteorder feature.

@cbeck88
Copy link

cbeck88 commented Jan 17, 2019

@pftbest are you interested in posting your fork of rmp on crates.io? I need pretty much the exact same thing. I've been unable to find an existing rust library on crates.io that supports a "robust" serialization format (meaning contains the description of the data schema as well as the bytes) e.g. messagepack, CBOR, avro, and supports serde, and supports no_std.

There is corepack but it seems to be broken in our tests i.e. when we swap rmp for corepack in part of our system we start getting serialization failures, and it has some other open issues

@pftbest
Copy link

pftbest commented Jan 22, 2019

@cbeck88 Sorry, I've missed your message. I don't think I will post post my fork on crates.io, mainly because it is low quality hack. For example I've defined my own io::Read and io::Write traits, and implemented them only for arrayvec and arraystring, so if you want to use them with some other containers you'll have to modify the source code which is not possible with crates.io. Also there is a hard limit on string length of 512 bytes, because I don't use allocations, and have to create strings on the stack. You might want to change this number for your project.
Currently the code is based on old version of rmp, so it might contain bugs. I need to update it to latest version of rmp and maybe also replace io traits and parts of byteorder with genio but unfortunately I don't have time to do that at the moment.

If you still want to see the code you can download it here:
msgp_1.tar.gz

@cbeck88
Copy link

cbeck88 commented Jan 24, 2019

@pftbest Hey, I did some work to update your patch. I agree it's not the prettiest but it's really the best option right now at least for us. You can see our updated version of your patch here: 3Hren/msgpack-rust#187

I didn't fully understand what is needed for byteorder -- byteorder got an update not so long ago so it now has default-feature = false allowing it to be used in no_std, I believe it has std feature much like serde, and otherwise is core only. So it's possible that only the genio work is left?

We're interested in investing time and energy in this to make this as maintainable as possible. Thanks for making your patch available.

@pftbest
Copy link

pftbest commented Jan 24, 2019

@cbeck88, byteorder is needed because of how rmp works internally. It is using functions like read_u32 to get data from io::Read stream. byteorder provides this functions for std::io::Read when std feature is enabled. But in this case we have our own io::Read trait so I had to copy paste this code from byteorder. I saw that @Kixunil was working on a similar feature for genio but I don't know if it's complete.

P.S. I completely forgot that I had this code on github, I thought I only had a local copy. Thanks for finding it.

@Kixunil
Copy link

Kixunil commented Jun 21, 2019

Sorry for replying this late, yes, it does work, you just need to enable byteorder feature.

@jamesmunns
Copy link
Member

Closing this as part of the 2024 triage effort, as I believe quite a few of these have either been addressed, or are captured in the embedded rust book, or are encoded in other general best practices.

If anyone disagrees with this and would like it reopened, please let me know and we can discuss it in the next weekly meeting onMatrix.

@cbeck88
Copy link

cbeck88 commented Jun 24, 2024

thank you

for anyone who follows this issue in the future, in the project we were working on, we ended up using prost , which has moved from danburkert to the tokio-rs umbrella. https://github.com/tokio-rs/prost

at the time I made a PR that introduced no_std + alloc support: tokio-rs/prost#215
which was eventually merged here: tokio-rs/prost#319

I highly recommend that library, it's great

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants