ExamplesBy LevelBy TopicLearning Paths
490 Fundamental

String Fixed Array

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "String Fixed Array" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Every `String` allocation touches the heap: the allocator must find free memory, update bookkeeping, and the deallocator must run on drop. Key difference from OCaml: 1. **Stack allocation**: Rust's `FixedString<N>` lives entirely on the stack; OCaml's equivalent is always heap

Tutorial

The Problem

Every String allocation touches the heap: the allocator must find free memory, update bookkeeping, and the deallocator must run on drop. In embedded systems (no heap), real-time audio (allocation is forbidden in the audio thread), kernel code, and performance-critical parsers, heap allocation is either impossible or too expensive. A stack-allocated string of fixed maximum size avoids this: the bytes live in the stack frame, Copy semantics are possible, and there is no drop glue. This is the approach of arrayvec, heapless::String, and C's char buf[N].

🎯 Learning Outcomes

  • • Use const generics (const N: usize) to parameterise a struct by capacity at compile time
  • • Store string bytes in [u8; N] with a separate len: usize field
  • • Implement from_str, push_str, push, as_str, and clear
  • • Derive Copy for a fixed-size container — impossible for String
  • • Understand the const fn new() pattern for compile-time initialisation
  • Code Example

    #![allow(clippy::all)]
    //! # String Fixed Array — Stack-Allocated Strings
    //!
    //! Fixed-size strings without heap allocation.
    
    /// Fixed-size string buffer
    #[derive(Clone, Copy)]
    pub struct FixedString<const N: usize> {
        buffer: [u8; N],
        len: usize,
    }
    
    impl<const N: usize> FixedString<N> {
        pub const fn new() -> Self {
            Self {
                buffer: [0; N],
                len: 0,
            }
        }
    
        pub fn from_str(s: &str) -> Option<Self> {
            if s.len() > N {
                return None;
            }
            let mut fs = Self::new();
            fs.buffer[..s.len()].copy_from_slice(s.as_bytes());
            fs.len = s.len();
            Some(fs)
        }
    
        pub fn as_str(&self) -> &str {
            std::str::from_utf8(&self.buffer[..self.len]).unwrap()
        }
    
        pub fn len(&self) -> usize {
            self.len
        }
    
        pub fn is_empty(&self) -> bool {
            self.len == 0
        }
    
        pub fn capacity(&self) -> usize {
            N
        }
    
        pub fn push_str(&mut self, s: &str) -> bool {
            if self.len + s.len() > N {
                return false;
            }
            self.buffer[self.len..self.len + s.len()].copy_from_slice(s.as_bytes());
            self.len += s.len();
            true
        }
    
        pub fn push(&mut self, c: char) -> bool {
            let mut buf = [0u8; 4];
            let s = c.encode_utf8(&mut buf);
            self.push_str(s)
        }
    
        pub fn clear(&mut self) {
            self.len = 0;
        }
    }
    
    impl<const N: usize> Default for FixedString<N> {
        fn default() -> Self {
            Self::new()
        }
    }
    
    impl<const N: usize> std::fmt::Display for FixedString<N> {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{}", self.as_str())
        }
    }
    
    impl<const N: usize> std::fmt::Debug for FixedString<N> {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "FixedString<{}>({:?})", N, self.as_str())
        }
    }
    
    /// Type aliases for common sizes
    pub type String16 = FixedString<16>;
    pub type String32 = FixedString<32>;
    pub type String64 = FixedString<64>;
    pub type String256 = FixedString<256>;
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_create() {
            let s = String32::from_str("hello").unwrap();
            assert_eq!(s.as_str(), "hello");
            assert_eq!(s.len(), 5);
        }
    
        #[test]
        fn test_too_long() {
            let result = String16::from_str("this string is way too long");
            assert!(result.is_none());
        }
    
        #[test]
        fn test_push_str() {
            let mut s = String32::new();
            assert!(s.push_str("hello"));
            assert!(s.push_str(" "));
            assert!(s.push_str("world"));
            assert_eq!(s.as_str(), "hello world");
        }
    
        #[test]
        fn test_push_char() {
            let mut s = String16::new();
            s.push('H');
            s.push('i');
            assert_eq!(s.as_str(), "Hi");
        }
    
        #[test]
        fn test_clear() {
            let mut s = String32::from_str("hello").unwrap();
            s.clear();
            assert!(s.is_empty());
        }
    
        #[test]
        fn test_capacity() {
            let s = String64::new();
            assert_eq!(s.capacity(), 64);
        }
    
        #[test]
        fn test_stack_allocated() {
            // Verify it fits on stack
            let s = String256::from_str("stack allocated").unwrap();
            assert_eq!(
                std::mem::size_of_val(&s),
                256 + std::mem::size_of::<usize>()
            );
        }
    }

    Key Differences

  • Stack allocation: Rust's FixedString<N> lives entirely on the stack; OCaml's equivalent is always heap-allocated.
  • Const generics: Rust's const N: usize is a compile-time parameter — different N values produce distinct types; OCaml would use a runtime n parameter, losing the type-level capacity constraint.
  • **Copy derivation**: FixedString<N> is Copy because [u8; N] and usize are Copy; String cannot be Copy because it owns heap memory.
  • **const fn new**: Rust's const fn enables compile-time construction (static MY_STR: FixedString<16> = FixedString::new()); OCaml has no equivalent.
  • OCaml Approach

    OCaml does not have stack-allocated arrays of statically known size in the same sense. Bytes.create n allocates on the heap. For embedded/no-alloc contexts, OCaml is typically not used; C or Rust are preferred. In standard OCaml, the Bigarray module can use Bigarray.Array1.create Bigarray.int8_unsigned Bigarray.c_layout n for C-layout buffers, but these are still heap-allocated.

    (* Closest OCaml equivalent — heap allocated *)
    let fixed_string_of n s =
      if String.length s > n then None
      else Some (Bytes.of_string s)
    

    OCaml's module system can parameterise by capacity using a functor, but there is no const generic equivalent.

    Full Source

    #![allow(clippy::all)]
    //! # String Fixed Array — Stack-Allocated Strings
    //!
    //! Fixed-size strings without heap allocation.
    
    /// Fixed-size string buffer
    #[derive(Clone, Copy)]
    pub struct FixedString<const N: usize> {
        buffer: [u8; N],
        len: usize,
    }
    
    impl<const N: usize> FixedString<N> {
        pub const fn new() -> Self {
            Self {
                buffer: [0; N],
                len: 0,
            }
        }
    
        pub fn from_str(s: &str) -> Option<Self> {
            if s.len() > N {
                return None;
            }
            let mut fs = Self::new();
            fs.buffer[..s.len()].copy_from_slice(s.as_bytes());
            fs.len = s.len();
            Some(fs)
        }
    
        pub fn as_str(&self) -> &str {
            std::str::from_utf8(&self.buffer[..self.len]).unwrap()
        }
    
        pub fn len(&self) -> usize {
            self.len
        }
    
        pub fn is_empty(&self) -> bool {
            self.len == 0
        }
    
        pub fn capacity(&self) -> usize {
            N
        }
    
        pub fn push_str(&mut self, s: &str) -> bool {
            if self.len + s.len() > N {
                return false;
            }
            self.buffer[self.len..self.len + s.len()].copy_from_slice(s.as_bytes());
            self.len += s.len();
            true
        }
    
        pub fn push(&mut self, c: char) -> bool {
            let mut buf = [0u8; 4];
            let s = c.encode_utf8(&mut buf);
            self.push_str(s)
        }
    
        pub fn clear(&mut self) {
            self.len = 0;
        }
    }
    
    impl<const N: usize> Default for FixedString<N> {
        fn default() -> Self {
            Self::new()
        }
    }
    
    impl<const N: usize> std::fmt::Display for FixedString<N> {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{}", self.as_str())
        }
    }
    
    impl<const N: usize> std::fmt::Debug for FixedString<N> {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "FixedString<{}>({:?})", N, self.as_str())
        }
    }
    
    /// Type aliases for common sizes
    pub type String16 = FixedString<16>;
    pub type String32 = FixedString<32>;
    pub type String64 = FixedString<64>;
    pub type String256 = FixedString<256>;
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_create() {
            let s = String32::from_str("hello").unwrap();
            assert_eq!(s.as_str(), "hello");
            assert_eq!(s.len(), 5);
        }
    
        #[test]
        fn test_too_long() {
            let result = String16::from_str("this string is way too long");
            assert!(result.is_none());
        }
    
        #[test]
        fn test_push_str() {
            let mut s = String32::new();
            assert!(s.push_str("hello"));
            assert!(s.push_str(" "));
            assert!(s.push_str("world"));
            assert_eq!(s.as_str(), "hello world");
        }
    
        #[test]
        fn test_push_char() {
            let mut s = String16::new();
            s.push('H');
            s.push('i');
            assert_eq!(s.as_str(), "Hi");
        }
    
        #[test]
        fn test_clear() {
            let mut s = String32::from_str("hello").unwrap();
            s.clear();
            assert!(s.is_empty());
        }
    
        #[test]
        fn test_capacity() {
            let s = String64::new();
            assert_eq!(s.capacity(), 64);
        }
    
        #[test]
        fn test_stack_allocated() {
            // Verify it fits on stack
            let s = String256::from_str("stack allocated").unwrap();
            assert_eq!(
                std::mem::size_of_val(&s),
                256 + std::mem::size_of::<usize>()
            );
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_create() {
            let s = String32::from_str("hello").unwrap();
            assert_eq!(s.as_str(), "hello");
            assert_eq!(s.len(), 5);
        }
    
        #[test]
        fn test_too_long() {
            let result = String16::from_str("this string is way too long");
            assert!(result.is_none());
        }
    
        #[test]
        fn test_push_str() {
            let mut s = String32::new();
            assert!(s.push_str("hello"));
            assert!(s.push_str(" "));
            assert!(s.push_str("world"));
            assert_eq!(s.as_str(), "hello world");
        }
    
        #[test]
        fn test_push_char() {
            let mut s = String16::new();
            s.push('H');
            s.push('i');
            assert_eq!(s.as_str(), "Hi");
        }
    
        #[test]
        fn test_clear() {
            let mut s = String32::from_str("hello").unwrap();
            s.clear();
            assert!(s.is_empty());
        }
    
        #[test]
        fn test_capacity() {
            let s = String64::new();
            assert_eq!(s.capacity(), 64);
        }
    
        #[test]
        fn test_stack_allocated() {
            // Verify it fits on stack
            let s = String256::from_str("stack allocated").unwrap();
            assert_eq!(
                std::mem::size_of_val(&s),
                256 + std::mem::size_of::<usize>()
            );
        }
    }

    Deep Comparison

    String Fixed Array: Comparison

    See src/lib.rs for the Rust implementation.

    Exercises

  • **Display impl**: Implement fmt::Display for FixedString<N> so it can be used in format! and println!.
  • Const initialisation: Declare static EMPTY: FixedString<64> = FixedString::new() and verify it compiles — exploring the limits of const fn.
  • **Comparison with arrayvec**: Replace FixedString<N> with arrayvec::ArrayString<N> (which has Copy, Display, and Deref<Target=str>) and compare the API surface.
  • Open Source Repos