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}