feat: improve cstr_ffi module
This commit is contained in:
+436
-42
@@ -1,19 +1,59 @@
|
||||
//! When calling this dynamic library with outside programs,
|
||||
//! outer programs may usually need to fetch string resource produced by Rust code.
|
||||
//! However it is impossible pass Rust string directly to outer program.
|
||||
//! When calling Rust exported dynamic library with outside programs,
|
||||
//! outer programs may usually need to fetch string resource produced by Rust code,
|
||||
//! or push unchecked string resource to Rust code.
|
||||
//! However it is impossible to use Rust string directly to outer program.
|
||||
//!
|
||||
//! This module provide **thread independent** string cache for resolving this issue.
|
||||
//! When we need pass string to outer programs, we push that string into this string as C-like format,
|
||||
//! then return its pointer to outer program.
|
||||
//! So that outside program can utilize it like calling C/C++ library.
|
||||
//! The only thing that outer programs should note is that this string is volatile,
|
||||
//! once they get it, they must dupliate it immediately before any futher calling to this dynamic library.
|
||||
//! This module provides **thread independent** facilities for two directions:
|
||||
//!
|
||||
//! - Output (Rust -> C/C++): push strings / string vectors / string views into a thread-local
|
||||
//! cache (or re-expose caller-owned memory for views) and hand stable pointers out.
|
||||
//! - Input (C/C++ -> Rust): parse foreign-provided strings into borrowed `&str` without copying.
|
||||
//!
|
||||
//! # Output slots
|
||||
//!
|
||||
//! There are three independent kinds of output slots. A single FFI call may produce any number of
|
||||
//! each, in any combination:
|
||||
//!
|
||||
//! - **Single NUL-terminated string** via [`push_ffi_string`]. The string is *copied* into the
|
||||
//! cache; a stable [`CStrPtr`] is returned.
|
||||
//! - **String vector** (a list whose entries are NUL-terminated strings) via
|
||||
//! [`push_ffi_string_vec`] (nullptr-terminated list) or [`push_ffi_string_vec_with_len`]
|
||||
//! (count-terminated list). Entries are *copied* into the cache.
|
||||
//! - **String view** via [`push_ffi_string_view`] (single) or
|
||||
//! [`push_ffi_string_view_vec_with_len`] (vector of views). These are length-delimited and do
|
||||
//! **not** copy the string data; see "String view ownership" below.
|
||||
//!
|
||||
//! All cached slots stay valid until the next [`clear_ffi_strings`] call. Outer programs must
|
||||
//! duplicate any retrieved pointer immediately before issuing the next call into the library.
|
||||
//!
|
||||
//! # String view ownership
|
||||
//!
|
||||
//! Please read this chapter CAREFULLY!
|
||||
//!
|
||||
//! Functions in the `*_string_view*` family are designed exclusively for transmitting very long
|
||||
//! strings. To avoid an expensive copy they do **not** take ownership of and do **not** copy the
|
||||
//! string data. They only re-expose a pointer plus length into memory that the *caller* owns.
|
||||
//!
|
||||
//! This deliberately steps outside ordinary Rust lifetime guarantees and is extremely easy to
|
||||
//! misuse. The caller MUST guarantee that the underlying storage outlives every read performed by
|
||||
//! the foreign (C/C++) side. If the source string is dropped or moved before the foreign side
|
||||
//! finishes reading, the foreign side receives a dangling pointer and the behavior is undefined.
|
||||
//!
|
||||
//! It is highly suggested that use `SAFETY` annotation for this use case.
|
||||
use std::cell::RefCell;
|
||||
use std::ffi::{c_char, CStr, CString};
|
||||
use std::ffi::{CStr, CString, c_char};
|
||||
use std::slice;
|
||||
use thiserror::Error as TeError;
|
||||
|
||||
/// The type representing the raw pointer to immutable C-style NUL-terminated string.
|
||||
pub type CStyleString = *const c_char;
|
||||
/// The type representing a raw pointer to an immutable C/C++ character string.
|
||||
///
|
||||
/// This is a plain string-pointer alias used uniformly across the module. Whether the pointee is
|
||||
/// NUL-terminated or length-delimited depends on the specific function it is passed to or returned
|
||||
/// from; consult each function's documentation for the expected convention.
|
||||
pub type CStrPtr = *const c_char;
|
||||
|
||||
/// The type representing a pointer to a vector (array) of [`CStrPtr`] entries.
|
||||
pub type CStrVecPtr = *const CStrPtr;
|
||||
|
||||
// region: Error
|
||||
|
||||
@@ -34,64 +74,299 @@ type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
// endregion
|
||||
|
||||
// region: String View Type
|
||||
|
||||
/// A `#[repr(C)]` mirror of C++ `std::string_view`, exposed so that a *vector of string views*
|
||||
/// can be returned to the foreign side as one contiguous array.
|
||||
///
|
||||
/// A single string view is passed across the FFI boundary as two independent parameters
|
||||
/// (`ptr` and `len`); this struct exists solely so that many views can be packed into one array
|
||||
/// for [`push_ffi_string_view_vec_with_len`] / [`parse_ffi_string_view_vec_with_len`].
|
||||
///
|
||||
/// # Ownership
|
||||
///
|
||||
/// `ptr` does **not** point into memory owned by this crate. When produced by
|
||||
/// [`push_ffi_string_view_vec_with_len`] it points into caller-owned memory; see the module-level
|
||||
/// "String view ownership" section.
|
||||
#[repr(C)]
|
||||
pub struct CStringView {
|
||||
/// Pointer to the first byte of the viewed string. Not NUL-terminated in general and may
|
||||
/// contain interior NUL bytes.
|
||||
pub ptr: CStrPtr,
|
||||
/// Length in bytes of the viewed string.
|
||||
pub len: usize,
|
||||
}
|
||||
|
||||
/// The type representing a pointer to a vector (array) of [`CStringView`] entries.
|
||||
pub type CStrViewVecPtr = *const CStringView;
|
||||
|
||||
// endregion
|
||||
|
||||
// region: String Cache for Exposing
|
||||
|
||||
/// One independent string-vector slot.
|
||||
///
|
||||
/// `items` owns the actual `CString` data, each individually boxed so the pointers stay stable
|
||||
/// even when `items` grows. `ptrs` is the parallel pointer array consumed by C/C++; for the
|
||||
/// nullptr-terminated variant a trailing null pointer is appended to `ptrs`.
|
||||
struct CStringVec {
|
||||
items: Vec<Box<CString>>,
|
||||
ptrs: Vec<CStrPtr>,
|
||||
}
|
||||
|
||||
impl CStringVec {
|
||||
/// Build a string vector from the given entries.
|
||||
///
|
||||
/// A trailing null entry is always appended to the pointer array. This single layout serves
|
||||
/// both consumers: the nullptr-terminated form scans until the null, and the count-terminated
|
||||
/// form simply reads [`CStringVec::item_count`] entries and ignores the trailing null.
|
||||
///
|
||||
/// Capacity arithmetic uses checked addition because reserving `items.len() + 1` slots could
|
||||
/// in theory overflow on an absurdly large input; such a case is treated as unrecoverable.
|
||||
fn new(items: &[&str]) -> Result<Self> {
|
||||
let ptrs_cap = items
|
||||
.len()
|
||||
.checked_add(1)
|
||||
.expect("CStringVec capacity overflow");
|
||||
let mut obj = Self {
|
||||
items: Vec::with_capacity(items.len()),
|
||||
ptrs: Vec::with_capacity(ptrs_cap),
|
||||
};
|
||||
for s in items {
|
||||
let boxed = Box::new(CString::new(*s)?);
|
||||
obj.ptrs.push(boxed.as_ptr());
|
||||
obj.items.push(boxed);
|
||||
}
|
||||
obj.ptrs.push(std::ptr::null());
|
||||
Ok(obj)
|
||||
}
|
||||
|
||||
/// Return the head pointer of the parallel pointer array, for consumption by C/C++.
|
||||
fn head_ptr(&self) -> CStrVecPtr {
|
||||
self.ptrs.as_ptr()
|
||||
}
|
||||
|
||||
/// Return the number of real string entries (excludes any trailing null terminator).
|
||||
fn item_count(&self) -> usize {
|
||||
self.items.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// One independent string-view-vector slot.
|
||||
///
|
||||
/// Only the *container* (`views`) is owned by the cache; each [`CStringView::ptr`] inside points
|
||||
/// into caller-owned memory and is not managed here.
|
||||
struct CStringViewVec {
|
||||
views: Vec<CStringView>,
|
||||
}
|
||||
|
||||
impl CStringViewVec {
|
||||
/// Build a string-view vector that re-exposes the caller-owned input strings without
|
||||
/// copying their data.
|
||||
fn new(items: &[&str]) -> Self {
|
||||
let views: Vec<CStringView> = items
|
||||
.iter()
|
||||
.map(|s| CStringView {
|
||||
ptr: s.as_ptr() as CStrPtr,
|
||||
len: s.len(),
|
||||
})
|
||||
.collect();
|
||||
Self { views }
|
||||
}
|
||||
|
||||
/// Return the head pointer of the view array, for consumption by C/C++.
|
||||
fn head_ptr(&self) -> CStrViewVecPtr {
|
||||
self.views.as_ptr()
|
||||
}
|
||||
|
||||
/// Return the number of view entries.
|
||||
fn item_count(&self) -> usize {
|
||||
self.views.len()
|
||||
}
|
||||
}
|
||||
|
||||
struct StringCache {
|
||||
msg: CString,
|
||||
strings: Vec<Box<CString>>,
|
||||
string_vecs: Vec<Box<CStringVec>>,
|
||||
string_view_vecs: Vec<Box<CStringViewVec>>,
|
||||
}
|
||||
|
||||
impl StringCache {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
msg: CString::new("").expect("empty string must be valid for CString"),
|
||||
strings: Vec::new(),
|
||||
string_vecs: Vec::new(),
|
||||
string_view_vecs: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_msg(&mut self, msg: &str) -> Result<()> {
|
||||
self.msg = CString::new(msg)?;
|
||||
Ok(())
|
||||
/// Drop every slot held by this cache.
|
||||
///
|
||||
/// For string-view-vector slots only the *container* is dropped; the caller-owned string data
|
||||
/// behind each [`CStringView::ptr`] is not (and cannot be) touched here.
|
||||
fn clear(&mut self) {
|
||||
self.strings.clear();
|
||||
self.string_vecs.clear();
|
||||
self.string_view_vecs.clear();
|
||||
}
|
||||
|
||||
pub fn get_msg(&self) -> CStyleString {
|
||||
self.msg.as_ptr()
|
||||
/// Store one boxed NUL-terminated string and return its stable pointer.
|
||||
fn push_string(&mut self, s: Box<CString>) -> CStrPtr {
|
||||
let ptr = s.as_ptr();
|
||||
self.strings.push(s);
|
||||
ptr
|
||||
}
|
||||
|
||||
pub fn clear_msg(&mut self) {
|
||||
self.msg = CString::new("").expect("empty string must be valid for CString");
|
||||
/// Store one boxed string-vector slot.
|
||||
fn push_string_vec(&mut self, sv: Box<CStringVec>) {
|
||||
self.string_vecs.push(sv);
|
||||
}
|
||||
|
||||
/// Store one boxed string-view-vector slot.
|
||||
fn push_string_view_vec(&mut self, svv: Box<CStringViewVec>) {
|
||||
self.string_view_vecs.push(svv);
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Exposed Functions
|
||||
// region: Output Slots Functions
|
||||
|
||||
thread_local! {
|
||||
static STRING_CACHE: RefCell<StringCache> = RefCell::new(StringCache::new());
|
||||
}
|
||||
|
||||
/// Set thread local string exposed for C code.
|
||||
pub fn set_ffi_string(msg: &str) -> Result<()> {
|
||||
STRING_CACHE.with(|e| {
|
||||
e.borrow_mut().set_msg(msg)
|
||||
})
|
||||
}
|
||||
|
||||
/// Get const pointer to thread local string exposed for C code.
|
||||
pub fn get_ffi_string() -> CStyleString {
|
||||
STRING_CACHE.with(|e| e.borrow().get_msg())
|
||||
}
|
||||
|
||||
/// Clear thread local string exposed for C code.
|
||||
/// Clear every single-string, string-vector and string-view-vector slot stored for the current
|
||||
/// thread.
|
||||
///
|
||||
/// This function usually should be called at the beginning of every exposed C functions.
|
||||
pub fn clear_ffi_string() {
|
||||
STRING_CACHE.with(|e| {
|
||||
e.borrow_mut().clear_msg();
|
||||
});
|
||||
/// This function usually should be called at the beginning of every exposed C function so that
|
||||
/// each FFI call starts from an empty output cache.
|
||||
///
|
||||
/// Note: clearing a string-view-vector slot only drops the *container* held by this crate; the
|
||||
/// caller-owned string data behind each [`CStringView::ptr`] is not (and cannot be) touched here.
|
||||
pub fn clear_ffi_strings() {
|
||||
STRING_CACHE.with(|c| c.borrow_mut().clear());
|
||||
}
|
||||
|
||||
/// Parse string given by C code into Rust string.
|
||||
pub fn parse_ffi_string<'a>(ptr: CStyleString) -> Result<&'a str> {
|
||||
/// Push one independent NUL-terminated string into the thread-local cache and return a stable
|
||||
/// pointer to it.
|
||||
///
|
||||
/// The string is **copied** into the cache. The returned pointer stays valid until the next
|
||||
/// [`clear_ffi_strings`] call; the foreign side must duplicate it before the next FFI call.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `s` contains an interior NUL byte.
|
||||
pub fn push_ffi_string(s: &str) -> Result<CStrPtr> {
|
||||
let boxed = Box::new(CString::new(s)?);
|
||||
STRING_CACHE.with(|c| Ok(c.borrow_mut().push_string(boxed)))
|
||||
}
|
||||
|
||||
/// Push one independent string vector (NUL-terminated entries, **nullptr-terminated list**) into
|
||||
/// the thread-local cache and return a stable pointer to the pointer array.
|
||||
///
|
||||
/// The returned array has `items.len() + 1` entries whose final entry is a null pointer acting as
|
||||
/// the list terminator. The foreign side scans forward until it meets the null entry. An empty
|
||||
/// `items` yields a one-entry array containing only the terminating null (a valid empty list).
|
||||
///
|
||||
/// All string contents are copied into the cache and stay valid until the next
|
||||
/// [`clear_ffi_strings`] call.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if any entry contains an interior NUL byte.
|
||||
pub fn push_ffi_string_vec(items: &[&str]) -> Result<CStrVecPtr> {
|
||||
let sv = Box::new(CStringVec::new(items)?);
|
||||
let head = sv.head_ptr();
|
||||
STRING_CACHE.with(|c| c.borrow_mut().push_string_vec(sv));
|
||||
Ok(head)
|
||||
}
|
||||
|
||||
/// Push one independent string vector (NUL-terminated entries, **count-terminated list**) into the
|
||||
/// thread-local cache and return a stable pointer to the pointer array together with the entry
|
||||
/// count.
|
||||
///
|
||||
/// The returned array contains exactly `count` entries for the foreign side to read (plus an
|
||||
/// internal trailing null terminator that is ignored). The foreign side must rely on the returned
|
||||
/// count instead of scanning for a null. The returned pointer is always valid: the trailing null
|
||||
/// guarantees a non-empty allocation, so even when `count` is 0 the pointer is non-dangling.
|
||||
///
|
||||
/// All string contents are copied into the cache and stay valid until the next
|
||||
/// [`clear_ffi_strings`] call.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if any entry contains an interior NUL byte.
|
||||
pub fn push_ffi_string_vec_with_len(items: &[&str]) -> Result<(CStrVecPtr, usize)> {
|
||||
let sv = Box::new(CStringVec::new(items)?);
|
||||
let head = sv.head_ptr();
|
||||
let count = sv.item_count();
|
||||
STRING_CACHE.with(|c| c.borrow_mut().push_string_vec(sv));
|
||||
Ok((head, count))
|
||||
}
|
||||
|
||||
/// Re-expose a single string as a `(ptr, len)` pair **without copying and without storing anything
|
||||
/// in the cache**.
|
||||
///
|
||||
/// This is intended exclusively for very long strings. The returned pointer points directly into
|
||||
/// the memory of `s`; no data is duplicated and no slot is created in the cache.
|
||||
///
|
||||
/// # Ownership / Lifetime -- READ CAREFULLY
|
||||
///
|
||||
/// The caller owns the underlying storage and **must** keep it alive -- unmoved and undropped --
|
||||
/// for as long as the foreign (C/C++) side keeps reading through the returned pointer. If `s`
|
||||
/// (or the buffer backing it) is released before the foreign side finishes reading, the foreign
|
||||
/// side observes a dangling pointer and the behavior is undefined. This crate does not and cannot
|
||||
/// enforce this constraint.
|
||||
///
|
||||
/// Because the result is length-delimited, `s` is permitted to contain interior NUL bytes; no
|
||||
/// scanning is performed. This function cannot fail and returns no `Result`.
|
||||
pub fn push_ffi_string_view(s: &str) -> (CStrPtr, usize) {
|
||||
(s.as_ptr() as CStrPtr, s.len())
|
||||
}
|
||||
|
||||
/// Build a vector of [`CStringView`] over the given strings and return a stable pointer to that
|
||||
/// array together with the entry count.
|
||||
///
|
||||
/// Only the *container* array is stored in the cache (so the returned [`CStrViewVecPtr`] stays
|
||||
/// valid until the next [`clear_ffi_strings`] call); the string data behind each
|
||||
/// [`CStringView::ptr`] is **not** copied and **not** owned by this crate.
|
||||
///
|
||||
/// # Ownership / Lifetime -- READ CAREFULLY
|
||||
///
|
||||
/// Each entry's `ptr` points directly into the memory of the corresponding input `&str`. The
|
||||
/// caller **must** keep every source string alive -- unmoved and undropped -- for as long as the
|
||||
/// foreign (C/C++) side reads through the array. If any source string is released before the
|
||||
/// foreign side finishes, the foreign side observes dangling pointers and the behavior is
|
||||
/// undefined. This crate does not and cannot enforce this constraint.
|
||||
///
|
||||
/// Because each entry is length-delimited, the inputs are permitted to contain interior NUL
|
||||
/// bytes; no scanning is performed. This function cannot fail and returns no `Result`.
|
||||
///
|
||||
/// When `count` is 0, the returned pointer may be a dangling pointer valid only for zero-sized
|
||||
/// reads (per `Vec::as_ptr` for an empty allocation). The foreign side must drive consumption off
|
||||
/// `count` and must not dereference the pointer when `count` is 0.
|
||||
pub fn push_ffi_string_view_vec_with_len(items: &[&str]) -> (CStrViewVecPtr, usize) {
|
||||
let svv = Box::new(CStringViewVec::new(items));
|
||||
let head = svv.head_ptr();
|
||||
let count = svv.item_count();
|
||||
STRING_CACHE.with(|c| c.borrow_mut().push_string_view_vec(svv));
|
||||
(head, count)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Input Slots Functions
|
||||
|
||||
/// Parse a NUL-terminated C/C++ string into a borrowed Rust `&str`.
|
||||
///
|
||||
/// The returned slice borrows the foreign memory; no allocation is performed. It is only valid
|
||||
/// while the caller keeps the original foreign buffer alive.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `ptr` is null or if the content is not valid UTF-8.
|
||||
pub fn parse_ffi_string<'a>(ptr: CStrPtr) -> Result<&'a str> {
|
||||
if ptr.is_null() {
|
||||
Err(Error::NullPtr)
|
||||
} else {
|
||||
@@ -100,4 +375,123 @@ pub fn parse_ffi_string<'a>(ptr: CStyleString) -> Result<&'a str> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a **nullptr-terminated** vector of NUL-terminated C/C++ strings into borrowed Rust
|
||||
/// `&str`s.
|
||||
///
|
||||
/// `ptrs` points at an array of [`CStrPtr`] terminated by a null entry. The function scans
|
||||
/// forward until it meets the null terminator. The returned slices borrow the foreign memory and
|
||||
/// are only valid while the caller keeps the original buffers alive. No allocation is performed
|
||||
/// for the string contents themselves (a `Vec` of references is returned).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `ptrs` is null or if any entry is not valid UTF-8.
|
||||
pub fn parse_ffi_string_vec<'a>(ptrs: CStrVecPtr) -> Result<Vec<&'a str>> {
|
||||
if ptrs.is_null() {
|
||||
return Err(Error::NullPtr);
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
let mut idx = 0usize;
|
||||
loop {
|
||||
let entry = unsafe { *ptrs.add(idx) };
|
||||
if entry.is_null() {
|
||||
break;
|
||||
}
|
||||
out.push(parse_ffi_string(entry)?);
|
||||
idx += 1;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Parse a **count-terminated** vector of NUL-terminated C/C++ strings into borrowed Rust `&str`s.
|
||||
///
|
||||
/// `ptrs` points at an array of exactly `count` [`CStrPtr`] entries (no terminator). The returned
|
||||
/// slices borrow the foreign memory and are only valid while the caller keeps the original buffers
|
||||
/// alive. No allocation is performed for the string contents themselves (a `Vec` of references is
|
||||
/// returned).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `ptrs` is null while `count > 0`, or if any entry is not valid UTF-8. When
|
||||
/// `count` is 0, a null `ptrs` is accepted and an empty vector is returned.
|
||||
pub fn parse_ffi_string_vec_with_len<'a>(
|
||||
ptrs: CStrVecPtr,
|
||||
count: usize,
|
||||
) -> Result<Vec<&'a str>> {
|
||||
if count == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if ptrs.is_null() {
|
||||
return Err(Error::NullPtr);
|
||||
}
|
||||
let mut out = Vec::with_capacity(count);
|
||||
for idx in 0..count {
|
||||
let entry = unsafe { *ptrs.add(idx) };
|
||||
out.push(parse_ffi_string(entry)?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Parse a length-delimited string view (C++ `std::string_view` semantics) given as an independent
|
||||
/// pointer and length into a borrowed Rust `&str`.
|
||||
///
|
||||
/// Unlike [`parse_ffi_string`], the buffer is not required to be NUL-terminated, so interior NUL
|
||||
/// bytes are permitted. A default-constructed C++ `std::string_view` (null pointer, zero length)
|
||||
/// is interpreted as an empty string.
|
||||
///
|
||||
/// # Lifetime
|
||||
///
|
||||
/// The returned slice borrows the foreign memory and is only valid while the caller keeps the
|
||||
/// original foreign buffer alive. No allocation is performed.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `ptr` is null while `len > 0`, or if the content is not valid UTF-8.
|
||||
pub fn parse_ffi_string_view<'a>(ptr: CStrPtr, len: usize) -> Result<&'a str> {
|
||||
if ptr.is_null() {
|
||||
return if len == 0 {
|
||||
Ok("")
|
||||
} else {
|
||||
Err(Error::NullPtr)
|
||||
};
|
||||
}
|
||||
let bytes = unsafe { slice::from_raw_parts(ptr as *const u8, len) };
|
||||
Ok(std::str::from_utf8(bytes)?)
|
||||
}
|
||||
|
||||
/// Parse a **count-terminated** vector of [`CStringView`] entries into borrowed Rust `&str`s.
|
||||
///
|
||||
/// `views` points at an array of exactly `count` [`CStringView`] entries. Each entry is treated
|
||||
/// with `std::string_view` semantics (pointer + length, no NUL requirement, interior NUL
|
||||
/// permitted). An entry with a null pointer and zero length is interpreted as an empty string.
|
||||
///
|
||||
/// # Lifetime
|
||||
///
|
||||
/// The returned slices borrow the foreign memory behind each view and are only valid while the
|
||||
/// caller keeps the original buffers alive. No allocation is performed for the string contents
|
||||
/// themselves (a `Vec` of references is returned).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if `views` is null while `count > 0`, if any entry has a null pointer with
|
||||
/// non-zero length, or if any entry is not valid UTF-8. When `count` is 0, a null `views` is
|
||||
/// accepted and an empty vector is returned.
|
||||
pub fn parse_ffi_string_view_vec_with_len<'a>(
|
||||
views: CStrViewVecPtr,
|
||||
count: usize,
|
||||
) -> Result<Vec<&'a str>> {
|
||||
if count == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if views.is_null() {
|
||||
return Err(Error::NullPtr);
|
||||
}
|
||||
let mut out = Vec::with_capacity(count);
|
||||
for idx in 0..count {
|
||||
let view = unsafe { &*views.add(idx) };
|
||||
out.push(parse_ffi_string_view(view.ptr, view.len)?);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
//! Integration tests for the `cstr_ffi` module.
|
||||
//!
|
||||
//! These exercise the public API only. The thread-local cache is reset at the start of every test
|
||||
//! to keep cases independent. Tests that hand raw pointers back into the parse functions do so
|
||||
//! immediately, within the scope where the backing storage is guaranteed to live.
|
||||
|
||||
use sarasacw_omrf::cstr_ffi;
|
||||
|
||||
fn cptr_of(bytes: &[u8]) -> cstr_ffi::CStrPtr {
|
||||
bytes.as_ptr() as cstr_ffi::CStrPtr
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_single_string_round_trip() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
let ptr = cstr_ffi::push_ffi_string("hello").unwrap();
|
||||
assert!(!ptr.is_null());
|
||||
assert_eq!(cstr_ffi::parse_ffi_string(ptr).unwrap(), "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pushed_strings_remain_valid_after_more_pushes() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
let p1 = cstr_ffi::push_ffi_string("first").unwrap();
|
||||
// Force the internal Vec to reallocate by pushing many entries.
|
||||
let later: Vec<_> = (0..64).map(|_| cstr_ffi::push_ffi_string("filler").unwrap()).collect();
|
||||
let p2 = cstr_ffi::push_ffi_string("second").unwrap();
|
||||
let p3 = cstr_ffi::push_ffi_string("third").unwrap();
|
||||
assert_eq!(cstr_ffi::parse_ffi_string(p1).unwrap(), "first");
|
||||
assert_eq!(cstr_ffi::parse_ffi_string(p2).unwrap(), "second");
|
||||
assert_eq!(cstr_ffi::parse_ffi_string(p3).unwrap(), "third");
|
||||
assert_eq!(cstr_ffi::parse_ffi_string(later[40]).unwrap(), "filler");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_ffi_string_rejects_interior_nul() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
assert!(cstr_ffi::push_ffi_string("a\x00b").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ffi_string_null_ptr_errors() {
|
||||
assert!(cstr_ffi::parse_ffi_string(std::ptr::null()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ffi_string_invalid_utf8_errors() {
|
||||
let mut buf: Vec<u8> = vec![b'h', b'i', 0xFF];
|
||||
buf.push(0);
|
||||
let ptr = cptr_of(&buf);
|
||||
assert!(cstr_ffi::parse_ffi_string(ptr).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_string_vec_nullptr_terminated_round_trip() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
let head = cstr_ffi::push_ffi_string_vec(&["a", "bb", "ccc"]).unwrap();
|
||||
let collected = cstr_ffi::parse_ffi_string_vec(head).unwrap();
|
||||
assert_eq!(collected, vec!["a", "bb", "ccc"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_string_vec_empty_is_valid_empty_list() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
let head = cstr_ffi::push_ffi_string_vec(&[]).unwrap();
|
||||
let collected = cstr_ffi::parse_ffi_string_vec(head).unwrap();
|
||||
assert!(collected.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_string_vec_rejects_interior_nul() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
assert!(cstr_ffi::push_ffi_string_vec(&["ok", "ba\x00d"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ffi_string_vec_null_ptr_errors() {
|
||||
assert!(cstr_ffi::parse_ffi_string_vec(std::ptr::null()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_string_vec_with_len_round_trip() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
let (head, count) = cstr_ffi::push_ffi_string_vec_with_len(&["x", "yy", "zzz"]).unwrap();
|
||||
assert_eq!(count, 3);
|
||||
let collected = cstr_ffi::parse_ffi_string_vec_with_len(head, count).unwrap();
|
||||
assert_eq!(collected, vec!["x", "yy", "zzz"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_string_vec_with_len_empty() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
let (head, count) = cstr_ffi::push_ffi_string_vec_with_len(&[]).unwrap();
|
||||
assert_eq!(count, 0);
|
||||
let collected = cstr_ffi::parse_ffi_string_vec_with_len(head, count).unwrap();
|
||||
assert!(collected.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ffi_string_vec_with_len_null_zero_len_is_empty() {
|
||||
assert!(
|
||||
cstr_ffi::parse_ffi_string_vec_with_len(std::ptr::null(), 0)
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ffi_string_vec_with_len_null_nonzero_errors() {
|
||||
assert!(cstr_ffi::parse_ffi_string_vec_with_len(std::ptr::null(), 1).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_string_vec_slots_coexist() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
let single = cstr_ffi::push_ffi_string("solo").unwrap();
|
||||
let (h1, c1) = cstr_ffi::push_ffi_string_vec_with_len(&["l1a", "l1b"]).unwrap();
|
||||
let h2 = cstr_ffi::push_ffi_string_vec(&["l2a", "l2b", "l2c"]).unwrap();
|
||||
assert_eq!(cstr_ffi::parse_ffi_string(single).unwrap(), "solo");
|
||||
assert_eq!(
|
||||
cstr_ffi::parse_ffi_string_vec_with_len(h1, c1).unwrap(),
|
||||
vec!["l1a", "l1b"]
|
||||
);
|
||||
assert_eq!(
|
||||
cstr_ffi::parse_ffi_string_vec(h2).unwrap(),
|
||||
vec!["l2a", "l2b", "l2c"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_ffi_string_view_points_into_source() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
let owned = String::from("a long string that we keep alive");
|
||||
let (ptr, len) = cstr_ffi::push_ffi_string_view(&owned);
|
||||
assert_eq!(len, owned.len());
|
||||
assert_eq!(ptr as *const u8, owned.as_ptr());
|
||||
let parsed = cstr_ffi::parse_ffi_string_view(ptr, len).unwrap();
|
||||
assert_eq!(parsed, owned);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_view_allows_interior_nul() {
|
||||
let src: &str = "ab\x00cd";
|
||||
let (ptr, len) = cstr_ffi::push_ffi_string_view(src);
|
||||
assert_eq!(len, 5);
|
||||
let parsed = cstr_ffi::parse_ffi_string_view(ptr, len).unwrap();
|
||||
assert_eq!(parsed.as_bytes(), src.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ffi_string_view_null_zero_is_empty() {
|
||||
assert_eq!(cstr_ffi::parse_ffi_string_view(std::ptr::null(), 0).unwrap(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ffi_string_view_null_nonzero_errors() {
|
||||
assert!(cstr_ffi::parse_ffi_string_view(std::ptr::null(), 3).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ffi_string_view_invalid_utf8_errors() {
|
||||
let bad: [u8; 2] = [b'a', 0xFF];
|
||||
let ptr = cptr_of(&bad);
|
||||
assert!(cstr_ffi::parse_ffi_string_view(ptr, 2).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_string_view_vec_with_len_round_trip() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
let a = String::from("alpha");
|
||||
let b = String::from("beta");
|
||||
let c = String::from("gamma");
|
||||
let (head, count) = cstr_ffi::push_ffi_string_view_vec_with_len(&[&a, &b, &c]);
|
||||
assert_eq!(count, 3);
|
||||
let parsed = cstr_ffi::parse_ffi_string_view_vec_with_len(head, count).unwrap();
|
||||
assert_eq!(parsed, vec!["alpha", "beta", "gamma"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_string_view_vec_with_len_empty() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
let (head, count) = cstr_ffi::push_ffi_string_view_vec_with_len(&[]);
|
||||
assert_eq!(count, 0);
|
||||
// count is 0, so head is ignored (may be dangling); parsing must yield an empty vector.
|
||||
let collected = cstr_ffi::parse_ffi_string_view_vec_with_len(head, count).unwrap();
|
||||
assert!(collected.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_view_vec_allows_interior_nul_entries() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
let s1: &str = "x\x00y";
|
||||
let s2: &str = "p\x00\x00q";
|
||||
let (head, count) = cstr_ffi::push_ffi_string_view_vec_with_len(&[s1, s2]);
|
||||
assert_eq!(count, 2);
|
||||
let parsed = cstr_ffi::parse_ffi_string_view_vec_with_len(head, count).unwrap();
|
||||
assert_eq!(parsed[0].as_bytes(), s1.as_bytes());
|
||||
assert_eq!(parsed[1].as_bytes(), s2.as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ffi_string_view_vec_null_zero_len_is_empty() {
|
||||
assert!(cstr_ffi::parse_ffi_string_view_vec_with_len(std::ptr::null(), 0)
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ffi_string_view_vec_null_nonzero_errors() {
|
||||
assert!(cstr_ffi::parse_ffi_string_view_vec_with_len(std::ptr::null(), 1).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_makes_cache_reusable() {
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
let _first = cstr_ffi::push_ffi_string("before-clear").unwrap();
|
||||
cstr_ffi::clear_ffi_strings();
|
||||
let after = cstr_ffi::push_ffi_string("after-clear").unwrap();
|
||||
assert_eq!(cstr_ffi::parse_ffi_string(after).unwrap(), "after-clear");
|
||||
}
|
||||
Reference in New Issue
Block a user