Skip to main content

rustbridge_ffi/
binary_types.rs

1//! FFI-safe binary types for C struct transport
2//!
3//! This module provides `#[repr(C)]` types that can be safely passed across the FFI
4//! boundary for binary transport. These types are designed to be used as an alternative
5//! to JSON serialization for performance-critical use cases.
6//!
7//! # Memory Ownership
8//!
9//! - **RbString**: Borrowed reference, caller owns the memory
10//! - **RbBytes**: Borrowed reference, caller owns the memory
11//! - **RbStringOwned**: Rust-owned string, must be freed via `rb_string_free`
12//! - **RbBytesOwned**: Rust-owned bytes, must be freed via `rb_bytes_free`
13//!
14//! # Safety
15//!
16//! All types use explicit `#[repr(C)]` layout for predictable memory representation.
17//! Pointer validity must be ensured by the caller for borrowed types.
18
19use std::ffi::c_void;
20use std::slice;
21
22// ============================================================================
23// Borrowed Types (caller-owned memory)
24// ============================================================================
25
26/// FFI-safe borrowed string reference
27///
28/// This is a view into a UTF-8 string that the caller owns. The string data
29/// must remain valid for the duration of the FFI call.
30///
31/// # Memory Layout
32///
33/// ```text
34/// +--------+--------+
35/// |  len   |  data  |
36/// | (u32)  | (*u8)  |
37/// +--------+--------+
38/// ```
39///
40/// # Invariants
41///
42/// - If `data` is non-null, it must point to valid UTF-8 bytes
43/// - If `data` is non-null, it must be null-terminated (for C compatibility)
44/// - `len` is the byte length, NOT including the null terminator
45/// - If `len == 0` and `data == null`, the string is considered "not present" (None)
46/// - If `len == 0` and `data != null`, the string is an empty string
47#[repr(C)]
48#[derive(Debug, Clone, Copy)]
49pub struct RbString {
50    /// Length in bytes (excluding null terminator)
51    pub len: u32,
52    /// Pointer to null-terminated UTF-8 data
53    pub data: *const u8,
54}
55
56impl RbString {
57    /// Create an empty/absent string (represents None)
58    #[inline]
59    pub const fn none() -> Self {
60        Self {
61            len: 0,
62            data: std::ptr::null(),
63        }
64    }
65
66    /// Create a string from a static str
67    ///
68    /// # Safety
69    ///
70    /// The string must be null-terminated. Use this only with string literals
71    /// or strings known to have a null terminator.
72    #[inline]
73    pub const fn from_static(s: &'static str) -> Self {
74        Self {
75            len: s.len() as u32,
76            data: s.as_ptr(),
77        }
78    }
79
80    /// Check if this string is present (not None)
81    #[inline]
82    pub fn is_present(&self) -> bool {
83        !self.data.is_null()
84    }
85
86    /// Check if this string is empty
87    #[inline]
88    pub fn is_empty(&self) -> bool {
89        self.len == 0
90    }
91
92    /// Convert to a Rust string slice
93    ///
94    /// # Safety
95    ///
96    /// - `data` must be valid for reads of `len` bytes
97    /// - `data` must point to valid UTF-8
98    /// - The memory must not be modified during the lifetime of the returned slice
99    #[inline]
100    pub unsafe fn as_str(&self) -> Option<&str> {
101        if self.data.is_null() {
102            return None;
103        }
104        let bytes = unsafe { slice::from_raw_parts(self.data, self.len as usize) };
105        // SAFETY: Caller guarantees valid UTF-8
106        Some(unsafe { std::str::from_utf8_unchecked(bytes) })
107    }
108
109    /// Convert to a Rust String (copies data)
110    ///
111    /// # Safety
112    ///
113    /// Same requirements as `as_str`
114    #[inline]
115    pub unsafe fn to_string(&self) -> Option<String> {
116        unsafe { self.as_str().map(String::from) }
117    }
118}
119
120// SAFETY: RbString is just a pointer + length, safe to send across threads
121// The actual data it points to may not be, but that's the caller's responsibility
122unsafe impl Send for RbString {}
123unsafe impl Sync for RbString {}
124
125impl Default for RbString {
126    fn default() -> Self {
127        Self::none()
128    }
129}
130
131/// FFI-safe borrowed byte slice reference
132///
133/// This is a view into binary data that the caller owns. The data
134/// must remain valid for the duration of the FFI call.
135///
136/// # Memory Layout
137///
138/// ```text
139/// +--------+--------+
140/// |  len   |  data  |
141/// | (u32)  | (*u8)  |
142/// +--------+--------+
143/// ```
144///
145/// # Invariants
146///
147/// - If `data` is non-null, it must point to `len` valid bytes
148/// - If `len == 0` and `data == null`, represents "not present" (None)
149/// - Maximum size is 4GB (u32::MAX bytes)
150#[repr(C)]
151#[derive(Debug, Clone, Copy)]
152pub struct RbBytes {
153    /// Length in bytes
154    pub len: u32,
155    /// Pointer to binary data
156    pub data: *const u8,
157}
158
159impl RbBytes {
160    /// Create an empty/absent byte slice (represents None)
161    #[inline]
162    pub const fn none() -> Self {
163        Self {
164            len: 0,
165            data: std::ptr::null(),
166        }
167    }
168
169    /// Create from a static byte slice
170    #[inline]
171    pub const fn from_static(bytes: &'static [u8]) -> Self {
172        Self {
173            len: bytes.len() as u32,
174            data: bytes.as_ptr(),
175        }
176    }
177
178    /// Check if this byte slice is present (not None)
179    #[inline]
180    pub fn is_present(&self) -> bool {
181        !self.data.is_null()
182    }
183
184    /// Check if this byte slice is empty
185    #[inline]
186    pub fn is_empty(&self) -> bool {
187        self.len == 0
188    }
189
190    /// Convert to a Rust byte slice
191    ///
192    /// # Safety
193    ///
194    /// - `data` must be valid for reads of `len` bytes
195    /// - The memory must not be modified during the lifetime of the returned slice
196    #[inline]
197    pub unsafe fn as_slice(&self) -> Option<&[u8]> {
198        if self.data.is_null() {
199            return None;
200        }
201        Some(unsafe { slice::from_raw_parts(self.data, self.len as usize) })
202    }
203
204    /// Convert to a Vec<u8> (copies data)
205    ///
206    /// # Safety
207    ///
208    /// Same requirements as `as_slice`
209    #[inline]
210    pub unsafe fn to_vec(&self) -> Option<Vec<u8>> {
211        unsafe { self.as_slice().map(Vec::from) }
212    }
213}
214
215// SAFETY: RbBytes is just a pointer + length, safe to send across threads
216unsafe impl Send for RbBytes {}
217unsafe impl Sync for RbBytes {}
218
219impl Default for RbBytes {
220    fn default() -> Self {
221        Self::none()
222    }
223}
224
225// ============================================================================
226// Owned Types (Rust-owned memory, must be freed)
227// ============================================================================
228
229/// FFI-safe owned string
230///
231/// Unlike `RbString`, this type owns its memory and must be freed by calling
232/// `rb_string_free`. This is used for strings returned from Rust to the host.
233///
234/// # Memory Ownership
235///
236/// - Memory is allocated by Rust
237/// - Must be freed by calling `rb_string_free`
238/// - Do NOT free with host language's free()
239#[repr(C)]
240#[derive(Debug)]
241pub struct RbStringOwned {
242    /// Length in bytes (excluding null terminator)
243    pub len: u32,
244    /// Pointer to null-terminated UTF-8 data (Rust-owned)
245    pub data: *mut u8,
246    /// Allocation capacity (for proper deallocation)
247    pub capacity: u32,
248}
249
250impl RbStringOwned {
251    /// Create an empty owned string
252    #[inline]
253    pub fn empty() -> Self {
254        Self {
255            len: 0,
256            data: std::ptr::null_mut(),
257            capacity: 0,
258        }
259    }
260
261    /// Create from a Rust String (takes ownership)
262    ///
263    /// The string will be null-terminated for C compatibility.
264    pub fn from_string(mut s: String) -> Self {
265        // Ensure null termination
266        s.push('\0');
267        let len = s.len() - 1; // Exclude null terminator from len
268        let capacity = s.capacity();
269        let data = s.as_mut_ptr();
270        std::mem::forget(s); // Prevent String destructor
271
272        Self {
273            len: len as u32,
274            data,
275            capacity: capacity as u32,
276        }
277    }
278
279    /// Create from a string slice (copies data)
280    pub fn from_slice(s: &str) -> Self {
281        Self::from_string(s.to_string())
282    }
283
284    /// Convert to borrowed RbString
285    #[inline]
286    pub fn as_borrowed(&self) -> RbString {
287        RbString {
288            len: self.len,
289            data: self.data,
290        }
291    }
292
293    /// Free the owned string
294    ///
295    /// # Safety
296    ///
297    /// - Must only be called once
298    /// - Must only be called on strings created by Rust
299    pub unsafe fn free(&mut self) {
300        if !self.data.is_null() && self.capacity > 0 {
301            // Reconstruct the String and let it drop
302            let _ = unsafe {
303                String::from_raw_parts(self.data, (self.len + 1) as usize, self.capacity as usize)
304            };
305            self.data = std::ptr::null_mut();
306            self.len = 0;
307            self.capacity = 0;
308        }
309    }
310}
311
312// SAFETY: RbStringOwned owns its memory and can be sent across threads
313unsafe impl Send for RbStringOwned {}
314
315impl Default for RbStringOwned {
316    fn default() -> Self {
317        Self::empty()
318    }
319}
320
321/// FFI-safe owned byte buffer
322///
323/// Unlike `RbBytes`, this type owns its memory and must be freed by calling
324/// `rb_bytes_free`. This is used for binary data returned from Rust to the host.
325///
326/// # Memory Ownership
327///
328/// - Memory is allocated by Rust
329/// - Must be freed by calling `rb_bytes_free`
330/// - Do NOT free with host language's free()
331#[repr(C)]
332#[derive(Debug)]
333pub struct RbBytesOwned {
334    /// Length in bytes
335    pub len: u32,
336    /// Pointer to binary data (Rust-owned)
337    pub data: *mut u8,
338    /// Allocation capacity (for proper deallocation)
339    pub capacity: u32,
340}
341
342impl RbBytesOwned {
343    /// Create an empty owned byte buffer
344    #[inline]
345    pub fn empty() -> Self {
346        Self {
347            len: 0,
348            data: std::ptr::null_mut(),
349            capacity: 0,
350        }
351    }
352
353    /// Create from a Vec<u8> (takes ownership)
354    pub fn from_vec(mut v: Vec<u8>) -> Self {
355        let len = v.len();
356        let capacity = v.capacity();
357        let data = v.as_mut_ptr();
358        std::mem::forget(v); // Prevent Vec destructor
359
360        Self {
361            len: len as u32,
362            data,
363            capacity: capacity as u32,
364        }
365    }
366
367    /// Create from a byte slice (copies data)
368    pub fn from_slice(bytes: &[u8]) -> Self {
369        Self::from_vec(bytes.to_vec())
370    }
371
372    /// Convert to borrowed RbBytes
373    #[inline]
374    pub fn as_borrowed(&self) -> RbBytes {
375        RbBytes {
376            len: self.len,
377            data: self.data,
378        }
379    }
380
381    /// Free the owned bytes
382    ///
383    /// # Safety
384    ///
385    /// - Must only be called once
386    /// - Must only be called on buffers created by Rust
387    pub unsafe fn free(&mut self) {
388        if !self.data.is_null() && self.capacity > 0 {
389            // Reconstruct the Vec and let it drop
390            let _ = unsafe {
391                Vec::from_raw_parts(self.data, self.len as usize, self.capacity as usize)
392            };
393            self.data = std::ptr::null_mut();
394            self.len = 0;
395            self.capacity = 0;
396        }
397    }
398}
399
400// SAFETY: RbBytesOwned owns its memory and can be sent across threads
401unsafe impl Send for RbBytesOwned {}
402
403impl Default for RbBytesOwned {
404    fn default() -> Self {
405        Self::empty()
406    }
407}
408
409// ============================================================================
410// FFI Response Buffer for Binary Transport
411// ============================================================================
412
413/// FFI buffer for binary transport responses
414///
415/// Similar to `FfiBuffer` but designed specifically for binary struct responses.
416/// The response data is a raw C struct that can be cast directly by the host.
417///
418/// # Memory Layout
419///
420/// ```text
421/// +------------+--------+----------+------------+
422/// | error_code |  len   | capacity |    data    |
423/// |   (u32)    | (u32)  |  (u32)   | (*mut u8)  |
424/// +------------+--------+----------+------------+
425/// ```
426///
427/// # Usage
428///
429/// - `error_code == 0`: Success, `data` points to response struct
430/// - `error_code != 0`: Error, `data` may point to error message
431#[repr(C)]
432#[derive(Debug)]
433pub struct RbResponse {
434    /// Error code (0 = success)
435    pub error_code: u32,
436    /// Size of response data in bytes
437    pub len: u32,
438    /// Allocation capacity
439    pub capacity: u32,
440    /// Pointer to response data (or error message)
441    pub data: *mut c_void,
442}
443
444impl RbResponse {
445    /// Create a successful response with struct data
446    ///
447    /// # Safety
448    ///
449    /// The type T must be `#[repr(C)]` and safe to transmit across FFI.
450    pub fn success<T: Sized>(value: T) -> Self {
451        let size = std::mem::size_of::<T>();
452        let align = std::mem::align_of::<T>();
453
454        // Allocate aligned memory
455        #[allow(clippy::expect_used)] // Safe: Layout is valid for any Sized type from std::mem
456        let layout = std::alloc::Layout::from_size_align(size, align)
457            .expect("Invalid layout for response type");
458
459        // SAFETY: We just created a valid layout
460        let data = unsafe { std::alloc::alloc(layout) as *mut c_void };
461
462        if data.is_null() {
463            return Self::error(11, "Failed to allocate response buffer");
464        }
465
466        // Copy the value into the allocated memory
467        // SAFETY: data is valid, aligned, and sized for T
468        unsafe {
469            std::ptr::write(data as *mut T, value);
470        }
471
472        Self {
473            error_code: 0,
474            len: size as u32,
475            capacity: size as u32,
476            data,
477        }
478    }
479
480    /// Create an error response
481    pub fn error(code: u32, message: &str) -> Self {
482        let mut msg = message.as_bytes().to_vec();
483        msg.push(0); // Null terminate
484
485        let len = msg.len();
486        let capacity = msg.capacity();
487        let data = msg.as_mut_ptr();
488        std::mem::forget(msg);
489
490        Self {
491            error_code: code,
492            len: len as u32,
493            capacity: capacity as u32,
494            data: data as *mut c_void,
495        }
496    }
497
498    /// Create an empty response (for invalid calls)
499    pub fn empty() -> Self {
500        Self {
501            error_code: 0,
502            len: 0,
503            capacity: 0,
504            data: std::ptr::null_mut(),
505        }
506    }
507
508    /// Check if this response indicates an error
509    #[inline]
510    pub fn is_error(&self) -> bool {
511        self.error_code != 0
512    }
513
514    /// Free the response buffer
515    ///
516    /// # Safety
517    ///
518    /// - Must only be called once
519    /// - Must only be called on responses created by Rust
520    /// - For success responses, the original type's size must match `len`
521    pub unsafe fn free(&mut self) {
522        if !self.data.is_null() && self.capacity > 0 {
523            if self.error_code != 0 {
524                // Error message is a Vec<u8>
525                let _ = unsafe {
526                    Vec::from_raw_parts(
527                        self.data as *mut u8,
528                        self.len as usize,
529                        self.capacity as usize,
530                    )
531                };
532            } else {
533                // Success data was allocated with alloc
534                // SAFETY: capacity was set from a valid layout during allocation
535                let layout = unsafe {
536                    std::alloc::Layout::from_size_align_unchecked(
537                        self.capacity as usize,
538                        std::mem::align_of::<usize>(), // Conservative alignment
539                    )
540                };
541                unsafe {
542                    std::alloc::dealloc(self.data as *mut u8, layout);
543                }
544            }
545            self.data = std::ptr::null_mut();
546            self.len = 0;
547            self.capacity = 0;
548        }
549    }
550
551    /// Get the response data as a typed reference
552    ///
553    /// # Safety
554    ///
555    /// - Response must be successful (error_code == 0)
556    /// - Type T must match the type used to create the response
557    /// - T must be `#[repr(C)]`
558    #[inline]
559    pub unsafe fn as_ref<T: Sized>(&self) -> Option<&T> {
560        if self.is_error() || self.data.is_null() {
561            return None;
562        }
563        Some(unsafe { &*(self.data as *const T) })
564    }
565}
566
567// SAFETY: RbResponse owns its data and can be sent across threads
568unsafe impl Send for RbResponse {}
569
570impl Default for RbResponse {
571    fn default() -> Self {
572        Self::empty()
573    }
574}
575
576// ============================================================================
577// Tests
578// ============================================================================
579
580#[cfg(test)]
581#[path = "binary_types/binary_types_tests.rs"]
582mod binary_types_tests;