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}