Skip to content

Commit d63010f

Browse files
committed
Refactors the library to use a single CArrayString type that
dynamically selects stack or heap storage for C-compatible strings, removing previous trait-based abstractions and separate types.
1 parent 83eafc5 commit d63010f

File tree

11 files changed

+395
-328
lines changed

11 files changed

+395
-328
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "stack-cstr"
3-
version = "0.1.3"
3+
version = "0.2.0"
44
edition = "2024"
55
authors = ["Junkang Yuan <[email protected]>"]
66
description = "High-performance stack-to-heap C string creation for Rust with FFI support"

README.md

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,36 @@
11
# stack_cstr
22

3-
`stack_cstr` is a high-performance Rust library for creating C-style strings (CStr/CString) efficiently.
4-
It tries to write the formatted string into a stack buffer first, and if the string is too long, it falls back to heap allocation.
5-
The final result is a safe C string that can be passed to FFI functions.
3+
`stack_cstr` is a high-performance Rust library for creating C-compatible strings (`&CStr`) efficiently.
4+
It uses a stack buffer for short strings to avoid heap allocation, and automatically falls back to heap allocation for longer strings.
5+
The resulting strings are safe to pass to FFI functions.
6+
7+
---
68

79
## Features
810

9-
- Stack buffer attempt with configurable sizes
10-
- Automatic heap fallback for long strings
11+
- Stack buffer allocation for short strings (default 128 bytes)
12+
- Automatic heap fallback for longer strings
1113
- Supports `format_args!` style formatting
12-
- Returns `Box<dyn CStrLike>` for easy FFI usage
14+
- Returns `CArrayString<128>` for easy FFI usage
1315
- Simple macro interface: `cstr!()`
14-
- Extensible stack sizes
16+
- Ergonomic and safe for passing to C APIs
17+
18+
---
19+
20+
## Usage Example
21+
22+
```rust
23+
use std::ffi::CStr;
24+
25+
use stack_cstr::cstr;
26+
27+
// Create a C-compatible string
28+
let s = cstr!("Pi = {:.2}", 3.14159);
29+
assert_eq!(s.as_c_str().to_str().unwrap(), "Pi = 3.14");
30+
31+
unsafe {
32+
// Pass to FFI as *const c_char
33+
let ptr = s.as_ptr();
34+
assert_eq!(CStr::from_ptr(ptr).to_str().unwrap(), "Pi = 3.14");
35+
}
36+
```

src/c_array_string.rs

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
use std::ffi::{CStr, CString, c_char};
2+
3+
use arrayvec::ArrayString;
4+
5+
use crate::{CStrError, ContainsNulError};
6+
7+
#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd, Hash)]
8+
pub enum CArrayString<const N: usize> {
9+
Stack(ArrayString<N>),
10+
Heap(CString),
11+
}
12+
13+
impl<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+
26+
impl<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+
36+
impl<const N: usize> From<&CString> for CArrayString<N> {
37+
fn from(value: &CString) -> Self {
38+
From::<&CStr>::from(value)
39+
}
40+
}
41+
42+
impl<const N: usize> From<CString> for CArrayString<N> {
43+
fn from(value: CString) -> Self {
44+
Self::Heap(value)
45+
}
46+
}
47+
48+
impl<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+
69+
impl<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+
/// ```
137+
impl<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)]
229+
mod 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+
}

0 commit comments

Comments
 (0)