screencapturekit/utils/
ffi_string.rs

1//! FFI string utilities
2//!
3//! Helper functions for retrieving strings from C/Objective-C APIs
4//! that use buffer-based string retrieval patterns.
5
6use std::ffi::CStr;
7
8/// Default buffer size for FFI string retrieval
9pub const DEFAULT_BUFFER_SIZE: usize = 1024;
10
11/// Smaller buffer size for short strings (e.g., device IDs, stream names)
12pub const SMALL_BUFFER_SIZE: usize = 256;
13
14/// Retrieves a string from an FFI function that writes to a buffer.
15///
16/// This is a common pattern in Objective-C FFI where a function:
17/// 1. Takes a buffer pointer and length
18/// 2. Writes a null-terminated string to the buffer
19/// 3. Returns a boolean indicating success
20///
21/// # Arguments
22/// * `buffer_size` - Size of the buffer to allocate
23/// * `ffi_call` - A closure that takes (`buffer_ptr`, `buffer_len`) and returns success bool
24///
25/// # Returns
26/// * `Some(String)` if the FFI call succeeded and the string was valid UTF-8
27/// * `None` if the FFI call failed or returned an empty string
28///
29/// # Safety
30/// The caller must ensure the `ffi_call` closure properly writes a null-terminated
31/// string to the provided buffer and does not write beyond the buffer length.
32///
33/// # Example
34/// ```
35/// use screencapturekit::utils::ffi_string::ffi_string_from_buffer;
36///
37/// let result = unsafe {
38///     ffi_string_from_buffer(64, |buf, len| {
39///         // Simulate FFI call that writes "hello" to buffer
40///         let src = b"hello\0";
41///         if len >= src.len() as isize {
42///             std::ptr::copy_nonoverlapping(src.as_ptr(), buf as *mut u8, src.len());
43///             true
44///         } else {
45///             false
46///         }
47///     })
48/// };
49/// assert_eq!(result, Some("hello".to_string()));
50/// ```
51#[allow(clippy::cast_possible_wrap)]
52pub unsafe fn ffi_string_from_buffer<F>(buffer_size: usize, ffi_call: F) -> Option<String>
53where
54    F: FnOnce(*mut i8, isize) -> bool,
55{
56    let mut buffer = vec![0i8; buffer_size];
57    let success = ffi_call(buffer.as_mut_ptr(), buffer.len() as isize);
58    if success {
59        let c_str = CStr::from_ptr(buffer.as_ptr());
60        let s = c_str.to_string_lossy().to_string();
61        if s.is_empty() {
62            None
63        } else {
64            Some(s)
65        }
66    } else {
67        None
68    }
69}
70
71/// Same as [`ffi_string_from_buffer`] but returns an empty string on failure
72/// instead of `None`.
73///
74/// Useful when the API should always return a string, even if empty.
75///
76/// # Safety
77/// The caller must ensure that the FFI call writes valid UTF-8 data to the buffer.
78#[allow(clippy::cast_possible_wrap)]
79pub unsafe fn ffi_string_from_buffer_or_empty<F>(buffer_size: usize, ffi_call: F) -> String
80where
81    F: FnOnce(*mut i8, isize) -> bool,
82{
83    ffi_string_from_buffer(buffer_size, ffi_call).unwrap_or_default()
84}
85
86/// Retrieves a string from an FFI function that returns an owned C string pointer.
87///
88/// This is more efficient than buffer-based retrieval as it avoids pre-allocation.
89/// The FFI function allocates the string (via `strdup`) and this function takes
90/// ownership and frees it.
91///
92/// # Arguments
93/// * `ffi_call` - A closure that returns an owned C string pointer (or null)
94///
95/// # Returns
96/// * `Some(String)` if the pointer was non-null and valid UTF-8
97/// * `None` if the pointer was null
98///
99/// # Safety
100/// The caller must ensure the returned pointer was allocated by Swift's `strdup`
101/// or equivalent, and that `sc_free_string` properly frees it.
102pub unsafe fn ffi_string_owned<F>(ffi_call: F) -> Option<String>
103where
104    F: FnOnce() -> *mut i8,
105{
106    let ptr = ffi_call();
107    if ptr.is_null() {
108        return None;
109    }
110    let c_str = CStr::from_ptr(ptr);
111    let result = c_str.to_string_lossy().to_string();
112    crate::ffi::sc_free_string(ptr);
113    if result.is_empty() {
114        None
115    } else {
116        Some(result)
117    }
118}
119
120/// Same as [`ffi_string_owned`] but returns an empty string on failure.
121///
122/// # Safety
123/// Same requirements as [`ffi_string_owned`].
124pub unsafe fn ffi_string_owned_or_empty<F>(ffi_call: F) -> String
125where
126    F: FnOnce() -> *mut i8,
127{
128    ffi_string_owned(ffi_call).unwrap_or_default()
129}