From 57aae8038e96599f19c5168ddaaef59f9d673e76 Mon Sep 17 00:00:00 2001 From: Cryptjar Date: Sat, 2 Apr 2022 18:59:51 +0200 Subject: [PATCH] Add string utilities to make working with string literals easier. See #3 --- examples/uno-string.rs | 114 ++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 4 ++ src/string.rs | 115 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 233 insertions(+) create mode 100644 examples/uno-string.rs create mode 100644 src/string.rs diff --git a/examples/uno-string.rs b/examples/uno-string.rs new file mode 100644 index 0000000..eaa1134 --- /dev/null +++ b/examples/uno-string.rs @@ -0,0 +1,114 @@ +// +// This file provides a example on how to use strings on an Arduino Uno. +// + + +// Define no_std only for AVR +#![cfg_attr(target_arch = "avr", no_std)] +#![no_main] +// +// To unwrap the Option in const context +#![feature(const_option)] + + +use avr_progmem::progmem; // The macro +use avr_progmem::string::ByteString; // Helper for storing strings +#[cfg(target_arch = "avr")] +use panic_halt as _; // halting panic implementation for AVR + + +progmem! { + /// The static data to be stored in program code section + /// Notice the usage of `ByteString`, to store a string as `[u8;N]`, + /// because you can't store a `str` and storing a `&str` wouldn't have + /// much of an effect. + static progmem SOME_TEXT: ByteString<189> = ByteString::new(" +A long test string literal, that is stored in progmem instead of DRAM. +However, to use it, it needs to be temporarily load into DRAM, +so an individual `ByteString` shouldn't be too long. + ").unwrap(); + + /// More data to be stored in program code section + static progmem MORE_TEXT: ByteString<102> = ByteString::new(" +However, you easily store your strings individual, limiting the amount of +temporary DRAM necessary. + ").unwrap(); + + /// Unicode works of course as expected + /// + static progmem UNICODE_TEXT: ByteString<137> = ByteString::new( + "dai 大賢者 kenja, Völlerei lässt grüßen, le garçon de théâtre, Ελληνική Δημοκρατία, Слава Україні" + ).unwrap(); +} + +// Include a fancy printer supporting Arduino Uno's USB-Serial output as well +// as stdout on non-AVR targets. +mod printer; +use printer::Printer; + +#[no_mangle] +fn main() -> ! { + let mut printer = { + #[cfg(target_arch = "avr")] + { + // Initialize the USB-Serial output on the Arduino Uno + + let dp = arduino_uno::Peripherals::take().unwrap(); + + let mut pins = arduino_uno::Pins::new(dp.PORTB, dp.PORTC, dp.PORTD); + + let serial = arduino_uno::Serial::new( + dp.USART0, + pins.d0, + pins.d1.into_output(&mut pins.ddr), + 9600, + ); + Printer(serial) + } + #[cfg(not(target_arch = "avr"))] + { + // Just use stdout for non-AVR targets + Printer + } + }; + + // Print some introduction text + printer.println("Hello from Arduino!"); + printer.println(""); + printer.println("--------------------------"); + printer.println(""); + + // Scope to limit the lifetime of `text_buffer` + { + // The temporary DRAM buffer for the string + let text_buffer = SOME_TEXT.load(); + let text: &str = &text_buffer; // Just derefs to `str` + printer.println(text); + } + + // Or just using temporaries + printer.println(&MORE_TEXT.load()); + printer.println(&UNICODE_TEXT.load()); + + // Even more convenient: use a one-off in-place progmem static via `progmem_str` + printer.println(avr_progmem::progmem_str!("Just a lone literal progmem str")); + printer.println(avr_progmem::progmem_str!("And another one")); + + // Print some final lines + printer.println(""); + printer.println("--------------------------"); + printer.println(""); + printer.println("DONE"); + + // It is very convenient to just exit on non-AVR platforms, otherwise users + // might get the impression that the program hangs, whereas it already + // succeeded. + #[cfg(not(target_arch = "avr"))] + std::process::exit(0); + + // Otherwise, that is on AVR, just go into an infinite loop, because on AVR + // we just can't exit! + loop { + // Done, just do nothing + } +} diff --git a/src/lib.rs b/src/lib.rs index e7229e1..033735c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,9 @@ // However, it seems in more recent Rust version there is no more `llvm_asm`. // And docs.rs uses the latest Rust version. #![cfg_attr(not(doc), feature(llvm_asm))] +// +// For string support, we need to convert from slice to array in const context. +#![cfg_attr(not(doc), feature(const_raw_ptr_deref))] //! //! Progmem utilities for the AVR architectures. @@ -187,6 +190,7 @@ pub mod raw; +pub mod string; mod wrapper; pub use wrapper::ProgMem; diff --git a/src/string.rs b/src/string.rs new file mode 100644 index 0000000..6ab8756 --- /dev/null +++ b/src/string.rs @@ -0,0 +1,115 @@ +use core::ops::Deref; + + + +/// Our own `core::array::TryFromSliceError` +/// +/// Used in [`array_ref_try_from_slice`]. +// We need a local copy of this type, because we need to instantiate it, but it +// has a private field. +struct TryFromSliceError(()); + +/// Const version of `<&[T; N]>::try_from(&[T])` +/// +/// Original Source: +/// https://github.com/rust-lang/rust/blob/eb82facb1626166188d49599a3313fc95201f556/library/core/src/array/mod.rs#L203-L215 +const fn array_ref_try_from_slice<'a, T, const N: usize>( + slice: &[T], +) -> Result<&[T; N], TryFromSliceError> { + if slice.len() == N { + let ptr = slice.as_ptr() as *const [T; N]; + // SAFETY: ok because we just checked that the length fits + unsafe { Ok(&*ptr) } + } else { + Err(TryFromSliceError(())) + } +} + +/// A string stored as byte array. +/// +/// This type is a simple wrapper around a byte array `[u8;N]` and therefore, +/// is stored as such. +/// However, this type primarily is created from `&str` and derefs to `&str`, +/// thus it can be used similar to `String` except that it is not mutable. +/// +/// This type is particularly useful to store string literals in progmem. +/// +/// # Example +/// +/// ```rust +/// progmem! { +/// // Stores a string as a byte array, i.e. `[u8;19]`, but makes it usable +/// // as `&str` (via `Deref`) +/// static progmem TEXT: ByteString<19> = ByteString::new( +/// "dai 大賢者 kenja" +/// ); +/// } +/// // usage: +/// let text_buffer = TEXT.load(); // The temporary DRAM buffer for `TEXT` +/// let text: &str = &text_buffer; // Just derefs to `str` +/// assert_eq!(text, "dai 大賢者 kenja") +/// ``` + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ByteString(pub [u8; N]); + +impl ByteString { + /// Creates a new byte array from the given string + pub const fn new(s: &str) -> Option { + Self::from_bytes(s.as_bytes()) + } + + /// Wraps the given byte slice + pub const fn from_bytes(bytes: &[u8]) -> Option { + let res = array_ref_try_from_slice(bytes); + + match res { + Ok(array) => Some(Self(*array)), + Err(_e) => None, + } + } +} + +impl Deref for ByteString { + type Target = str; + + fn deref(&self) -> &str { + core::str::from_utf8(&self.0).unwrap() + } +} + +/// Define a string in progmem +/// +/// This is a short-cut macro to create an ad-hoc static storing the given +/// string literal as by [`ByteString`] and load it here from progmem into a +/// temporary and return it as `&str`. +/// +/// This macro allows to conveniently put literal string into progmem exactly, +/// where they are used. However, since they are directly loaded into a +/// temporary you don't get a `&'static str` back, and must use the `&str` +/// immediately (i.e. pass it as a function parameter). +/// You can't even store the returned `&str` in a local `let` assignment. +/// +/// # Example +/// +/// ```rust +/// use avr_progmem::progmem_str as S; +/// fn print(s: &str) { +/// // -- snip -- +/// # assert_eq!(s, "dai 大賢者 kenja") +/// } +/// // Put the literal as byte array into progmem and load it here as `&str` +/// print(S!("dai 大賢者 kenja")); +/// ``` +#[macro_export] +macro_rules! progmem_str { + ($text:literal) => {{ + const TEXT_LEN: usize = ::as_bytes($text).len(); + $crate::progmem! { + static progmem TEXT: $crate::string::ByteString = $crate::string::ByteString::new( + $text + ).unwrap(); + } + &*TEXT.load() + }}; +}