Skip to main content

shape_jit/ffi/
v2_string_ffi.rs

1//! v2 String FFI functions for JIT-compiled code.
2//!
3//! These functions operate on the v2 `StringObj` layout — a compact, C-compatible
4//! representation that the JIT can manipulate with native pointer types instead
5//! of NaN-boxed u64 values.
6//!
7//! ## StringObj memory layout (24 bytes)
8//!
9//! ```text
10//! Offset  Size  Field
11//! ------  ----  -----
12//!   0       4   refcount (u32)
13//!   4       2   kind (u16, = HK_STRING = 0)
14//!   6       2   (padding)
15//!   8       8   data (*const u8, UTF-8 bytes)
16//!  16       4   len (u32, byte length)
17//!  20       4   (padding)
18//! ```
19//!
20//! The 8-byte header at offset 0 packs refcount + kind + padding so that
21//! the `data` pointer is naturally 8-byte aligned.
22
23use std::alloc::{Layout, alloc, dealloc};
24use std::sync::atomic::{AtomicU32, Ordering};
25
26// ---------------------------------------------------------------------------
27// StringObj repr
28// ---------------------------------------------------------------------------
29
30/// Heap kind constant for strings (matches HeapKind::String = 0).
31const HK_STRING_U16: u16 = 0;
32
33/// A compact, repr(C) string object for the v2 runtime.
34///
35/// The JIT emits raw loads/stores at known offsets, so every field position
36/// must be stable.
37#[repr(C)]
38struct StringObj {
39    /// Reference count (offset 0). Accessed atomically.
40    refcount: AtomicU32,
41    /// Heap kind discriminator (offset 4). Always `HK_STRING_U16`.
42    kind: u16,
43    /// Padding to align `data` at offset 8.
44    _pad_header: u16,
45    /// Pointer to the UTF-8 byte buffer (offset 8). The buffer is allocated
46    /// immediately after the StringObj header when created by `jit_v2_string_alloc`.
47    data: *const u8,
48    /// Byte length of the string (offset 16).
49    len: u32,
50    /// Padding to round the struct to 24 bytes (offset 20).
51    _pad_tail: u32,
52}
53
54// Compile-time layout assertions.
55const _: () = {
56    assert!(std::mem::size_of::<StringObj>() == 24);
57    // data field must be at offset 8
58    // len field must be at offset 16
59};
60
61// ---------------------------------------------------------------------------
62// Helpers
63// ---------------------------------------------------------------------------
64
65/// Layout for a StringObj + trailing string data.
66fn layout_for(byte_len: usize) -> Layout {
67    // StringObj (24 bytes, 8-aligned) followed by `byte_len` bytes of string data.
68    let header_layout = Layout::new::<StringObj>();
69    let data_layout = Layout::from_size_align(byte_len.max(1), 1).unwrap();
70    let (combined, _offset) = header_layout.extend(data_layout).unwrap();
71    combined.pad_to_align()
72}
73
74#[inline]
75unsafe fn as_obj(ptr: *mut u8) -> &'static StringObj {
76    unsafe { &*(ptr as *const StringObj) }
77}
78
79#[inline]
80unsafe fn as_obj_mut(ptr: *mut u8) -> &'static mut StringObj {
81    unsafe { &mut *(ptr as *mut StringObj) }
82}
83
84// ---------------------------------------------------------------------------
85// Public FFI functions
86// ---------------------------------------------------------------------------
87
88/// Allocate a new `StringObj` from raw UTF-8 bytes. Returns a raw pointer
89/// to the `StringObj` (which the JIT treats as `*mut u8` / I64).
90///
91/// The string data is copied into a buffer allocated immediately after the
92/// header, so the caller does not need to keep `data` alive.
93///
94/// Refcount is initialised to 1.
95#[unsafe(no_mangle)]
96pub extern "C" fn jit_v2_string_alloc(data: *const u8, len: u32) -> *mut u8 {
97    let byte_len = len as usize;
98    let layout = layout_for(byte_len);
99
100    unsafe {
101        let ptr = alloc(layout);
102        if ptr.is_null() {
103            // OOM — return null; caller must handle.
104            return std::ptr::null_mut();
105        }
106
107        // Data buffer starts right after the StringObj header.
108        let data_dst = ptr.add(std::mem::size_of::<StringObj>());
109
110        // Copy the source bytes into the trailing buffer.
111        if byte_len > 0 && !data.is_null() {
112            std::ptr::copy_nonoverlapping(data, data_dst, byte_len);
113        }
114
115        // Initialise the header fields.
116        let obj = as_obj_mut(ptr);
117        // Write refcount via raw pointer to avoid requiring &mut AtomicU32
118        // before the memory is fully initialised.
119        std::ptr::write(&raw mut obj.refcount, AtomicU32::new(1));
120        obj.kind = HK_STRING_U16;
121        obj._pad_header = 0;
122        obj.data = data_dst;
123        obj.len = len;
124        obj._pad_tail = 0;
125
126        ptr
127    }
128}
129
130/// Return the byte length of the string.
131#[unsafe(no_mangle)]
132pub extern "C" fn jit_v2_string_len(str_ptr: *mut u8) -> i64 {
133    if str_ptr.is_null() {
134        return 0;
135    }
136    unsafe { as_obj(str_ptr).len as i64 }
137}
138
139/// Return a pointer to the raw UTF-8 data bytes.
140#[unsafe(no_mangle)]
141pub extern "C" fn jit_v2_string_data(str_ptr: *mut u8) -> *const u8 {
142    if str_ptr.is_null() {
143        return std::ptr::null();
144    }
145    unsafe { as_obj(str_ptr).data }
146}
147
148/// Concatenate two v2 strings. Returns a freshly allocated `StringObj` with
149/// refcount 1. Neither input is consumed (their refcounts are unchanged).
150#[unsafe(no_mangle)]
151pub extern "C" fn jit_v2_string_concat(a: *mut u8, b: *mut u8) -> *mut u8 {
152    unsafe {
153        let a_len = if a.is_null() {
154            0usize
155        } else {
156            as_obj(a).len as usize
157        };
158        let b_len = if b.is_null() {
159            0usize
160        } else {
161            as_obj(b).len as usize
162        };
163        let total = a_len + b_len;
164
165        let layout = layout_for(total);
166        let ptr = alloc(layout);
167        if ptr.is_null() {
168            return std::ptr::null_mut();
169        }
170
171        let data_dst = ptr.add(std::mem::size_of::<StringObj>());
172
173        // Copy a's data.
174        if a_len > 0 {
175            std::ptr::copy_nonoverlapping(as_obj(a).data, data_dst, a_len);
176        }
177        // Copy b's data.
178        if b_len > 0 {
179            std::ptr::copy_nonoverlapping(as_obj(b).data, data_dst.add(a_len), b_len);
180        }
181
182        let obj = as_obj_mut(ptr);
183        std::ptr::write(&raw mut obj.refcount, AtomicU32::new(1));
184        obj.kind = HK_STRING_U16;
185        obj._pad_header = 0;
186        obj.data = data_dst;
187        obj.len = total as u32;
188        obj._pad_tail = 0;
189
190        ptr
191    }
192}
193
194/// Compare two v2 strings for byte-equality. Returns 1 if equal, 0 otherwise.
195#[unsafe(no_mangle)]
196pub extern "C" fn jit_v2_string_eq(a: *mut u8, b: *mut u8) -> u8 {
197    if a == b {
198        return 1;
199    }
200    if a.is_null() || b.is_null() {
201        return 0;
202    }
203    unsafe {
204        let obj_a = as_obj(a);
205        let obj_b = as_obj(b);
206        if obj_a.len != obj_b.len {
207            return 0;
208        }
209        let len = obj_a.len as usize;
210        if len == 0 {
211            return 1;
212        }
213        let slice_a = std::slice::from_raw_parts(obj_a.data, len);
214        let slice_b = std::slice::from_raw_parts(obj_b.data, len);
215        if slice_a == slice_b { 1 } else { 0 }
216    }
217}
218
219/// Print a v2 string to stdout (with trailing newline), used by the `print`
220/// builtin in JIT-compiled code.
221#[unsafe(no_mangle)]
222pub extern "C" fn jit_v2_string_print(str_ptr: *mut u8) {
223    if str_ptr.is_null() {
224        println!();
225        return;
226    }
227    unsafe {
228        let obj = as_obj(str_ptr);
229        let len = obj.len as usize;
230        let slice = std::slice::from_raw_parts(obj.data, len);
231        // Best-effort: interpret as UTF-8, replace invalid sequences.
232        let s = std::str::from_utf8(slice).unwrap_or("<invalid utf8>");
233        println!("{}", s);
234    }
235}
236
237/// Increment the reference count (retain). No-op on null.
238#[unsafe(no_mangle)]
239pub extern "C" fn jit_v2_string_retain(str_ptr: *mut u8) {
240    if str_ptr.is_null() {
241        return;
242    }
243    unsafe {
244        as_obj(str_ptr).refcount.fetch_add(1, Ordering::Relaxed);
245    }
246}
247
248/// Decrement the reference count (release). Deallocates the object when the
249/// count reaches zero. No-op on null.
250#[unsafe(no_mangle)]
251pub extern "C" fn jit_v2_string_release(str_ptr: *mut u8) {
252    if str_ptr.is_null() {
253        return;
254    }
255    unsafe {
256        let obj = as_obj(str_ptr);
257        // Acquire on the decrement so that all prior writes to the object are
258        // visible before we potentially deallocate.
259        let prev = obj.refcount.fetch_sub(1, Ordering::Release);
260        if prev == 1 {
261            // Ensure all writes from other threads are visible before dealloc.
262            std::sync::atomic::fence(Ordering::Acquire);
263            let byte_len = obj.len as usize;
264            let layout = layout_for(byte_len);
265            dealloc(str_ptr, layout);
266        }
267    }
268}
269
270// ---------------------------------------------------------------------------
271// Tests
272// ---------------------------------------------------------------------------
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_string_obj_layout() {
280        assert_eq!(std::mem::size_of::<StringObj>(), 24);
281
282        // Verify field offsets using a zeroed instance.
283        let obj = StringObj {
284            refcount: AtomicU32::new(0),
285            kind: 0,
286            _pad_header: 0,
287            data: std::ptr::null(),
288            len: 0,
289            _pad_tail: 0,
290        };
291        let base = &obj as *const _ as usize;
292        assert_eq!(
293            &obj.refcount as *const _ as usize - base,
294            0,
295            "refcount at offset 0"
296        );
297        assert_eq!(
298            &obj.kind as *const _ as usize - base,
299            4,
300            "kind at offset 4"
301        );
302        assert_eq!(
303            &obj.data as *const _ as usize - base,
304            8,
305            "data at offset 8"
306        );
307        assert_eq!(
308            &obj.len as *const _ as usize - base,
309            16,
310            "len at offset 16"
311        );
312    }
313
314    #[test]
315    fn test_alloc_and_len() {
316        let src = b"hello";
317        let ptr = jit_v2_string_alloc(src.as_ptr(), src.len() as u32);
318        assert!(!ptr.is_null());
319
320        assert_eq!(jit_v2_string_len(ptr), 5);
321
322        // Clean up.
323        jit_v2_string_release(ptr);
324    }
325
326    #[test]
327    fn test_alloc_empty() {
328        let ptr = jit_v2_string_alloc(std::ptr::null(), 0);
329        assert!(!ptr.is_null());
330        assert_eq!(jit_v2_string_len(ptr), 0);
331        jit_v2_string_release(ptr);
332    }
333
334    #[test]
335    fn test_data_roundtrip() {
336        let src = b"world";
337        let ptr = jit_v2_string_alloc(src.as_ptr(), src.len() as u32);
338        assert!(!ptr.is_null());
339
340        let data = jit_v2_string_data(ptr);
341        let len = jit_v2_string_len(ptr) as usize;
342        let slice = unsafe { std::slice::from_raw_parts(data, len) };
343        assert_eq!(slice, b"world");
344
345        jit_v2_string_release(ptr);
346    }
347
348    #[test]
349    fn test_concat() {
350        let a = jit_v2_string_alloc(b"foo".as_ptr(), 3);
351        let b = jit_v2_string_alloc(b"bar".as_ptr(), 3);
352        let c = jit_v2_string_concat(a, b);
353        assert!(!c.is_null());
354
355        assert_eq!(jit_v2_string_len(c), 6);
356        let data = jit_v2_string_data(c);
357        let slice = unsafe { std::slice::from_raw_parts(data, 6) };
358        assert_eq!(slice, b"foobar");
359
360        jit_v2_string_release(a);
361        jit_v2_string_release(b);
362        jit_v2_string_release(c);
363    }
364
365    #[test]
366    fn test_concat_with_empty() {
367        let a = jit_v2_string_alloc(b"abc".as_ptr(), 3);
368        let b = jit_v2_string_alloc(std::ptr::null(), 0);
369        let c = jit_v2_string_concat(a, b);
370        assert_eq!(jit_v2_string_len(c), 3);
371
372        let data = jit_v2_string_data(c);
373        let slice = unsafe { std::slice::from_raw_parts(data, 3) };
374        assert_eq!(slice, b"abc");
375
376        jit_v2_string_release(a);
377        jit_v2_string_release(b);
378        jit_v2_string_release(c);
379    }
380
381    #[test]
382    fn test_eq_same_content() {
383        let a = jit_v2_string_alloc(b"test".as_ptr(), 4);
384        let b = jit_v2_string_alloc(b"test".as_ptr(), 4);
385        assert_eq!(jit_v2_string_eq(a, b), 1);
386
387        jit_v2_string_release(a);
388        jit_v2_string_release(b);
389    }
390
391    #[test]
392    fn test_eq_different_content() {
393        let a = jit_v2_string_alloc(b"abc".as_ptr(), 3);
394        let b = jit_v2_string_alloc(b"xyz".as_ptr(), 3);
395        assert_eq!(jit_v2_string_eq(a, b), 0);
396
397        jit_v2_string_release(a);
398        jit_v2_string_release(b);
399    }
400
401    #[test]
402    fn test_eq_different_lengths() {
403        let a = jit_v2_string_alloc(b"ab".as_ptr(), 2);
404        let b = jit_v2_string_alloc(b"abc".as_ptr(), 3);
405        assert_eq!(jit_v2_string_eq(a, b), 0);
406
407        jit_v2_string_release(a);
408        jit_v2_string_release(b);
409    }
410
411    #[test]
412    fn test_eq_same_pointer() {
413        let a = jit_v2_string_alloc(b"dup".as_ptr(), 3);
414        assert_eq!(jit_v2_string_eq(a, a), 1);
415        jit_v2_string_release(a);
416    }
417
418    #[test]
419    fn test_eq_null() {
420        let a = jit_v2_string_alloc(b"x".as_ptr(), 1);
421        assert_eq!(jit_v2_string_eq(a, std::ptr::null_mut()), 0);
422        assert_eq!(jit_v2_string_eq(std::ptr::null_mut(), a), 0);
423        assert_eq!(jit_v2_string_eq(std::ptr::null_mut(), std::ptr::null_mut()), 1);
424        jit_v2_string_release(a);
425    }
426
427    #[test]
428    fn test_retain_release() {
429        let ptr = jit_v2_string_alloc(b"rc".as_ptr(), 2);
430        assert!(!ptr.is_null());
431
432        // Retain bumps refcount to 2.
433        jit_v2_string_retain(ptr);
434        unsafe {
435            assert_eq!(as_obj(ptr).refcount.load(Ordering::Relaxed), 2);
436        }
437
438        // First release drops to 1 — no dealloc.
439        jit_v2_string_release(ptr);
440        unsafe {
441            assert_eq!(as_obj(ptr).refcount.load(Ordering::Relaxed), 1);
442        }
443
444        // Second release drops to 0 — deallocates.
445        jit_v2_string_release(ptr);
446        // ptr is now dangling; we cannot read it.
447    }
448
449    #[test]
450    fn test_null_safety() {
451        // All functions should handle null gracefully.
452        assert_eq!(jit_v2_string_len(std::ptr::null_mut()), 0);
453        assert_eq!(jit_v2_string_data(std::ptr::null_mut()), std::ptr::null());
454        jit_v2_string_print(std::ptr::null_mut()); // should not crash
455        jit_v2_string_retain(std::ptr::null_mut()); // no-op
456        jit_v2_string_release(std::ptr::null_mut()); // no-op
457    }
458
459    #[test]
460    fn test_concat_null_inputs() {
461        // concat(null, null) should produce an empty string.
462        let c = jit_v2_string_concat(std::ptr::null_mut(), std::ptr::null_mut());
463        assert!(!c.is_null());
464        assert_eq!(jit_v2_string_len(c), 0);
465        jit_v2_string_release(c);
466    }
467
468    #[test]
469    fn test_unicode() {
470        let src = "hello 🌍";
471        let bytes = src.as_bytes();
472        let ptr = jit_v2_string_alloc(bytes.as_ptr(), bytes.len() as u32);
473        assert!(!ptr.is_null());
474        assert_eq!(jit_v2_string_len(ptr) as usize, bytes.len());
475
476        let data = jit_v2_string_data(ptr);
477        let slice = unsafe { std::slice::from_raw_parts(data, bytes.len()) };
478        assert_eq!(std::str::from_utf8(slice).unwrap(), src);
479
480        jit_v2_string_release(ptr);
481    }
482}