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;