Skip to main content

ffi_bridge/
memory.rs

1//! # memory — FFI-safe heap buffers and strings
2//!
3//! Provides [`FfiBuffer`] and [`FfiString`]: the two heap-allocated primitive
4//! types that cross the Go↔Rust boundary.
5//!
6//! ## Ownership model
7//!
8//! ```text
9//!   Rust allocates → caller (Go) reads → Rust frees
10//! ```
11//!
12//! * Rust is always the allocator. Go **never** allocates these types directly.
13//! * The Go side must call the matching `ffi_*_free` exported function when it
14//!   is done with a value. Rust's allocator is invoked; Go's GC is not involved.
15//! * `FfiBuffer` and `FfiString` are `repr(C)` structs of raw pointers + sizes.
16//!   They have **no Drop impl** — they cannot be safely dropped by Rust without
17//!   explicit deallocation, which is intentional: the Go side controls lifetime.
18
19use std::alloc::{alloc, dealloc, Layout};
20use std::ptr;
21
22// ─── FfiBuffer ────────────────────────────────────────────────────────────────
23
24/// FFI-safe byte buffer with explicit ownership semantics.
25///
26/// `data` points to a heap allocation of `capacity` bytes managed by
27/// Rust's global allocator. `len` is the number of initialized bytes.
28///
29/// # Safety
30///
31/// * This type does **not** implement `Drop`. Callers must call
32///   [`ffi_buffer_free`] to release the memory.
33/// * Do not copy this struct without transferring ownership—double-free will result.
34#[repr(C)]
35pub struct FfiBuffer {
36    pub data: *mut u8,
37    pub len: usize,
38    pub capacity: usize,
39}
40
41// SAFETY: raw pointer is Send-able since ownership of the allocation is
42// transferred across the FFI boundary one-at-a-time.
43unsafe impl Send for FfiBuffer {}
44unsafe impl Sync for FfiBuffer {}
45
46impl FfiBuffer {
47    /// Returns an `FfiBuffer` with all fields zero (null data pointer).
48    ///
49    /// Represents an empty / absent buffer. Safe to pass to [`ffi_buffer_free`].
50    #[inline]
51    pub fn null() -> Self {
52        FfiBuffer {
53            data: ptr::null_mut(),
54            len: 0,
55            capacity: 0,
56        }
57    }
58
59    /// Allocate a new buffer of `capacity` bytes.
60    ///
61    /// Returns [`FfiBuffer::null`] if `capacity == 0`.
62    ///
63    /// # Panics
64    ///
65    /// Panics if the allocator returns a null pointer (OOM).
66    pub fn new(capacity: usize) -> Self {
67        if capacity == 0 {
68            return Self::null();
69        }
70        let layout = Layout::array::<u8>(capacity).expect("capacity overflow");
71        // SAFETY: layout is non-zero and valid.
72        let data = unsafe { alloc(layout) };
73        if data.is_null() {
74            // Global allocator contract: null means OOM.
75            panic!("ffi_buffer_alloc: out of memory (capacity={})", capacity);
76        }
77        FfiBuffer {
78            data,
79            len: 0,
80            capacity,
81        }
82    }
83
84    /// Consume a `Vec<u8>` and wrap it as an `FfiBuffer`.
85    ///
86    /// `std::mem::forget` is used to prevent Vec from running its destructor;
87    /// the caller must call [`ffi_buffer_free`] when done.
88    pub fn from_vec(mut vec: Vec<u8>) -> Self {
89        vec.shrink_to_fit();
90        let buf = FfiBuffer {
91            data: vec.as_mut_ptr(),
92            len: vec.len(),
93            capacity: vec.capacity(),
94        };
95        std::mem::forget(vec);
96        buf
97    }
98
99    /// Serialize `value` to JSON and store it in a new `FfiBuffer`.
100    pub fn from_json<T: serde::Serialize>(value: &T) -> Result<Self, crate::errors::FfiError> {
101        let json = serde_json::to_vec(value)
102            .map_err(|e| crate::errors::FfiError::Serialization(e.to_string()))?;
103        Ok(Self::from_vec(json))
104    }
105
106    /// Return the initialized bytes as a slice.
107    ///
108    /// # Safety
109    ///
110    /// The caller must ensure `self.data` is valid for `self.len` bytes and
111    /// that no concurrent write is occurring.
112    #[inline]
113    pub unsafe fn as_slice(&self) -> &[u8] {
114        if self.data.is_null() || self.len == 0 {
115            return &[];
116        }
117        std::slice::from_raw_parts(self.data, self.len)
118    }
119
120    /// Deserialize the buffer's contents as JSON into type `T`.
121    ///
122    /// # Safety
123    ///
124    /// Same requirements as [`as_slice`](Self::as_slice).
125    pub unsafe fn to_json<T: serde::de::DeserializeOwned>(
126        &self,
127    ) -> Result<T, crate::errors::FfiError> {
128        serde_json::from_slice(self.as_slice())
129            .map_err(|e| crate::errors::FfiError::Serialization(e.to_string()))
130    }
131
132    /// Deallocate the buffer.
133    ///
134    /// # Safety
135    ///
136    /// Must only be called once. After this call `self.data` is dangling.
137    pub unsafe fn dealloc(self) {
138        if self.data.is_null() || self.capacity == 0 {
139            return;
140        }
141        let layout = Layout::array::<u8>(self.capacity).expect("capacity overflow");
142        dealloc(self.data, layout);
143    }
144}
145
146/// Allocate an [`FfiBuffer`] of `capacity` bytes.
147///
148/// **Exported as:** `ffi_buffer_alloc`
149#[no_mangle]
150pub extern "C" fn ffi_buffer_alloc(capacity: usize) -> FfiBuffer {
151    FfiBuffer::new(capacity)
152}
153
154/// Free an [`FfiBuffer`] previously allocated by this crate.
155///
156/// Safe to call on a zeroed / null buffer.
157///
158/// **Exported as:** `ffi_buffer_free`
159#[no_mangle]
160pub extern "C" fn ffi_buffer_free(buf: FfiBuffer) {
161    // SAFETY: caller guarantees single-ownership.
162    unsafe { buf.dealloc() };
163}
164
165// ─── FfiString ────────────────────────────────────────────────────────────────
166
167/// FFI-safe UTF-8 string.
168///
169/// `data` points to a heap-allocated byte array of `len` bytes.
170/// The bytes are valid UTF-8 but are **not** necessarily null-terminated
171/// beyond `len`; always use `len` to determine the string length.
172#[repr(C)]
173pub struct FfiString {
174    pub data: *mut u8,
175    pub len: usize,
176}
177
178unsafe impl Send for FfiString {}
179unsafe impl Sync for FfiString {}
180
181impl FfiString {
182    /// Returns an empty `FfiString` (null data pointer, len=0).
183    #[inline]
184    pub fn null() -> Self {
185        FfiString {
186            data: ptr::null_mut(),
187            len: 0,
188        }
189    }
190
191    /// Allocate and copy a UTF-8 string.
192    pub fn new(s: &str) -> Self {
193        let bytes = s.as_bytes();
194        if bytes.is_empty() {
195            return Self::null();
196        }
197        let layout = Layout::array::<u8>(bytes.len()).expect("string too large");
198        let data = unsafe { alloc(layout) };
199        if data.is_null() {
200            panic!("ffi_string_alloc: out of memory");
201        }
202        unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), data, bytes.len()) };
203        FfiString {
204            data,
205            len: bytes.len(),
206        }
207    }
208
209    /// Convert to a `&str`.
210    ///
211    /// # Safety
212    ///
213    /// `self.data` must be valid for `self.len` bytes of valid UTF-8.
214    pub unsafe fn as_str(&self) -> &str {
215        if self.data.is_null() || self.len == 0 {
216            return "";
217        }
218        let bytes = std::slice::from_raw_parts(self.data as *const u8, self.len);
219        std::str::from_utf8_unchecked(bytes)
220    }
221
222    /// Deallocate the string's buffer.
223    ///
224    /// # Safety
225    ///
226    /// Must only be called once.
227    pub unsafe fn dealloc(self) {
228        if self.data.is_null() || self.len == 0 {
229            return;
230        }
231        let layout = Layout::array::<u8>(self.len).expect("string too large");
232        dealloc(self.data, layout);
233    }
234}
235
236/// Allocate and copy a UTF-8 string of `len` bytes starting at `str`.
237///
238/// **Exported as:** `ffi_string_alloc`
239///
240/// # Safety
241///
242/// `str` must be valid for `len` bytes.
243#[no_mangle]
244pub unsafe extern "C" fn ffi_string_alloc(str: *const u8, len: usize) -> FfiString {
245    if str.is_null() || len == 0 {
246        return FfiString::null();
247    }
248    let bytes = std::slice::from_raw_parts(str, len);
249    let s = match std::str::from_utf8(bytes) {
250        Ok(s) => s,
251        Err(_) => return FfiString::null(), // Caller passed non-UTF-8 — return null
252    };
253    FfiString::new(s)
254}
255
256/// Free an [`FfiString`] previously allocated by this crate.
257///
258/// **Exported as:** `ffi_string_free`
259#[no_mangle]
260pub extern "C" fn ffi_string_free(str: FfiString) {
261    // SAFETY: caller guarantees single-ownership.
262    unsafe { str.dealloc() };
263}
264
265// ─── Tests ────────────────────────────────────────────────────────────────────
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn buffer_new_zero_capacity_is_null() {
273        let buf = FfiBuffer::new(0);
274        assert!(buf.data.is_null());
275        assert_eq!(buf.len, 0);
276        assert_eq!(buf.capacity, 0);
277    }
278
279    #[test]
280    fn buffer_alloc_and_free() {
281        let buf = FfiBuffer::new(64);
282        assert!(!buf.data.is_null());
283        assert_eq!(buf.capacity, 64);
284        assert_eq!(buf.len, 0);
285        ffi_buffer_free(buf);
286    }
287
288    #[test]
289    fn buffer_from_vec_round_trip() {
290        let data = b"hello ffi".to_vec();
291        let buf = FfiBuffer::from_vec(data);
292        assert_eq!(buf.len, 9);
293        let slice = unsafe { buf.as_slice() };
294        assert_eq!(slice, b"hello ffi");
295        ffi_buffer_free(buf);
296    }
297
298    #[test]
299    fn buffer_from_json_round_trip() {
300        #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
301        struct Msg {
302            value: u32,
303        }
304
305        let msg = Msg { value: 42 };
306        let buf = FfiBuffer::from_json(&msg).unwrap();
307        let decoded: Msg = unsafe { buf.to_json() }.unwrap();
308        assert_eq!(decoded, msg);
309        ffi_buffer_free(buf);
310    }
311
312    #[test]
313    fn string_null_on_zero_len() {
314        let s = FfiString::new("");
315        assert!(s.data.is_null());
316    }
317
318    #[test]
319    fn string_roundtrip() {
320        let s = FfiString::new("hello world");
321        assert_eq!(s.len, 11);
322        let back = unsafe { s.as_str() };
323        assert_eq!(back, "hello world");
324        ffi_string_free(s);
325    }
326}