diff --git a/Cargo.toml b/Cargo.toml index 51062db..53ec84c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,12 @@ categories = ["embedded", "encoding", "graphics", "no-std"] documentation = "https://docs.rs/stockbook" exclude = [".github", "docs"] +[features] +progmem = ["avr-progmem"] + [dependencies] +avr-progmem = { version = ">=0.2.0, <0.4.0", optional = true } +cfg-if = "1.0.0" stockbook-stamp-macro = { version = "=0.2.0", path = "macro" } [workspace] diff --git a/README.md b/README.md index 510b28d..9afcfd3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Stockbook embeds 1-bit raster images in your code at compile time. -Designed primarily for `#![no_std]` usage, in embedded or other program-memory-constrained environments. +Designed primarily for `#![no_std]` usage, in embedded or other program-memory-constrained environments. Compatible with [`avr-progmem`](https://crates.io/crates/avr_progmem). ```toml [dependencies] @@ -46,6 +46,10 @@ fn draw_pixel_at(x: usize, y: usize) { Stockbook uses the [image](https://docs.rs/image) crate under the hood. See its own [list of supported formats](https://docs.rs/image/latest/image/codecs/index.html#supported-formats) for more details. +## Feature flags + +- **`progmem`** — wraps all pixel data of `Stamp`s in [`avr_progmem::wrapper::ProgMem`](https://docs.rs/avr-progmem/latest/avr_progmem/wrapper/struct.ProgMem.html)s. Combined with the `avr` target, this allows you to keep most of the data in program memory without the need to copy it to RAM. A no-op for non-`avr` targets. + ## Unstable features Although this library works on `stable`, any changes to images referenced by the `stamp!` macro might not be detected because of caching. Therefore, until [`track_path` API](https://doc.rust-lang.org/stable/proc_macro/tracked_path/fn.path.html) ([Tracking Issue](https://github.com/rust-lang/rust/issues/99515)) stabilizes, it is recommended to use the `nightly` toolchain, however functionality behind this feature is unstable and may change or stop compiling at any time. diff --git a/macro/src/lib.rs b/macro/src/lib.rs index e7a6a3e..162d64b 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -22,6 +22,10 @@ use syn::{ /// This macro will encode the image and yield an expression of type /// [`Stamp`][Stamp] with the pixel data included. /// +/// If the `"progmem"` feature is enabled and the target is set to `avr`, the pixel +/// data will be placed into the `.progmem.data` section using the +/// `#[link_section = ".progmem.data"]` attribute. +/// /// # Examples /// /// Assume there are two files in the same directory: a 16x12 pixel image @@ -168,33 +172,37 @@ impl ToTokens for Stamp { fn to_tokens(&self, tokens: &mut TokenStream2) { let width = self.width; let height = self.height; - let array = syn::ExprReference { + let array_len = self.data.len(); + let array = syn::ExprArray { attrs: Default::default(), - and_token: Default::default(), - raw: Default::default(), - mutability: Default::default(), - expr: Box::new(syn::Expr::Array(syn::ExprArray { - attrs: Default::default(), - bracket_token: Default::default(), - elems: self - .data - .iter() - .map(|byte| { - syn::Expr::Lit(syn::ExprLit { - attrs: Default::default(), - lit: syn::Lit::Int(syn::LitInt::new( - &byte.to_string(), - Span::call_site(), - )), - }) + bracket_token: Default::default(), + elems: self + .data + .iter() + .map(|byte| { + syn::Expr::Lit(syn::ExprLit { + attrs: Default::default(), + lit: syn::Lit::Int(syn::LitInt::new(&byte.to_string(), Span::call_site())), }) - .collect(), - })), + }) + .collect(), + }; + + // If the `"progmem"` feature is enabled, the array will be wrapped in the + // `ProgMem` struct, thus it is impossible for safe Rust to directly access the + // progmem data + let progmem_attr = quote! { + #[cfg_attr(all(target_arch = "avr", feature = "progmem"), link_section = ".progmem.data")] }; tokens.extend(quote! { - unsafe { - ::stockbook::Stamp::from_raw_unchecked(#width, #height, #array) + { + #progmem_attr + static VALUE: [u8; #array_len] = #array; + + unsafe { + ::stockbook::Stamp::from_raw(#width, #height, VALUE.as_ptr()) + } } }); } diff --git a/src/data.rs b/src/data.rs new file mode 100644 index 0000000..e21d51e --- /dev/null +++ b/src/data.rs @@ -0,0 +1,113 @@ +#[cfg(feature = "progmem")] +use avr_progmem::{raw::read_byte, wrapper::ProgMem}; +use cfg_if::cfg_if; +use core::fmt::Debug; + +/// Byte array wrapper — source of data for a [`Stamp`](crate::Stamp). +pub struct Data { + #[cfg(not(feature = "progmem"))] + source: *const u8, + + #[cfg(feature = "progmem")] + source: ProgMem, +} + +impl Data { + /// Constructs a new instance of this type. + /// + /// # Safety + /// + /// `ptr` must point to a byte array. + /// + /// If the `"progmem"` feature is enabled, `ptr` must point to a valid byte array + /// that is stored in the program memory domain. The array must be initialized, + /// readable, and immutable (i.e. it must not be changed). Also the pointer must be + /// valid for the `'static` lifetime. + pub const unsafe fn from_raw(ptr: *const u8) -> Self { + cfg_if! { + if #[cfg(feature = "progmem")] { + Self { + source: ProgMem::new(ptr), + } + } else { + Self { + source: ptr, + } + } + } + } + + /// Returns a byte at `idx`, without doing bounds checking. + /// + /// # Safety + /// + /// Calling this method with an out-of-bounds index is undefined behavior, even if + /// the resulting reference is not used. + pub unsafe fn get_unchecked(&self, idx: usize) -> u8 { + let ptr = self.as_ptr().add(idx); + Self::deref(ptr) + } + + /// Return the raw pointer to the inner value. + /// + /// If the `"progmem"` feature is enabled, the returned pointer must not be + /// dereferenced via the default Rust operations. + pub fn as_ptr(&self) -> *const u8 { + cfg_if! { + if #[cfg(feature = "progmem")] { + self.source.as_ptr() + } else { + self.source + } + } + } + + unsafe fn deref(ptr: *const u8) -> u8 { + cfg_if! { + if #[cfg(feature = "progmem")] { + // Since we're building with the `"progmem"` feature, `ptr` is valid in the program + // domain. + read_byte(ptr) + } else { + *ptr + } + } + } +} + +impl Debug for Data { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:p}", self.as_ptr()) + } +} + +impl Clone for Data { + fn clone(&self) -> Self { + cfg_if! { + if #[cfg(feature = "progmem")] { + // SAFETY: we construct a `ProgMem` with a pointer we got from a `ProgMem`. + // Required becase `ProgMem` doesn't provide a `Clone` implementation. + let source = unsafe { ProgMem::new(self.source.as_ptr()) }; + Self { + source, + } + } else { + Self { + source: self.source, + } + } + } + } +} + +unsafe impl Send for Data { + // SAFETY: pointers per-se are sound to send and share. Furthermore, we never mutate + // the underling value, thus `Data` can be considered as some sort of a sharable + // `'static` "reference". Thus it can be shared and transferred between threads. +} + +unsafe impl Sync for Data { + // SAFETY: pointers per-se are sound to send and share. Furthermore, we never mutate + // the underling value, thus `Data` can be considered as some sort of a sharable + // `'static` "reference". Thus it can be shared and transferred between threads. +} diff --git a/src/iter/pixels.rs b/src/iter/pixels.rs index 85a3853..e391fcc 100644 --- a/src/iter/pixels.rs +++ b/src/iter/pixels.rs @@ -5,7 +5,7 @@ use core::iter::FusedIterator; /// /// This type is created by the [`pixels`](Stamp::pixels) method on [`Stamp`]. See /// its documentation for more details. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Pixels<'a> { cursor: Cursor<'a>, cursor_back: CursorBack<'a>, @@ -23,7 +23,7 @@ impl<'a> Pixels<'a> { } /// An iterator that cycles throygh all pixels of a [`Stamp`] from front to back. -#[derive(Debug)] +#[derive(Debug, Clone)] struct Cursor<'a> { x: usize, y: usize, @@ -57,7 +57,7 @@ impl Iterator for Cursor<'_> { } /// An iterator that cycles throygh all pixels of a [`Stamp`] from back to front. -#[derive(Debug)] +#[derive(Debug, Clone)] struct CursorBack<'a> { x: usize, y: usize, @@ -126,7 +126,7 @@ mod tests { #[test] fn test_zero_size_stamp() { - let stamp = Stamp::from_raw(0, 0, &[]); + let stamp = unsafe { Stamp::from_raw(0, 0, [].as_ptr()) }; let mut pixels = stamp.pixels(); assert_eq!(pixels.next(), None); @@ -134,7 +134,7 @@ mod tests { #[test] fn test_zero_width_stamp() { - let stamp = Stamp::from_raw(0, 3, &[]); + let stamp = unsafe { Stamp::from_raw(0, 3, [].as_ptr()) }; let mut pixels = stamp.pixels(); assert_eq!(pixels.next(), None); @@ -142,7 +142,7 @@ mod tests { #[test] fn test_zero_height_stamp() { - let stamp = Stamp::from_raw(3, 0, &[]); + let stamp = unsafe { Stamp::from_raw(3, 0, [].as_ptr()) }; let mut pixels = stamp.pixels(); assert_eq!(pixels.next(), None); @@ -150,7 +150,7 @@ mod tests { #[test] fn test_double_ended() { - let stamp = Stamp::from_raw(2, 2, &[0b1010_0000]); + let stamp = unsafe { Stamp::from_raw(2, 2, [0b1010_0000].as_ptr()) }; let mut pixels = stamp.pixels(); assert_eq!(pixels.next(), Some((0, 0, Color::White))); @@ -163,7 +163,7 @@ mod tests { #[test] fn test_rev() { - let stamp = Stamp::from_raw(2, 2, &[0b1010_0000]); + let stamp = unsafe { Stamp::from_raw(2, 2, [0b1010_0000].as_ptr()) }; let mut pixels = stamp.pixels().rev(); assert_eq!(pixels.next(), Some((1, 1, Color::Black))); diff --git a/src/lib.rs b/src/lib.rs index 2e1f64a..5980b64 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,8 @@ //! Stockbook embeds 1-bit raster images in your code at compile time. //! //! Designed primarily for `#![no_std]` usage, in embedded or other -//! program-memory-constrained environments. +//! program-memory-constrained environments. Compatible with +//! [`avr-progmem`](https://crates.io/crates/avr_progmem). //! //! The main functionality of Stockbook is the [`stamp!`] macro, which lets you //! include data similarly to how [`include_bytes!`] does, but from an image, @@ -21,7 +22,7 @@ //! ```rust //! use stockbook::{stamp, Color, Stamp}; //! -//! # const STAR_DATA: &[u8] = &[ +//! # const STAR_DATA: [u8; 18] = [ //! # 0b00000110, 0b00000000, 0b01100000, 0b00001111, 0b00000000, 0b11110000, //! # 0b11111111, 0b11110111, 0b11111110, 0b00111111, 0b11000001, 0b11111000, //! # 0b00111111, 0b11000011, 0b10011100, 0b01110000, 0b11100110, 0b00000110, @@ -43,7 +44,7 @@ //! # static mut ACTUAL_PIXELS: Vec<(usize, usize)> = Vec::new(); //! # //! # macro_rules! stamp { -//! # ($path:literal) => { Stamp::from_raw(12, 12, &STAR_DATA) }; +//! # ($path:literal) => { unsafe { Stamp::from_raw(12, 12, STAR_DATA.as_ptr()) } }; //! # } //! static STAR_SPRITE: Stamp = stamp!("assets/star.png"); //! @@ -70,6 +71,14 @@ //! own [list of supported formats](https://docs.rs/image/latest/image/codecs/index.html#supported-formats) //! for more details. //! +//! ## Feature flags +//! +//! - **`progmem`** — wraps all pixel data of `Stamp`s in +//! [`avr_progmem::wrapper::ProgMem`](https://docs.rs/avr-progmem/latest/avr_progmem/wrapper/struct.ProgMem.html)s. +//! Combined with the `avr` target, this allows you to keep most of the data in +//! program memory without the need to copy it to RAM. A no-op for non-`avr` +//! targets. +//! //! ## Unstable features //! //! Although this library works on `stable`, any changes to images referenced by the @@ -82,8 +91,10 @@ #![no_std] #![warn(missing_docs)] +mod data; mod iter; +use data::*; use iter::*; pub use stockbook_stamp_macro::stamp; @@ -95,76 +106,12 @@ pub use stockbook_stamp_macro::stamp; /// _(0, 0)_ is the top-left corner of the stamp. /// /// Stamp's pixel colors are represented internally as an array of bytes, in which -/// individual bits correspond to individual pixels. The last byte must be padded -/// and the rest of the slice is completely ignored. -#[derive(Debug, Clone, Copy)] +/// individual bits correspond to individual pixels. +#[derive(Debug, Clone)] pub struct Stamp { width: usize, height: usize, - data: &'static [u8], -} - -impl Stamp { - /// Constructs a stamp and validates the length of `data`. - /// - /// This is a quasi-internal API — the intended way of constructing [`Stamp`]s - /// is via the [`stamp!`] macro. - /// - /// # Panics - /// - /// This function panics if the length of `data` does not match the number of - /// pixels, which is assumed to be `width * height`. - /// - /// For example, here the dimensions of the stamp are 3x3, so 9 pixels in total, and - /// so `data` must contain at least 9 bits (2 bytes rounding up), which it does: - /// - /// ```rust - /// use stockbook::Stamp; - /// - /// let stamp = Stamp::from_raw(3, 3, &[0b11111111, 0b1_0000000]); - /// ``` - /// - /// Here, only 8 bits are provided, so the function panics: - /// - /// ```rust,should_panic - /// # use stockbook::Stamp; - /// let stamp = Stamp::from_raw(3, 3, &[0b11111111]); - /// ``` - /// - /// Similarly here, but in a const context, the program fails to compile: - /// - /// ```rust,compile_fail - /// # use stockbook::Stamp; - /// static STAMP: Stamp = Stamp::from_raw(3, 3, &[0b11111111]); - /// ``` - pub const fn from_raw(width: usize, height: usize, data: &'static [u8]) -> Self { - if Self::bytes_count(width * height) > data.len() { - panic!("length of `data` doesn't match the number of pixels"); - } - - // SAFETY: we just checked that the length of `data` matches the number of pixels - unsafe { Self::from_raw_unchecked(width, height, data) } - } - - /// Constructs a stamp without any checks on the length of `data`. - /// - /// For a safe alternative see [`from_raw`](Stamp::from_raw) or the [`stamp!`] - /// macro. - /// - /// # Safety - /// - /// Callers must ensure that the length of `data` matches the number of pixels. - pub const unsafe fn from_raw_unchecked( - width: usize, - height: usize, - data: &'static [u8], - ) -> Self { - Self { - width, - height, - data, - } - } + data: Data, } impl Stamp { @@ -176,7 +123,7 @@ impl Stamp { /// use stockbook::{stamp, Stamp}; /// /// # macro_rules! stamp { - /// # ($path:literal) => { Stamp::from_raw(3, 2, &[0b000_000_00]) }; + /// # ($path:literal) => { unsafe { Stamp::from_raw(3, 2, [0b000_000_00].as_ptr()) } }; /// # } /// static IMAGE: Stamp = stamp!("image_3x2.png"); /// @@ -195,7 +142,7 @@ impl Stamp { /// use stockbook::{stamp, Stamp}; /// /// # macro_rules! stamp { - /// # ($path:literal) => { Stamp::from_raw(3, 2, &[0b000_000_00]) }; + /// # ($path:literal) => { unsafe { Stamp::from_raw(3, 2, [0b000_000_00].as_ptr()) } }; /// # } /// static IMAGE: Stamp = stamp!("image_3x2.png"); /// @@ -214,7 +161,7 @@ impl Stamp { /// use stockbook::{stamp, Stamp}; /// /// # macro_rules! stamp { - /// # ($path:literal) => { Stamp::from_raw(3, 2, &[0b000_000_00]) }; + /// # ($path:literal) => { unsafe { Stamp::from_raw(3, 2, [0b000_000_00].as_ptr()) } }; /// # } /// static IMAGE: Stamp = stamp!("image_3x2.png"); /// @@ -233,7 +180,7 @@ impl Stamp { /// use stockbook::{stamp, Stamp}; /// /// # macro_rules! stamp { - /// # ($path:literal) => { Stamp::from_raw(3, 2, &[0b000_000_00]) }; + /// # ($path:literal) => { unsafe { Stamp::from_raw(3, 2, [0b000_000_00].as_ptr()) } }; /// # } /// static IMAGE: Stamp = stamp!("image_3x2.png"); /// @@ -252,7 +199,7 @@ impl Stamp { /// use stockbook::{stamp, Color, Stamp}; /// /// # macro_rules! stamp { - /// # ($path:literal) => { Stamp::from_raw(5, 4, &[0b00000000, 0b00000000, 0b0000_0000]) }; + /// # ($path:literal) => { unsafe { Stamp::from_raw(5, 4, [0b00000000, 0b00000000, 0b0000_0000].as_ptr()) } }; /// # } /// static IMAGE: Stamp = stamp!("image_5x4.png"); /// @@ -275,7 +222,7 @@ impl Stamp { /// use stockbook::{stamp, Color, Stamp}; /// /// # macro_rules! stamp { - /// # ($path:literal) => { Stamp::from_raw(3, 3, &[0b101_010_10, 0b1_0000000]) }; + /// # ($path:literal) => { unsafe { Stamp::from_raw(3, 3, [0b101_010_10, 0b1_0000000].as_ptr()) } }; /// # } /// static IMAGE: Stamp = stamp!("checkerboard_3x3.png"); /// @@ -309,7 +256,7 @@ impl Stamp { /// use stockbook::{stamp, Color, Stamp}; /// /// # macro_rules! stamp { - /// # ($path:literal) => { Stamp::from_raw(3, 3, &[0b101_010_10, 0b1_0000000]) }; + /// # ($path:literal) => { unsafe { Stamp::from_raw(3, 3, [0b101_010_10, 0b1_0000000].as_ptr()) } }; /// # } /// static IMAGE: Stamp = stamp!("checkerboard_3x3.png"); /// @@ -330,7 +277,7 @@ impl Stamp { /// use stockbook::{stamp, Color, Stamp}; /// /// # macro_rules! stamp { - /// # ($path:literal) => { Stamp::from_raw(3, 3, &[0b101_010_10, 0b1_0000000]) }; + /// # ($path:literal) => { unsafe { Stamp::from_raw(3, 3, [0b101_010_10, 0b1_0000000].as_ptr()) } }; /// # } /// static IMAGE: Stamp = stamp!("checkerboard_3x3.png"); /// @@ -365,7 +312,7 @@ impl Stamp { /// use stockbook::{stamp, Color, Stamp}; /// /// # macro_rules! stamp { - /// # ($path:literal) => { Stamp::from_raw(3, 3, &[0b101_010_10, 0b1_0000000]) }; + /// # ($path:literal) => { unsafe { Stamp::from_raw(3, 3, [0b101_010_10, 0b1_0000000].as_ptr()) } }; /// # } /// static IMAGE: Stamp = stamp!("checkerboard_3x3.png"); /// @@ -386,17 +333,52 @@ impl Stamp { Color::Black } } -} - -impl Stamp { - const fn bytes_count(pixel_count: usize) -> usize { - let d = pixel_count / 8; - let r = pixel_count % 8; - if r > 0 { - d + 1 - } else { - d + /// Constructs a new stamp. + /// + /// You should not need to call this function directly. It is recommended to use the + /// [`stamp!`] macro instead, which calls this constructor for you, while enforcing + /// its contract. + /// + /// # Safety + /// + /// `data` must point to an array of bytes with at least `(width * height) / 8` + /// elements rounding up to the nearest integer. Bits after the `width * height`-th + /// one are ignored. Also general Rust pointer dereferencing constraints apply, i.e. + /// it must not be dangling. + /// + /// For example, here the dimensions of the stamp are 3x3, so 9 pixels in total, and + /// so `data` must contain at least 9 bits (2 bytes rounding up), which it does: + /// + /// ```rust + /// use stockbook::Stamp; + /// + /// let stamp = unsafe { Stamp::from_raw(3, 3, [0b11111111, 0b1_0000000].as_ptr()) }; + /// ``` + /// + /// Here, only 8 bits are provided, so this is undefined behavior: + /// + /// ```rust,no_run + /// # use stockbook::Stamp; + /// let stamp = unsafe { Stamp::from_raw(3, 3, [0b11111111].as_ptr()) }; // Undefined behavior + /// ``` + /// + /// Similarly here, but in a const context: + /// + /// ```rust,no_run + /// # use stockbook::Stamp; + /// static STAMP: Stamp = unsafe { Stamp::from_raw(3, 3, [0b11111111].as_ptr()) }; // Undefined behavior + /// ``` + /// + /// If the `"progmem"` feature is enabled, `data` must point to a valid byte array + /// that is stored in the program memory domain. The array must be initialized, + /// readable, and immutable (i.e. it must not be changed). Also the pointer must be + /// valid for the `'static` lifetime. + pub const unsafe fn from_raw(width: usize, height: usize, data: *const u8) -> Self { + Self { + width, + height, + data: Data::from_raw(data), } } }