stack_cstr/
c_array_string.rs

1use std::ffi::{CStr, CString, c_char};
2
3use arrayvec::ArrayString;
4
5use crate::{CStrError, ContainsNulError};
6
7#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd, Hash)]
8pub enum CArrayString<const N: usize> {
9    Stack(ArrayString<N>),
10    Heap(CString),
11}
12
13impl<const N: usize> From<&CStr> for CArrayString<N> {
14    fn from(value: &CStr) -> Self {
15        if value.count_bytes() < N {
16            let mut buf = ArrayString::<N>::new();
17            buf.push_str(unsafe { str::from_utf8_unchecked(value.to_bytes()) });
18            buf.push('\0');
19            Self::Stack(buf)
20        } else {
21            Self::Heap(value.to_owned())
22        }
23    }
24}
25
26impl<const N: usize> TryFrom<&[u8]> for CArrayString<N> {
27    type Error = CStrError;
28
29    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
30        CStr::from_bytes_with_nul(value)
31            .map(CArrayString::from)
32            .map_err(Into::into)
33    }
34}
35
36impl<const N: usize> From<&CString> for CArrayString<N> {
37    fn from(value: &CString) -> Self {
38        From::<&CStr>::from(value)
39    }
40}
41
42impl<const N: usize> From<CString> for CArrayString<N> {
43    fn from(value: CString) -> Self {
44        Self::Heap(value)
45    }
46}
47
48impl<const N: usize> TryFrom<&str> for CArrayString<N> {
49    type Error = CStrError;
50
51    fn try_from(value: &str) -> Result<Self, Self::Error> {
52        if value.len() < N {
53            let bytes = value.as_bytes();
54            match core::slice::memchr::memchr(0, bytes) {
55                Some(_i) => Err(Into::into(ContainsNulError)),
56                None => Ok({
57                    let mut buf = ArrayString::<N>::new();
58                    buf.push_str(value);
59                    buf.push('\0');
60                    Self::Stack(buf)
61                }),
62            }
63        } else {
64            CString::new(value).map(Self::Heap).map_err(Into::into)
65        }
66    }
67}
68
69impl<const N: usize> TryFrom<&String> for CArrayString<N> {
70    type Error = CStrError;
71
72    fn try_from(value: &String) -> Result<Self, Self::Error> {
73        TryFrom::<&str>::try_from(value)
74    }
75}
76
77/// A C-compatible string type that can be stored on the stack or heap.
78///
79/// `CArrayString<N>` provides a unified abstraction over two storage strategies:
80///
81/// 1. **Stack-allocated:** Uses [`ArrayString<N>`] for small strings that fit into
82///    a fixed-size buffer. This avoids heap allocation and is very efficient.
83/// 2. **Heap-allocated:** Uses [`CString`] when the string exceeds the stack buffer,
84///    ensuring the string is always valid and null-terminated.
85///
86/// This type guarantees:
87/// - [`as_ptr`] always returns a valid, null-terminated C string pointer for the lifetime of `self`.
88/// - [`as_c_str`] always returns a valid [`CStr`] reference.
89///
90/// # Stack vs Heap Behavior
91///
92/// When creating a `CArrayString` via [`new`], the string is first attempted to be stored on
93/// the stack. If it does not fit, it falls back to a heap allocation:
94///
95/// ```text
96/// ┌───────────────┐
97/// │ Stack Buffer  │  (ArrayString<N>)
98/// └───────────────┘
99///       │ fits
100///       └─> use stack
101///
102///       │ does not fit
103///       └─> allocate heap (CString)
104/// ```
105///
106/// # Performance Considerations
107///
108/// - Small strings that fit in the stack buffer avoid heap allocations and are faster.
109/// - Large strings trigger heap allocation, which may be slower and use more memory.
110/// - Prefer choosing `N` large enough for your common use case to minimize heap fallbacks.
111///
112/// # Examples
113///
114/// ```
115/// use std::ffi::CStr;
116/// 
117/// use stack_cstr::CArrayString;
118///
119/// // Small string fits on stack
120/// let stack_str = CArrayString::<16>::new(format_args!("hello"));
121/// assert!(matches!(stack_str, CArrayString::Stack(_)));
122///
123/// // Large string falls back to heap
124/// let heap_str = CArrayString::<4>::new(format_args!("this is too long"));
125/// assert!(matches!(heap_str, CArrayString::Heap(_)));
126///
127/// // Accessing as CStr
128/// let cstr: &CStr = heap_str.as_c_str();
129/// assert_eq!(cstr.to_str().unwrap(), "this is too long");
130///
131/// // Raw pointer for FFI
132/// let ptr = stack_str.as_ptr();
133/// unsafe {
134///     assert_eq!(CStr::from_ptr(ptr).to_str().unwrap(), "hello");
135/// }
136/// ```
137impl<const N: usize> CArrayString<N> {
138    /// Creates a new C-compatible string using `format_args!`.
139    ///
140    /// Attempts to store the formatted string in a stack buffer of size `N`.
141    /// Falls back to a heap allocation if the string does not fit.
142    ///
143    /// # Parameters
144    ///
145    /// - `fmt`: The formatted arguments, typically produced by `format_args!`.
146    ///
147    /// # Returns
148    ///
149    /// A `CArrayString<N>` containing the formatted string.
150    ///
151    /// # Notes
152    ///
153    /// - If the stack buffer overflows or writing fails, the string is stored on the heap.
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// use stack_cstr::CArrayString;
159    ///
160    /// let s = CArrayString::<8>::new(format_args!("hi {}!", "you"));
161    /// assert!(s.as_c_str().to_str().unwrap().starts_with("hi"));
162    /// ```
163    pub fn new(fmt: std::fmt::Arguments) -> CArrayString<N> {
164        fn try_stack<const N: usize>(
165            fmt: std::fmt::Arguments,
166        ) -> Result<ArrayString<N>, CStrError> {
167            let mut buf: ArrayString<N> = ArrayString::new();
168            std::fmt::write(&mut buf, fmt)?;
169            buf.try_push('\0')?;
170            Ok(buf)
171        }
172
173        match try_stack::<N>(fmt) {
174            Ok(arr) => Self::Stack(arr),
175            Err(_) => Self::Heap(CString::new(std::fmt::format(fmt)).unwrap()),
176        }
177    }
178
179    /// Returns a raw pointer to the null-terminated C string.
180    ///
181    /// The pointer is valid for the lifetime of `self`.
182    /// This is useful for passing the string to C APIs via FFI.
183    ///
184    /// # Examples
185    ///
186    /// ```
187    /// use std::ffi::CStr;
188    /// 
189    /// use stack_cstr::CArrayString;
190    ///
191    /// let s = CArrayString::<8>::new(format_args!("hello"));
192    /// let ptr = s.as_ptr();
193    /// unsafe {
194    ///     assert_eq!(CStr::from_ptr(ptr).to_str().unwrap(), "hello");
195    /// }
196    /// ```
197    pub fn as_ptr(&self) -> *const c_char {
198        match self {
199            CArrayString::Stack(s) => s.as_ptr() as _,
200            CArrayString::Heap(s) => s.as_ptr(),
201        }
202    }
203
204    /// Returns a reference to the underlying [`CStr`].
205    ///
206    /// Provides safe access to the string as a `&CStr` without exposing the
207    /// underlying storage strategy.
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// use std::ffi::CStr;
213    /// 
214    /// use stack_cstr::CArrayString;
215    ///
216    /// let s = CArrayString::<8>::new(format_args!("hello"));
217    /// let cstr: &CStr = s.as_c_str();
218    /// assert_eq!(cstr.to_str().unwrap(), "hello");
219    /// ```
220    pub fn as_c_str(&self) -> &CStr {
221        match self {
222            CArrayString::Stack(s) => unsafe { CStr::from_bytes_with_nul_unchecked(s.as_bytes()) },
223            CArrayString::Heap(s) => s.as_c_str(),
224        }
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_stack_overflow() {
234        assert_eq!(
235            CArrayString::<12>::try_from("hello world")
236                .unwrap()
237                .as_c_str()
238                .to_str()
239                .unwrap(),
240            "hello world"
241        );
242        assert_eq!(
243            CArrayString::<11>::try_from("hello world")
244                .unwrap()
245                .as_c_str()
246                .to_str()
247                .unwrap(),
248            "hello world"
249        );
250    }
251
252    #[test]
253    fn test_cstr() {
254        assert_eq!(
255            CArrayString::<12>::from(c"hello world")
256                .as_c_str()
257                .to_str()
258                .unwrap(),
259            "hello world"
260        );
261        assert_eq!(
262            CArrayString::<11>::from(c"hello world")
263                .as_c_str()
264                .to_str()
265                .unwrap(),
266            "hello world"
267        );
268    }
269
270    #[test]
271    fn test_format_args() {
272        let s1 = "hello";
273        let s2 = "world";
274        assert_eq!(
275            CArrayString::<12>::new(format_args!("{s1} world"))
276                .as_c_str()
277                .to_str()
278                .unwrap(),
279            "hello world"
280        );
281        assert_eq!(
282            CArrayString::<11>::new(format_args!("hello {s2}"))
283                .as_c_str()
284                .to_str()
285                .unwrap(),
286            "hello world"
287        );
288    }
289}