Skip to main content

shape_value/v2/
string_obj.rs

1//! Refcounted, repr(C) string for v2 runtime.
2//!
3//! ## Memory layout (24 bytes)
4//!
5//! ```text
6//! Offset  Size  Field
7//! ------  ----  -----
8//!   0       8   header (HeapHeader)
9//!   8       8   data (*const u8, UTF-8 bytes, NOT null-terminated)
10//!  16       4   len (u32, byte count)
11//!  20       4   _pad (u32)
12//! ```
13
14use super::heap_header::{HeapHeader, HEAP_KIND_V2_STRING};
15
16/// Refcounted, repr(C) string for v2 runtime.
17/// Total: 24 bytes (header 8 + data ptr 8 + len 4 + pad 4).
18#[repr(C)]
19pub struct StringObj {
20    pub header: HeapHeader,
21    /// Pointer to UTF-8 bytes. NOT null-terminated.
22    pub data: *const u8,
23    /// Byte length (not char count).
24    pub len: u32,
25    pub _pad: u32,
26}
27
28impl StringObj {
29    /// Allocate a new StringObj from a `&str`. Copies the bytes.
30    pub fn new(s: &str) -> *mut Self {
31        let layout = std::alloc::Layout::new::<Self>();
32        let ptr = unsafe { std::alloc::alloc(layout) as *mut Self };
33
34        // Allocate and copy string data
35        let data = if s.is_empty() {
36            std::ptr::null()
37        } else {
38            let data_layout = std::alloc::Layout::from_size_align(s.len(), 1).unwrap();
39            let data_ptr = unsafe { std::alloc::alloc(data_layout) };
40            unsafe { std::ptr::copy_nonoverlapping(s.as_ptr(), data_ptr, s.len()) };
41            data_ptr as *const u8
42        };
43
44        unsafe {
45            (*ptr).header = HeapHeader::new(HEAP_KIND_V2_STRING);
46            (*ptr).data = data;
47            (*ptr).len = s.len() as u32;
48            (*ptr)._pad = 0;
49        }
50        ptr
51    }
52
53    /// Get string as `&str`.
54    ///
55    /// # Safety
56    /// `ptr` must point to a valid, live `StringObj`.
57    pub unsafe fn as_str(ptr: *const Self) -> &'static str {
58        unsafe {
59            if (*ptr).len == 0 {
60                ""
61            } else {
62                let bytes = std::slice::from_raw_parts((*ptr).data, (*ptr).len as usize);
63                std::str::from_utf8_unchecked(bytes)
64            }
65        }
66    }
67
68    /// Byte length of the string.
69    ///
70    /// # Safety
71    /// `ptr` must point to a valid, live `StringObj`.
72    pub unsafe fn len(ptr: *const Self) -> u32 {
73        unsafe { (*ptr).len }
74    }
75
76    /// Whether the string is empty.
77    ///
78    /// # Safety
79    /// `ptr` must point to a valid, live `StringObj`.
80    pub unsafe fn is_empty(ptr: *const Self) -> bool {
81        unsafe { (*ptr).len == 0 }
82    }
83
84    /// Free the StringObj and its data buffer.
85    ///
86    /// # Safety
87    /// `ptr` must point to a valid `StringObj` with no remaining references.
88    /// Must not be called more than once on the same pointer.
89    pub unsafe fn drop(ptr: *mut Self) {
90        unsafe {
91            if (*ptr).len > 0 && !(*ptr).data.is_null() {
92                let data_layout =
93                    std::alloc::Layout::from_size_align((*ptr).len as usize, 1).unwrap();
94                std::alloc::dealloc((*ptr).data as *mut u8, data_layout);
95            }
96            let layout = std::alloc::Layout::new::<Self>();
97            std::alloc::dealloc(ptr as *mut u8, layout);
98        }
99    }
100
101    /// Byte offset constants for JIT codegen.
102    pub const OFFSET_DATA: usize = 8;
103    pub const OFFSET_LEN: usize = 16;
104}
105
106// Compile-time size assertion.
107const _: () = {
108    assert!(std::mem::size_of::<StringObj>() == 24);
109};
110
111// HeapElement impl per ADR-006 §2.7.24 Q25.A SUPERSEDED + R20 S2-prime
112// audit deliverable (b) §4.1.B decision. Constrains `StringObj` to the
113// HeapHeader-at-offset-0 v2-raw element-carrier contract; enables
114// `TypedArray<*const StringObj>::drop_array_heap` per-T release dispatch
115// via compile-time monomorphization (no runtime NativeKind probe).
116unsafe impl super::heap_element::HeapElement for StringObj {
117    unsafe fn release_elem(ptr: *const Self) {
118        if unsafe { super::refcount::v2_release(&(*ptr).header) } {
119            unsafe { Self::drop(ptr as *mut Self) };
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_size_of_string_obj() {
130        assert_eq!(std::mem::size_of::<StringObj>(), 24);
131    }
132
133    #[test]
134    fn test_create_and_read_hello() {
135        unsafe {
136            let ptr = StringObj::new("hello");
137            assert_eq!(StringObj::as_str(ptr), "hello");
138            assert_eq!(StringObj::len(ptr), 5);
139            assert!(!StringObj::is_empty(ptr));
140            assert_eq!((*ptr).header.kind(), HEAP_KIND_V2_STRING);
141            assert_eq!((*ptr).header.get_refcount(), 1);
142            StringObj::drop(ptr);
143        }
144    }
145
146    #[test]
147    fn test_unicode_string() {
148        unsafe {
149            let s = "日本語";
150            let ptr = StringObj::new(s);
151            assert_eq!(StringObj::as_str(ptr), "日本語");
152            // 3 chars x 3 bytes each = 9 bytes
153            assert_eq!(StringObj::len(ptr), 9);
154            assert!(!StringObj::is_empty(ptr));
155            StringObj::drop(ptr);
156        }
157    }
158
159    #[test]
160    fn test_empty_string() {
161        unsafe {
162            let ptr = StringObj::new("");
163            assert_eq!(StringObj::as_str(ptr), "");
164            assert_eq!(StringObj::len(ptr), 0);
165            assert!(StringObj::is_empty(ptr));
166            StringObj::drop(ptr);
167        }
168    }
169
170    #[test]
171    fn test_drop_does_not_leak() {
172        // Create and drop several strings — under Miri/valgrind this would catch leaks.
173        unsafe {
174            for _ in 0..100 {
175                let ptr = StringObj::new("leak test string with some content");
176                StringObj::drop(ptr);
177            }
178            // Empty string variant
179            for _ in 0..100 {
180                let ptr = StringObj::new("");
181                StringObj::drop(ptr);
182            }
183        }
184    }
185
186    #[test]
187    fn test_field_offsets() {
188        let ptr = StringObj::new("test");
189        let base = ptr as usize;
190        unsafe {
191            let data_offset = &(*ptr).data as *const _ as usize - base;
192            let len_offset = &(*ptr).len as *const _ as usize - base;
193
194            assert_eq!(data_offset, StringObj::OFFSET_DATA, "data must be at offset 8");
195            assert_eq!(len_offset, StringObj::OFFSET_LEN, "len must be at offset 16");
196
197            StringObj::drop(ptr);
198        }
199    }
200
201    #[test]
202    fn test_refcount_starts_at_one() {
203        unsafe {
204            let ptr = StringObj::new("refcount test");
205            assert_eq!((*ptr).header.get_refcount(), 1);
206            StringObj::drop(ptr);
207        }
208    }
209
210    #[test]
211    fn test_emoji_string() {
212        unsafe {
213            let s = "hello 🌍🚀✨";
214            let ptr = StringObj::new(s);
215            assert_eq!(StringObj::as_str(ptr), s);
216            assert_eq!(StringObj::len(ptr), s.len() as u32);
217            StringObj::drop(ptr);
218        }
219    }
220
221    #[test]
222    fn test_long_string_1mb() {
223        unsafe {
224            let s = "x".repeat(1_000_000);
225            let ptr = StringObj::new(&s);
226            assert_eq!(StringObj::len(ptr), 1_000_000);
227            assert_eq!(StringObj::as_str(ptr), s.as_str());
228            StringObj::drop(ptr);
229        }
230    }
231}