use std::ffi::CStr;
pub const DEFAULT_BUFFER_SIZE: usize = 1024;
pub const SMALL_BUFFER_SIZE: usize = 256;
const STACK_BUFFER_BYTES: usize = 256;
#[allow(clippy::cast_possible_wrap)]
pub unsafe fn ffi_string_from_buffer<F>(buffer_size: usize, ffi_call: F) -> Option<String>
where
F: FnOnce(*mut i8, isize) -> bool,
{
if buffer_size <= STACK_BUFFER_BYTES {
let mut buffer = [0i8; STACK_BUFFER_BYTES];
let success = ffi_call(buffer.as_mut_ptr(), buffer_size as isize);
if !success {
return None;
}
return parse_buffer(&buffer[..buffer_size]);
}
let mut buffer = vec![0i8; buffer_size];
let success = ffi_call(buffer.as_mut_ptr(), buffer.len() as isize);
if !success {
return None;
}
parse_buffer(&buffer)
}
fn parse_buffer(buffer: &[i8]) -> Option<String> {
let bytes = unsafe { std::slice::from_raw_parts(buffer.as_ptr().cast::<u8>(), buffer.len()) };
let nul_pos = bytes.iter().position(|&b| b == 0)?;
let s = String::from_utf8_lossy(&bytes[..nul_pos]).into_owned();
if s.is_empty() {
None
} else {
Some(s)
}
}
#[allow(clippy::cast_possible_wrap)]
pub unsafe fn ffi_string_from_buffer_or_empty<F>(buffer_size: usize, ffi_call: F) -> String
where
F: FnOnce(*mut i8, isize) -> bool,
{
ffi_string_from_buffer(buffer_size, ffi_call).unwrap_or_default()
}
pub unsafe fn ffi_string_owned<F, Free>(ffi_call: F, free_fn: Free) -> Option<String>
where
F: FnOnce() -> *mut i8,
Free: FnOnce(*mut i8),
{
struct FreeGuard<F: FnOnce(*mut i8)> {
ptr: *mut i8,
free_fn: Option<F>,
}
impl<F: FnOnce(*mut i8)> Drop for FreeGuard<F> {
fn drop(&mut self) {
if !self.ptr.is_null() {
if let Some(free) = self.free_fn.take() {
free(self.ptr);
}
}
}
}
let ptr = ffi_call();
if ptr.is_null() {
return None;
}
let _guard = FreeGuard {
ptr,
free_fn: Some(free_fn),
};
let bytes = CStr::from_ptr(ptr).to_bytes();
if bytes.is_empty() {
return None;
}
Some(String::from_utf8_lossy(bytes).into_owned())
}
pub unsafe fn ffi_string_owned_or_empty<F, Free>(ffi_call: F, free_fn: Free) -> String
where
F: FnOnce() -> *mut i8,
Free: FnOnce(*mut i8),
{
ffi_string_owned(ffi_call, free_fn).unwrap_or_default()
}
#[inline]
pub unsafe fn take_owned_cstring<Free>(ptr: *mut i8, free_fn: Free) -> Option<String>
where
Free: FnOnce(*mut i8),
{
if ptr.is_null() {
return None;
}
ffi_string_owned(|| ptr, free_fn)
}
#[inline]
pub unsafe fn take_owned_cstring_c<Free>(
ptr: *mut core::ffi::c_char,
free_fn: Free,
) -> Option<String>
where
Free: FnOnce(*mut core::ffi::c_char),
{
if ptr.is_null() {
return None;
}
let bytes = CStr::from_ptr(ptr).to_bytes().to_vec();
free_fn(ptr);
if bytes.is_empty() {
None
} else {
Some(String::from_utf8_lossy(&bytes).into_owned())
}
}
#[cfg(test)]
mod take_owned_tests {
use super::*;
use std::ffi::CString;
#[test]
fn take_owned_cstring_null_returns_none() {
let freed = std::cell::Cell::new(false);
let result = unsafe { take_owned_cstring(core::ptr::null_mut(), |_| freed.set(true)) };
assert_eq!(result, None);
assert!(!freed.get(), "free fn must not be called for null ptr");
}
#[test]
fn take_owned_cstring_returns_value_and_frees() {
let s = CString::new("hello").unwrap();
let raw = s.into_raw();
let freed = std::cell::Cell::new(false);
let result = unsafe {
take_owned_cstring(raw.cast::<i8>(), |p| {
freed.set(true);
let _ = CString::from_raw(p.cast::<core::ffi::c_char>());
})
};
assert_eq!(result.as_deref(), Some("hello"));
assert!(freed.get(), "free fn must be called for non-null ptr");
}
#[test]
fn take_owned_cstring_c_returns_value_and_frees() {
let s = CString::new("world").unwrap();
let raw = s.into_raw();
let freed = std::cell::Cell::new(false);
let result = unsafe {
take_owned_cstring_c(raw, |p| {
freed.set(true);
let _ = CString::from_raw(p);
})
};
assert_eq!(result.as_deref(), Some("world"));
assert!(freed.get());
}
}