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}