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

Add no_std support #226

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open

Conversation

vstroebel
Copy link
Contributor

@vstroebel vstroebel commented Feb 18, 2022

This is my attempt to make this crate usable on no_std.

I've splitted this changes into several commits to make it easier to review:

  1. Introduce trait JpegRead as an replacement for std::io::Read. On std JpegRead is implemented for every std::io:::Read which should allow users to use every type implementing Read, too.
  2. Some use relate changes that Move most uses of the std crate to core and alloc #196 didn't cover
  3. Integer only replacement for f32::ceil based code that isn't available in ´core´
  4. An ugly replacement for f32::fract. Maybe someone has a better idea to implement this for no_std.
  5. An integration test that works on no_std. The integration test suite heavily depends on file system ops and the png crate. Having at least one test that works on no_std is better than nothing....
  6. Add a std feature and use it as a marker for std/no_std.
  7. Change github action to test std and no_std code

This should fix #199

Copy link
Member

@HeroicKatora HeroicKatora left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opinion: This affects too much code, too visibly. Infecting and changing all public bounds is just one uncomfortable change that most users won't be positively affected by. The structure of this implementation forces us to do all-or-nothing, however I don't think this ready for merging. I'd like at least a significantly better test coverage of individual (and somewhat awkward) floating point workarounds, and that they not be present in the usual configuratio. Some more detailed concers with specific implementation choices are listed below.

Overall, I would much rather we have crate that addresses those concerns separately. One that can be shared across multiple of image-rs crates for no_std compat (png, tiff would be good starts) and provides Seek maybe as well. With focussed test coverage, orthogonality of design. There are plenty but none I'm aware of have been too convincing regarding the SemVer / compatibility problems briefly mentioned below.

Comment on lines +31 to +32
#[cfg(feature = "std")]
impl<T: Read> JpegRead for T {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work perfectly with features. If you include the crate with no-default-features, then this impl does not exist and consequently, anyone can legally define a type and impls like so:

struct SomeReader;
impl std::io::Read for SomeReader { … }
impl JpegRead for SomeReader { … }

However, then another unrelated dependency may include jpeg-decoder with feature = std and suddenly those two impls collide as the first implies this impl block, which conflicts with the second. This isn't necessarily a deal breaker, it's not unsound, but it's rather odd and unpredictable.

*self = remaining;
Ok(())
} else {
panic!();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't very complete yet, right? And that's probably why I'm a bit wary introducing a new trait just for jpeg-decoder and then have it replicate mostly internals from what's usually in std.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And now that it existed once, my desire for much more extensive test coverage of all the impls has grown quite a lot.

Comment on lines +50 to 56
#[cfg(feature = "std")]
/// An I/O error occurred while decoding the image.
Io(IoError),

#[cfg(feature = "std")]
/// An internal error occurred while decoding the image.
Internal(Box<dyn StdError + Send + Sync + 'static>), //TODO: not used, can be removed with the next version bump
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having extensively worked with no_std-but-std-compatible crates in networking, variants guarded behind cfg flags are a major pita. This seems like the biggest risk to reject the PR.

If the enum itself is not non_exhaustive then consumers intending to match on these variants will having to work quite awkwardly around the presence and absence of certain variants/branches. Especially since it depends on the feature of a dependency, which they may not want to publicly expose as a dependency/toggle themselves. This works just good enough if the program is small and jpeg-decoder is a direct dependency but doesn't scale well. (Again, consider the feature may be activated by an unrelated crate).

This forces downstream crates, if they intend to be compatible with both, to run check coverage of both with and without the feature combinations. And in the end you get this where it's a jungle of things that may or may not work with each other.

let size = size as u32 * idct_size as u32;
let dim = size / 8 + u32::from(size % 8 != 0);
dim as u16
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this change, you can probably PR this on its own. I like it better than the floating point code in any case as there aren't any subtle accuracy concerns.

@@ -113,6 +112,12 @@ pub struct IccChunk {
pub data: Vec<u8>,
}

fn calc_output_size(size: u16, idct_size: usize) -> u16 {
let size = size as u32 * idct_size as u32;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make sure the types are such that u32::from can be used in both cases, to statically ensure this can't overflow?

@@ -164,6 +164,13 @@ impl Upsample for UpsamplerH2V1 {
}
}

// no_std version to calculate fractional part of positive numbers
fn fract(v: f32) -> f32 {
debug_assert!(v >= 0.0 && v <= (1 << core::f32::MANTISSA_DIGITS - 1) as f32);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are doing this for positive numbers only, then why isn't it using f32::floor()? How does the conversion to usize work but not the fract() method? I'm just super confused. This needs better justification/eval of alternatives before affecting std code as well.

@@ -176,7 +183,7 @@ impl Upsample for UpsamplerH1V2 {
let row_near = row as f32 / 2.0;
// If row_near's fractional is 0.0 we want row_far to be the previous row and if it's 0.5 we
// want it to be the next row.
let row_far = (row_near + row_near.fract() * 3.0 - 0.25).min((input_height - 1) as f32);
let row_far = (row_near + fract(row_near) * 3.0 - 0.25).min((input_height - 1) as f32);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not just use integer fixed-point math here?

Comment on lines +14 to +15
#[cfg(feature = "std")]
mod std_tests {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't mind if the whole module were moved to a separate file here.

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

Successfully merging this pull request may close these issues.

Make this crate usable in a no_std environment
2 participants