Skip to main content

seq_runtime/string_ops/
cstring.rs

1//! FFI C-string glue: Seq String ↔ null-terminated C char buffer.
2//! Used by the FFI wrapper codegen to pass strings across the C ABI.
3
4use crate::seqstring::global_string;
5use crate::stack::{Stack, push};
6use crate::value::Value;
7
8// ============================================================================
9// FFI String Helpers
10// ============================================================================
11
12/// Convert a Seq String on the stack to a null-terminated C string.
13///
14/// The returned pointer must be freed by the caller using free().
15/// This peeks the string from the stack (caller pops after use).
16///
17/// Stack effect: ( String -- ) returns ptr to C string
18///
19/// # Memory Safety
20///
21/// The returned C string is a **completely independent copy** allocated via
22/// `malloc()`. It has no connection to Seq's memory management:
23///
24/// - The Seq String on the stack remains valid and unchanged
25/// - The returned pointer is owned by the C world and must be freed with `free()`
26/// - Even if the Seq String is garbage collected, the C string remains valid
27/// - Multiple calls with the same Seq String produce independent C strings
28///
29/// This design ensures FFI calls cannot cause use-after-free or double-free
30/// bugs between Seq and C code.
31///
32/// # Safety
33/// Stack must have a String value on top. The unused second argument
34/// exists for future extension (passing output buffer).
35#[unsafe(no_mangle)]
36pub unsafe extern "C" fn patch_seq_string_to_cstring(stack: Stack, _out: *mut u8) -> *mut u8 {
37    assert!(!stack.is_null(), "string_to_cstring: stack is empty");
38
39    use crate::stack::peek;
40    use crate::value::Value;
41
42    // Peek the string value (don't pop - caller will pop after we return)
43    let val = unsafe { peek(stack) };
44    let s = match &val {
45        Value::String(s) => s,
46        other => panic!(
47            "string_to_cstring: expected String on stack, got {:?}",
48            other
49        ),
50    };
51
52    let str_ptr = s.as_ptr();
53    let len = s.len();
54
55    // Guard against overflow: len + 1 for null terminator
56    let alloc_size = len.checked_add(1).unwrap_or_else(|| {
57        panic!(
58            "string_to_cstring: string too large for C conversion (len={})",
59            len
60        )
61    });
62
63    // Allocate space for string + null terminator
64    let ptr = unsafe { libc::malloc(alloc_size) as *mut u8 };
65    if ptr.is_null() {
66        panic!("string_to_cstring: malloc failed");
67    }
68
69    // Copy string data
70    unsafe {
71        std::ptr::copy_nonoverlapping(str_ptr, ptr, len);
72        // Add null terminator
73        *ptr.add(len) = 0;
74    }
75
76    ptr
77}
78
79/// Convert a null-terminated C string to a Seq String and push onto stack.
80///
81/// The C string is NOT freed by this function.
82///
83/// Stack effect: ( -- String )
84///
85/// # Safety
86/// cstr must be a valid null-terminated C string.
87#[unsafe(no_mangle)]
88pub unsafe extern "C" fn patch_seq_cstring_to_string(stack: Stack, cstr: *const u8) -> Stack {
89    if cstr.is_null() {
90        // NULL string - push empty string
91        return unsafe { push(stack, Value::String(global_string(String::new()))) };
92    }
93
94    // Get string length
95    let len = unsafe { libc::strlen(cstr as *const libc::c_char) };
96
97    // Create Rust string from C string
98    let slice = unsafe { std::slice::from_raw_parts(cstr, len) };
99    let s = String::from_utf8_lossy(slice).into_owned();
100
101    unsafe { push(stack, Value::String(global_string(s))) }
102}