Using std::io::{Read, Write, Cursor} in a nostd environment
I'm surprised there's no ticket for this, so here we go.
It isn't possible in nostd to use std::io::Cursor. That seems to be mostly because it requires std::io::Read, Write, and Seek. However, I'd argue that those traits should also be part of libcore, and only the specific implementations be in libstd. After all, the Read/Write/Seek traits and the Cursor struct don't seem to rely on anything that's part of the stdlib: they don't need allocation, etc...
My use-case is, I have to write data into an array, and treating that array in a file would lead to much more idiomatic code. Furthermore it'd allow using crates like byteorder in nostd environment.
The problem with this, which has been covered in previous issues, is that
ReadandWritehavestd::io::Errorhardcoded in their method signatures which requires quite a bit ofstdmachinery including the ability to allocate.Peter Atashian at 2018-02-18 21:27:57
Do you have a link to previous issues ? I tried looking for it, but nothing came up :(.
Robin Lambertz at 2018-02-18 21:32:20
I... can't find the previous issues. Maybe it was just scattered comments over the place and IRC discussions...
Peter Atashian at 2018-02-18 21:41:30
These traits being absent is the reason rust-core_io exists. Relibc would thank you if
core::iobecame a thing, and I could then continue on my newly started quest to make more crates supportno_std.jD91mZM2 at 2019-04-21 14:15:51
Anything here requires the
std::error::Errortrait, which requiresstd::backtrace::Backtrace, but maybe all that only requires core+alloc. Assuming so..We should explore replacing
std::io::Errorwith some more flexible error type, vaguely inspired by theSmallErrortype suggested in https://github.com/rust-lang/rfcs/pull/2820#issuecomment-567439210 that encodes theio::error::Reprenum with some vtable pointer.In essence, it should act like
Box<dyn Error+Send+Sync+'static>except small error types get passed by value.We could maybe preserve
mem::size_of::<SmallError>()being only twicemem::size_of::<usize>()too.Also, I'm unsure if
std::error::Errorcould feature gate thestd::backtrace::Backtracemethods onalloc, but maybe. We must encodeDropinside theerrorr::Errortrait though, which I suspect either requires magic similar toBox, or else makesDropa supertrait forerrorr::Error. We'd wind up with anErrortrait likepub trait Error: Drop + Debug + Display { fn description(&self) -> &str { "description() is deprecated; use Display" } fn cause(&self) -> Option<&dyn Error> { self.source() } fn source(&self) -> Option<&(dyn Error + 'static)> { None } fn type_id(&self, _: private::Internal) -> TypeId where Self: 'static { TypeId::of::<Self>() } #[cfg(feature = "alloc")] fn backtrace(&self) -> Option<&Backtrace> { None } }We must then use wrapper types that handle drop correctly.
#[derive(Clone)] pub struct BoxError { inner: Box<dyn Error+Send+Sync+'static>, } // delegate Deref+DerefMut+Drop to inner impl Drop for BoxError { fn drop(&mut self) { self.deref_mut().drop() } } use core::mem; use core::raw::TraitObject; const SMALL_ERROR_SIZE : usize = mem::size_of::<usize>(); #[derive(Clone)] pub struct SmallError { vtable: *mut (), data: [u8; SMALL_ERROR_SIZE], } impl Drop for SmallError { fn drop(&mut self) { self.deref_mut().drop() } } impl Send for SmallError {} impl Sync for SmallError {} impl SmallError { pub fn new<E: Error+Clone+Send+Sync>(err: E) -> SmallError // where mem::size_of::<E>() <= SMALL_ERROR_SIZE // sigh { assert!(mem::size_of::<E>() <= SMALL_ERROR_SIZE); let data = [0u8; SMALL_ERROR_SIZE], core::intrinsics::copy(&err as *const E, &mut data as *mut [u8; SMALL_ERROR_SIZE] as *mut E , 1); let vtable = unsafe { mem::transmute::<&mut dyn Error,TraitObject>(&mut dyn err).vtable }; // mem::forget(err); SmallError { vtable, data } } } impl Deref for SmallError { type Target = dyn Error+Send+Sync; // Should we add +Clone when mutli-trait objects exist fn deref(&self) -> &Self::Target { let SmallError { vtable, ref data } = self; mem::transmute::<TraitObject,&mut dyn Error>(TraitObject { vtable, data }) } } impl DerefMut for SmallError { fn deref(&mut self) -> &mut Self::Target { let SmallError { vtable, ref data } = self; mem::transmute::<TraitObject,&mut dyn Error>(TraitObject { vtable, data }) } }We then need some outer type that calls
DropforBoxErrorandSmallError, but forwardsErrormethods, likepub struct DerefError(&dyn Deref<Target = dyn Error+Send+Sync+'static>+DerefMut+Drop) impl Drop for DerefError { fn drop(&mut self) { self.drop() } } impl Error for DerefError { // delegate all methods to self.deref() }Jeff Burdges at 2020-01-03 02:17:29
Backtrace cannot really exist outside std. It is highly dependent on OS features. For instance, to get function names (symbolication step of printing a backtrace), it requires opening the executable as a file and parsing it to access the debug information table, e.g. it requires doing open(argv[0]). Or it might require dlresolve or other kind of libc stuff.
In an ideal world, binaries compiled in no_std could just have an "empty" implementation of Backtraces. I don't think that's possible however, since libstd simply reexports everything in libcore verbatim. It's not possible for libstd to use a different implementation of things than libcore AFAICT.
Unfortunately, moving backtraces to be an exposed interface of std::error::Error seems to permanently kill any possibility of std::error and std::io being available in libcore.
Robin Lambertz at 2020-01-03 15:44:38
Could we replace
&Backtracewith some opaquepub struct BacktraceRef(usize);type that exists libcore? We'd then provide inherent methods for that type only in std, assuming that flies with whatever magic.Jeff Burdges at 2020-01-03 17:14:14
Std cannot (currently) add inherent impls for structs it does not itself define. It plays by the same rule as the rest of the ecosystem when it comes to coherence, there's no magic going on here (modulo some escape hatches for fundamental types that don't apply here). Libstd can implement libstd traits on libcore structs, or implement libcore traits for libstd structs. But it can't add its own inherent impls for (libcore) structs, only the crate defining a type can do that.
The best I can figure out is changing the backtrace function to return a
&'a dyn BacktraceTraitand do away entirely with the Backtrace structs (they'd be an implementation detail). This would prevent adding new functionality to backtraces (since they're now a trait that anyone can impl) but it means libcore's Error could return an empty backtrace impl.Except that's also undesirable because now all the errors living in libcore won't get to use backtraces - only errors in libstd would use the "right" backtrace implementation.
Maybe that'd be fixable using the same mechanism global allocators use? Have some kind of backtrace factory, have libstd give out a default implementation, and ask libcore users to provide their own (probably stubbed) implementation.
Robin Lambertz at 2020-01-03 19:58:29
I suppose std cannot use features for dependencies like many crates do if we want std to ever actually be the standard library on some future operating system. In other words, if features are additive then std should always have all its features enabled.
I think
&dyn BacktraceTraitsounds fine becausestd::backtrace::Backtraceindicates it'd resembletrait BacktraceTrait : Debug + Display { fn status(&self) -> BacktraceStatus; }I still think
BacktraceRefcould be defined in core, but you defineBacktraceTraitin std. Also we've several proposals for extensions like (a) doing inherent methods across distinguished crate boundaries, or (b) allowing similar opaque extern types like you gets from C header files, but your&dyn BacktraceTraitworks well enough here I think.As for implementation, there are several options here:
- We could treat
Errorspecial with the trait object tricks I wrote above. - We could implement those same tricks via some proc macro, and some
SmallBoxDyntype, so that other trait objects could benefit form a smallCopytypes being passed, and do not require even alloc, while larger types get boxed, requiring alloc. - We could do some
SmallBoxtype that handled the forwarding via compiler magic for the forwarding vtables, but also worked without always passing through trait objects.
Jeff Burdges at 2020-01-04 14:27:55
- We could treat
I think
anyhow::Erroralready provides no_std wrapper forstd::io::Error. At present, it requires alloc but it looks like a reasonable starting point if we want to make this dependency optional.We'd need to remove the
Boxin https://docs.rs/anyhow/1.0.26/src/anyhow/lib.rs.html#322-324 which increases the size from one to twousizes.It appears
anyhow::Erroralready manually implements its trait object internally, which I considered overkill but if they needed it then maybe my suggestions here also do.We should reimplement the
std::error::Errortrait so that itsnewmethod does not depend uponstd.I think
anyhow::Erroraddresses theBacktracemachinery by depending upon the backtrace crate, so maybe that already works no_std but requires alloc. We need aBacktraceTraitthat avoids even needing alloc, as discussed above.We should also remove the backtrace that
anyhow::Errorattaches https://docs.rs/anyhow/1.0.25/src/anyhow/error.rs.html#672 because it requires alloc. We could however permit layering one internally when using alloc via another internal wrapper type.After all this we could then reimplement all traits from
std::ioand their async cousins. Appears doable although not sure until you try.Jeff Burdges at 2020-01-21 23:55:02
I believe that the error-handling working group is making it a priority to move
std::error::Errorinto libcore, so with luck this has a path forward.Std cannot (currently) add inherent impls for structs it does not itself define.
This isn't quite true; with lang items, anything is possible. This is how, for example, both std and core can define inherent impls on floats: https://github.com/rust-lang/rust/blob/1705a7d64b833d1c4b69958b0627bd054e6d764b/library/std/src/f32.rs#L31
bstrie at 2021-03-19 02:13:34
@roblabla Hey, I am working in no_std env and want to send sensors reading as a post request. For that I need readings, is that anyway I can write data to a file in no_std env. Using the discovery book I am able to get reading in itm.txt using itmdump
Nitin Saxena at 2021-08-12 11:03:12
For that I need readings, is that anyway I can write data to a file in no_std env
no_stdis portable to systems without files, so no. However, there's probably something on crates.io that you can use. For example, thelibcandwinapicrates expose access to common OS apis, and there are crates built on top of them.(EDIT: to be clear, I'm in favor of finding a way for core to access these traits (and any types like Cursor which can be implemented there...) but that won't help this user write data to a file)
Thom Chiovoloni at 2021-08-12 23:06:08
Found nice fresh slice of the std::io to use as a workaround in no_std environments: https://github.com/dataphract/acid_io
Slava Dobromyslov at 2022-01-19 06:02:09
https://github.com/rust-lang/rust/pull/99917 has been merged so is anyone going to create a PR?
Caio at 2022-08-23 23:36:55
I think it needs a lot more discussion. It returns
std::io::Error, which cannot trivially be moved into core, even now thatstd::error::Errorcan.See https://github.com/rust-lang/project-error-handling/issues/11 on some of the challenges there, and discussion if doing so is even desirable. My personal stance is that it should be closer to something like
core::io::Write<Error = ...>, as I mentioned in https://github.com/rust-lang/project-error-handling/issues/11#issuecomment-1089352124. That would allow keeping useful information in std::io::Error, without regressing performance, etc.Thom Chiovoloni at 2022-08-24 01:23:44
Thank you for the explanation @thomcc.
Here goes a copy-and-paste of
library/std/src/io/error.rswith the newerror_in_corefeature -> https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=e30835b743ae49d77feb0c1f7c89d934As seen above, the only thing preventing the inclusion of
std::iostructures and traits into thealloccrate (notcorebut that would still enable#[no_std]) is theErrorData::Os(i32)variant and itsstd::sys::*calls.Not sure if the the new provider API can help here but if not, then
core::io::Write<Error = ...>seems like the only alternative.Caio at 2022-08-24 11:50:14
I think it needs a lot more discussion. It returns
std::io::Error, which cannot trivially be moved into core, even now thatstd::error::Errorcan.See rust-lang/project-error-handling#11 on some of the challenges there, and discussion if doing so is even desirable. My personal stance is that it should be closer to something like
core::io::Write<Error = ...>, as I mentioned in rust-lang/project-error-handling#11 (comment). That would allow keeping useful information in std::io::Error, without regressing performance, etc.I feel like, there could be already an
allocfeature, then we can go further and have thecore::io::Write<_>thing?Ryan Lahfa at 2023-04-30 13:51:49
Now that
core::error::Errorlives oncore... is there anything blocking this?Altair Bueno at 2025-01-11 17:53:02
Changing the error type would be a break. Moving the existing error type likely still has problems, which are solvable but which aren't actually solved yet.
Lokathor at 2025-01-11 17:56:34
Yeah, the issue is the io::Error type, not the error::Error trait.
Currently, the former requires alloc/Box, making it difficult to move into core.
Meriel Luna Mittelbach at 2025-01-11 18:01:16
Just out of curiosity, everything will work fine if they're moved into
alloccrate, right?Charles Lew at 2025-01-11 18:05:03
The fact that
io::{Write, Read}do not live incore, makes it impossible to write general purpose io-touching code that would work for bothstdandno_stdapplications, and is a huge structural rift in the ecosystem in this regard.I'm surprised that there are not that many people complaining about it.
In 2025, 7 years after this issue was opened, what do people to workaround it? What's the go to solution?
Are there any sights on addressing this issue? Any conclusions on what approach would be ideal? Could we maybe get new
core::io::{Write, Read}traits that e.g. would be generic over the error type so one could write their nostd/std code around that, and then havestd::iobe special case of that (possibly even with some hacker/special casing to make it work)? Or would that not work / what else could? I'm trying to collect all the apspects and status of the problem as it is causing great suffering to projects I'm involved in.dpc at 2025-02-28 23:24:21
Are there any sights on addressing this issue? Any conclusions on what approach would be ideal?
I did create https://github.com/rust-lang/rust/pull/116685 as a demo of how
std::io::Errorcould be moved tocore, which would allow moving all the core I/O traits tocore. If that's the approach we want to take, feel free to create your own PR based on that PR.Jacob Lifshay at 2025-03-01 01:53:09
Read::read_to_endtakes aVecas an argument so it'll requireallocat a minimumJonathan Behrens at 2025-03-03 22:21:43
Another reason to just have separate io traits in
core.What
no_stdcode needs is an ability to express generic and composable read and write primitives, generic over the error, which would avoiding touching allocation. I don't thinkstd::iois suitable for that, because it's a OS-level IO primitive.dpc at 2025-03-04 01:03:09
I haven't seen
ciboriummentioned yet, which has its own I/O abstraction layer forno_std/no_allococcasions. https://github.com/enarx/ciborium/tree/main/ciborium-ioEllen Emilia Anna Zscheile at 2025-03-04 19:10:52
I'm surprised that there are not that many people complaining about it.
I might have missed this issue when I looked for it in the past, not sure. Part of the problem is that I needed a solution now (and about three years ago), but I would be very grateful to see some progress on
core::io.In 2025, 7 years after this issue was opened, what do people to workaround it? What's the go to solution?
So I am actually dealing with interesting environments that either need a custom std or that need to use
no_std, but have enough to implement most of what is in the Rust standard library.One such environment is Intel SGX, where you don't have actual I/O inside enclaves, but can implement OCALLs to interface the operating system indirectly through the untrusted userspace application loading your enclave. Thus, you can expose enough of the libc API to the enclave to implement most of the Rust standard library. From what I know Fortanix went the route of having a custom target with its own implementation of the Rust standard library (thus avoiding the need for
no_std). However, if you want to build enclaves directly with the Intel SGX SDK, you currently have to do this withno_std. I have some small examples of how to do this without Rust nightly, and I think Teaclave went this route as well but might still depend on Rust nightly.Another such environment is DynamoRIO (and Intel Pin), which is an instrumentation framework. This means that you can write DynamoRIO clients which can trace programs as they execute and even manipulate the code. However, as the program should not be aware of the existence of such DynamoRIO clients, the clients have to adhere to certain restrictions, including that they cannot use libc directly (thus, by extension the Rust standard library). Also, because using libc directly can alter the program's behavior and memory layout. However, as you often need libc in DynamoRIO clients, DynamoRIO provides its own API that allows you to access the file system, create threads, etc. without possibly altering the program's behavior.
I have successfully implemented DynamoRIO bindings for Rust using
no_stdand I have abstracted away DynamoRIO's API in adrstdcrate. For the I/O-part, I originally used acid_io. Unfortunately, that crate seems to pull in Rust's standard library (probably due to lack of maintenance) which makes it unusable for dynamorio-rs, so I recently switched to no_std_io instead.I have also looked at embedded-io and ciborium-io, but they are just less complete compared to the no_std_io crate. This might be fine for embedded I/O, but it doesn't make as much sense if you can actually implement most of Rust's standard library and just want to offer a batteries-included experience. This is also why I like to create awareness of such environments where you can actually implement a standard library (albeit with some caveats), but might need
no_stdbecause the original standard library cannot be used (or a custom target with its own implementation of the standard library), and it wouldn't surprise me if more such environments exists (e.g., it might be possible to implement some part of Rust's standard library for UEFI applications, but there might be some caveats too). As many people, understandably, tend to think of actualno_stdenvironments like embedded hardware instead. Just adding my few cents FWIW.S.J.R. van Schaik at 2025-03-07 16:30:55
I haven't seen ciborium mentioned yet, which has its own I/O abstraction layer for no_std/no_alloc occasions. enarx/ciborium@main/ciborium-io
Relevant xkcd

We do not want to use an alternative. We want the standard implementation to be actually standard.
Altair Bueno at 2025-03-15 11:26:12