-
Notifications
You must be signed in to change notification settings - Fork 45
Add no_std Support
#200
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
base: master
Are you sure you want to change the base?
Add no_std Support
#200
Conversation
|
Not sure why the test |
197g
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we split this up over two or three PRs?
There's probably a way to change the architecture to make this work right, more sans-io, but this looks on the surface like adding some confusion in trying to support two different underlying models of streams at the same time. I find it very hard to judge the correctness of what's being implemented through the wrappers and traits that are intermingled with pulling things out of the internals.
The best way to make this reviewable would be:
- first PR the changes to trait bounds (e.g.
ReadtoBufRead) and API surface ofstd, so that their justification can be done independently from no-std. This also makes it easier to judge ramifications of them in isolation. - then PR some cleanup of the error types as necessary.
- then PR the traits for no-std which now should appear as their separate API surface so we don't have to worry about changes to std behavior at this stage.
I've indicated some of the critical parts of the code in comments, they apply to one of these three bullet points respectively.
|
|
||
| #[cfg(feature = "color_quant")] | ||
| use std::collections::{HashMap, HashSet}; | ||
| use alloc::collections::{BTreeMap, BTreeSet}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's one of those little things that I meant with: "will there be a tradeoff between no-std and performance". Those are not equivalent, though of course it may not matter much. The use, building a palette, is one of the noticeably slowest paths of naively encoding.
Maybe this is actually good because performance sensitive users may already be falling back to supplying a palette and thus not executing any of the set operations. But maybe they do. It's very speculative.
| /// Returned if the encoder could not allocate enough memory to proceed. | ||
| OutOfMemory, | ||
| /// Returned when an IO error has occurred. | ||
| Io(Box<dyn error::Error + Send + Sync + 'static>), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I get an IO error I want to inspect at least the error code, i.e. if it is WouldBlock or ConnectionReset, StorageFull etc. Exactly this is being erase which the majority of std users do not benefit from. Another tradeoff between no-std and proper modelling.
| vec.try_reserve( | ||
| usize::from(self.current_frame.width) | ||
| * usize::from(self.current_frame.height) | ||
| / 4, | ||
| ) | ||
| .map_err(|_| DecodingError::InsufficientBuffer)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part I actually like, io::ErrorKind is arguably incorrect. If you want to rework the interface bit-by-bit into something amenable to no-std, it would be a good start to ensure all the internal errors are no longer dependent on io::Error/io::ErrorKind in any way. This could also clean up some stringly typed errors.
| impl<R: io::Read> Decoder<IoBufReader<io::BufReader<R>>> { | ||
| /// Create a new decoder with default options. | ||
| #[inline] | ||
| pub fn new(reader: R) -> Result<Self, DecodingError> { | ||
| DecodeOptions::new().read_info(reader) | ||
| } | ||
| } | ||
|
|
||
| impl<R: BufRead> Decoder<R> { | ||
| /// Create a new decoder with default options and the provided [`BufRead`]. | ||
| #[inline] | ||
| pub fn new_with_buffered_reader(reader: R) -> Result<Self, DecodingError> { | ||
| DecodeOptions::new().read_info_with_buffered_reader(reader) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good change regardless. We may have been double-buffering all the time and the interface for constructing the reader isn't actually that different. The only change change for existing downstream users would be that they have to denote the type as Decoder<BufReader<R>> instead of Decoder<R> if they actually annotated the type exactly.
If you intend to publish this as a crate in some capacity, compare to |
|
Marking as draft while I split out smaller PRs based on this one. Once the splitting is complete I'll close this PR. |
Objective
Before committing to
no_stdsupport inimage, there are open questions around how to sever dependence onstd::iowithout compromising current performance or compatibility. Sinceimage-gifcurrent does not haveno_stdsupport, and is comparatively small, solving that problem here could indicate how the problem could be handled more broadly.Solution Overview
Similar to
ciborium-io, I have re-defined a subset of thestd::iotraits for use withinimage-gif. However, unlikeciborium-io, I do not blanket-implement the trait for all types implementing thestd::iooriginals. Instead, each redefined trait gets a transparent wrapper type which forwards theimage-gifmethods to the underlyingstd::ioones.Since the wrapper is only available with the feature that adds the implementation, features remain additive. For
image-gif, I have not provided any baked-in implementations for types likeVec<u8>, opting instead to leave it up to end users to write their own wrappers aroundcore/alloctypes if they need to. This reduces the maintenance burden onimage-gifitself, since it only has 1 implementation to test, the one that simply forwards tostd::iowhich it was already testing.Solution
core::error::Error. This simplifies feature gates considerably.color_quantfeature to useBTreeMapandBTreeSetinstead ofHashMapandHashSet. This is purely an internal change and should not have any observable side effects.BufReadandWritetraits totraits.rswhich contain the bare-minimum functionality required for the rest of the crate. These are public so crate consumers may define their own readers/writers.WriteBytesExtto be a super-trait ofWrite. I also changed the implementations to useu16::to_le_bytes(likewise foru32andu64). This solves what I believe is a possible bug in the implementation where one of those values could be partially written before an error is returned, which is inconsistent with the otherwise total use ofwrite_all.stdfeature] AddedIoBufReaderandIoWritewrapper types and implementedBufReadandWritefor them.EncodingFormatError::BufferImproperlySizedvariant. This reduces reliance onstd::io::Errorand is not a breaking change, asEncodingFormatErroris alreadynon_exhaustiveEncodingError::MissingWriterandEncodingError::OutOfMemoryfor the same reasons above. This is a breaking change as it was notnon_exhaustive. I have now marked itnon_exhaustiveto prevent this issue in the future.EncodingError::Ioto wrap aBox<dyn Error>instead of anio::Error. This just allows any error type to be used for IO errors, but is a breaking change.Encoder::new_with_writerwhich constructs anEncoder<W>whereW: Write. This is contrast to the existingEncoder::new, which now returns anEncoder<IoWriter<W>>whereW: std::io::Write.DecodingError::InsufficientBuffer,DecodingError::UnexpectedEof,DecodingError::MissingDecodervariants for the same reasons as above.DecodingError::Ioto wrap aBox<dyn Error>for the same reasons as above.OutputBuffer::Veccode-path forLzwReader::decode_bytes. This wasn't required, but it appeared simple enough.DecodeOptions::read_info_with_buffered_readerwhich constructs aDecoder<R>whereR: BufRead. This is contrast to the existingDecodeOptions::read_info, which now returns anDecoder<IoBufReader<std::io::BufReader<R>>>whereR: std::io::Read.Decoder::new_with_buffered_readerwhich constructs aDecoder<R>whereR: BufRead. This is contrast to the existingDecoder::new, which now returns anDecoder<IoBufReader<std::io::BufReader<R>>>whereR: std::io::Read.cargo fmt(this is the reason the diff is so large, the actualno_stdchanges are closer to +400/-200). Please refer to the individual commits to see the effect of formatting on its own.