Skip to main content

windjammer_runtime/
ffi.rs

1//! FFI-safe string types for extern "C" function boundaries.
2//!
3//! Rust `String` and `&str` are NOT FFI-safe (they have unspecified layout).
4//! Use `FfiString` for passing strings across extern "C" boundaries.
5
6/// C-compatible string representation: pointer + length.
7/// Caller owns the allocation; use `ffi_free_string` to free.
8#[repr(C)]
9pub struct FfiString {
10    pub ptr: *const u8,
11    pub len: usize,
12}
13
14impl FfiString {
15    /// Create from Rust string. Caller must call `ffi_free_string` on the result.
16    #[inline]
17    pub fn from_string(s: String) -> Self {
18        let bytes = s.into_bytes();
19        let len = bytes.len();
20        let ptr = Box::into_raw(bytes.into_boxed_slice()) as *const u8;
21        Self { ptr, len }
22    }
23
24    /// Create from &str (copies). Caller must call `ffi_free_string` on the result.
25    #[inline]
26    #[allow(clippy::should_implement_trait)]
27    pub fn from_str(s: &str) -> Self {
28        Self::from_string(s.to_string())
29    }
30
31    /// Convert to Rust String. Consumes the FfiString (memory is freed).
32    #[inline]
33    #[allow(clippy::inherent_to_string)]
34    pub fn to_string(self) -> String {
35        ffi_to_string(self)
36    }
37
38    /// Create empty FfiString (no allocation to free).
39    #[inline]
40    pub fn empty() -> Self {
41        Self {
42            ptr: std::ptr::null(),
43            len: 0,
44        }
45    }
46}
47
48/// Convert Rust String to FFI-safe FfiString.
49/// Caller must call `ffi_free_string` on the result when done.
50#[inline]
51pub fn string_to_ffi(s: String) -> FfiString {
52    FfiString::from_string(s)
53}
54
55/// Convert FfiString to Rust String.
56/// The FfiString is consumed (memory is freed as part of this call).
57#[inline]
58pub fn ffi_to_string(ffi: FfiString) -> String {
59    if ffi.ptr.is_null() || ffi.len == 0 {
60        return String::new();
61    }
62    let bytes = unsafe { Vec::from_raw_parts(ffi.ptr as *mut u8, ffi.len, ffi.len) };
63    String::from_utf8(bytes).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned())
64}
65
66/// Free an FfiString returned from an extern function.
67/// Safe to call with null ptr or empty FfiString.
68#[no_mangle]
69pub extern "C" fn ffi_free_string(ffi: FfiString) {
70    if !ffi.ptr.is_null() && ffi.len > 0 {
71        let _ = unsafe { Vec::from_raw_parts(ffi.ptr as *mut u8, ffi.len, ffi.len) };
72    }
73}
74
75// =============================================================================
76// FfiBytes - FFI-safe Vec<u8> for extern "C" boundaries
77// =============================================================================
78
79/// C-compatible byte buffer: pointer + length + capacity.
80/// Caller owns the allocation; use `ffi_free_bytes` to free.
81#[repr(C)]
82pub struct FfiBytes {
83    pub ptr: *mut u8,
84    pub len: usize,
85    pub cap: usize,
86}
87
88/// Convert Rust Vec<u8> to FFI-safe FfiBytes.
89/// Caller must call `ffi_free_bytes` on the result when done.
90#[inline]
91pub fn vec_to_ffi(mut v: Vec<u8>) -> FfiBytes {
92    let ptr = v.as_mut_ptr();
93    let len = v.len();
94    let cap = v.capacity();
95    std::mem::forget(v);
96    FfiBytes { ptr, len, cap }
97}
98
99/// Convert FfiBytes to Rust Vec<u8>.
100/// The FfiBytes is consumed (memory is freed as part of this call).
101#[inline]
102pub fn ffi_to_vec(ffi: FfiBytes) -> Vec<u8> {
103    if ffi.ptr.is_null() || ffi.len == 0 {
104        return Vec::new();
105    }
106    unsafe { Vec::from_raw_parts(ffi.ptr, ffi.len, ffi.cap) }
107}
108
109/// Free an FfiBytes returned from an extern function.
110/// Safe to call with null ptr or empty FfiBytes.
111#[no_mangle]
112pub extern "C" fn ffi_free_bytes(ffi: FfiBytes) {
113    if !ffi.ptr.is_null() && (ffi.len > 0 || ffi.cap > 0) {
114        let _ = unsafe { Vec::from_raw_parts(ffi.ptr, ffi.len, ffi.cap) };
115    }
116}