feat: improve cstr_ffi module
This commit is contained in:
+436
-42
@@ -1,19 +1,59 @@
|
|||||||
//! When calling this dynamic library with outside programs,
|
//! When calling Rust exported dynamic library with outside programs,
|
||||||
//! outer programs may usually need to fetch string resource produced by Rust code.
|
//! outer programs may usually need to fetch string resource produced by Rust code,
|
||||||
//! However it is impossible pass Rust string directly to outer program.
|
//! 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.
|
//! This module provides **thread independent** facilities for two directions:
|
||||||
//! 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.
|
//! - Output (Rust -> C/C++): push strings / string vectors / string views into a thread-local
|
||||||
//! So that outside program can utilize it like calling C/C++ library.
|
//! cache (or re-expose caller-owned memory for views) and hand stable pointers out.
|
||||||
//! The only thing that outer programs should note is that this string is volatile,
|
//! - Input (C/C++ -> Rust): parse foreign-provided strings into borrowed `&str` without copying.
|
||||||
//! once they get it, they must dupliate it immediately before any futher calling to this dynamic library.
|
//!
|
||||||
|
//! # 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::cell::RefCell;
|
||||||
use std::ffi::{c_char, CStr, CString};
|
use std::ffi::{CStr, CString, c_char};
|
||||||
|
use std::slice;
|
||||||
use thiserror::Error as TeError;
|
use thiserror::Error as TeError;
|
||||||
|
|
||||||
/// The type representing the raw pointer to immutable C-style NUL-terminated string.
|
/// The type representing a raw pointer to an immutable C/C++ character string.
|
||||||
pub type CStyleString = *const c_char;
|
///
|
||||||
|
/// 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
|
// region: Error
|
||||||
|
|
||||||
@@ -34,64 +74,299 @@ type Result<T> = std::result::Result<T, Error>;
|
|||||||
|
|
||||||
// endregion
|
// 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
|
// 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 {
|
struct StringCache {
|
||||||
msg: CString,
|
strings: Vec<Box<CString>>,
|
||||||
|
string_vecs: Vec<Box<CStringVec>>,
|
||||||
|
string_view_vecs: Vec<Box<CStringViewVec>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StringCache {
|
impl StringCache {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
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<()> {
|
/// Drop every slot held by this cache.
|
||||||
self.msg = CString::new(msg)?;
|
///
|
||||||
Ok(())
|
/// 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 {
|
/// Store one boxed NUL-terminated string and return its stable pointer.
|
||||||
self.msg.as_ptr()
|
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) {
|
/// Store one boxed string-vector slot.
|
||||||
self.msg = CString::new("").expect("empty string must be valid for CString");
|
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
|
// endregion
|
||||||
|
|
||||||
// region: Exposed Functions
|
// region: Output Slots Functions
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static STRING_CACHE: RefCell<StringCache> = RefCell::new(StringCache::new());
|
static STRING_CACHE: RefCell<StringCache> = RefCell::new(StringCache::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set thread local string exposed for C code.
|
/// Clear every single-string, string-vector and string-view-vector slot stored for the current
|
||||||
pub fn set_ffi_string(msg: &str) -> Result<()> {
|
/// thread.
|
||||||
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.
|
|
||||||
///
|
///
|
||||||
/// This function usually should be called at the beginning of every exposed C functions.
|
/// This function usually should be called at the beginning of every exposed C function so that
|
||||||
pub fn clear_ffi_string() {
|
/// each FFI call starts from an empty output cache.
|
||||||
STRING_CACHE.with(|e| {
|
///
|
||||||
e.borrow_mut().clear_msg();
|
/// 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.
|
/// Push one independent NUL-terminated string into the thread-local cache and return a stable
|
||||||
pub fn parse_ffi_string<'a>(ptr: CStyleString) -> Result<&'a str> {
|
/// 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() {
|
if ptr.is_null() {
|
||||||
Err(Error::NullPtr)
|
Err(Error::NullPtr)
|
||||||
} else {
|
} 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
|
// 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