Skip to main content

ferroui_native/
frn_string.rs

1//! String marshalling: Rust strings handed to native code as COM string
2//! objects, and helpers to read the strings native code returns.
3
4use crate::interop::*;
5use ferroui_microcom::{ComPtr, HResult};
6use std::ffi::{c_void, CString};
7
8/// A Rust string exposed to native code as an `IFrnString`.
9///
10/// The UTF-8 bytes are owned by the object and NUL-terminated, so the
11/// pointer native code obtains stays valid until the last reference is
12/// released.
13pub struct FrnString {
14    string: String,
15    /// The NUL-terminated UTF-8 copy handed to native code.
16    native: Vec<u8>,
17}
18
19impl FrnString {
20    pub fn new(s: &str) -> Self {
21        let mut native = Vec::with_capacity(s.len() + 1);
22        native.extend_from_slice(s.as_bytes());
23        native.push(0);
24        Self { string: s.to_owned(), native }
25    }
26
27    /// The string.
28    pub fn string(&self) -> &str {
29        &self.string
30    }
31
32    /// The UTF-8 bytes of the string.
33    pub fn bytes(&self) -> &[u8] {
34        self.string.as_bytes()
35    }
36}
37
38impl IFrnStringImpl for FrnString {
39    fn pointer(&self) -> Result<*mut c_void, HResult> {
40        // An empty string has no native buffer in the reference
41        // implementation either.
42        if self.string.is_empty() {
43            return Ok(std::ptr::null_mut());
44        }
45        Ok(self.native.as_ptr() as *mut c_void)
46    }
47
48    fn length(&self) -> Result<i32, HResult> {
49        Ok(self.string.len() as i32)
50    }
51}
52
53/// A list of Rust strings exposed to native code as an `IFrnStringArray`.
54pub struct FrnStringArray {
55    items: Vec<(String, ComPtr<IFrnString>)>,
56}
57
58impl FrnStringArray {
59    pub fn new<S: AsRef<str>>(items: impl IntoIterator<Item = S>) -> Self {
60        Self {
61            items: items
62                .into_iter()
63                .map(|s| (s.as_ref().to_owned(), IFrnString::from_impl(FrnString::new(s.as_ref()))))
64                .collect(),
65        }
66    }
67
68    /// The strings of the array.
69    pub fn to_string_array(&self) -> Vec<String> {
70        self.items.iter().map(|(s, _)| s.clone()).collect()
71    }
72}
73
74impl IFrnStringArrayImpl for FrnStringArray {
75    fn get_count(&self) -> u32 {
76        self.items.len() as u32
77    }
78
79    fn get(&self, index: u32) -> Result<Option<ComPtr<IFrnString>>, HResult> {
80        match self.items.get(index as usize) {
81            Some((_, item)) => Ok(Some(item.clone())),
82            None => Err(HResult::INVALIDARG),
83        }
84    }
85}
86
87/// The UTF-8 bytes of a native string; empty when it has no buffer.
88pub fn frn_string_bytes(s: &IFrnString) -> Vec<u8> {
89    let (Ok(ptr), Ok(len)) = (s.pointer(), s.length()) else {
90        return Vec::new();
91    };
92    if ptr.is_null() || len <= 0 {
93        return Vec::new();
94    }
95    // SAFETY: an `IFrnString` owns `length` bytes at `pointer` for as long as
96    // it is alive; the bytes are copied before the borrow of `s` ends.
97    unsafe { std::slice::from_raw_parts(ptr as *const u8, len as usize) }.to_vec()
98}
99
100/// The content of a native string, or `None` when it has no buffer.
101pub fn frn_string_to_string(s: &IFrnString) -> Option<String> {
102    match s.pointer() {
103        Ok(ptr) if !ptr.is_null() => Some(String::from_utf8_lossy(&frn_string_bytes(s)).into_owned()),
104        _ => None,
105    }
106}
107
108/// The content of a native string array; strings without a buffer are
109/// empty.
110pub fn frn_string_array_to_vec(array: &IFrnStringArray) -> Vec<String> {
111    (0..array.get_count())
112        .map(|index| match array.get(index) {
113            Ok(Some(s)) => frn_string_to_string(&s).unwrap_or_default(),
114            _ => String::new(),
115        })
116        .collect()
117}
118
119/// A C string for a `const char*` parameter. Native code stops at the first
120/// NUL, so that is where the string is cut.
121pub(crate) fn to_c_string(s: &str) -> CString {
122    let end = s.bytes().position(|b| b == 0).unwrap_or(s.len());
123    CString::new(&s.as_bytes()[..end]).unwrap_or_default()
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn string_round_trips_through_its_com_interface() {
132        let s = IFrnString::from_impl(FrnString::new("héllo ⌘"));
133        assert_eq!(s.length(), Ok("héllo ⌘".len() as i32));
134        assert_eq!(frn_string_to_string(&s).as_deref(), Some("héllo ⌘"));
135        assert_eq!(frn_string_bytes(&s), "héllo ⌘".as_bytes());
136        // The buffer is NUL-terminated.
137        let ptr = s.pointer().unwrap() as *const u8;
138        // SAFETY: the buffer is `length + 1` bytes long.
139        assert_eq!(unsafe { *ptr.add("héllo ⌘".len()) }, 0);
140    }
141
142    #[test]
143    fn empty_string_has_no_buffer() {
144        let s = IFrnString::from_impl(FrnString::new(""));
145        assert!(s.pointer().unwrap().is_null());
146        assert_eq!(s.length(), Ok(0));
147        assert_eq!(frn_string_to_string(&s), None);
148        assert!(frn_string_bytes(&s).is_empty());
149    }
150
151    #[test]
152    fn string_array_round_trips() {
153        let source = FrnStringArray::new(["a", "", "ccc"]);
154        assert_eq!(source.to_string_array(), ["a", "", "ccc"]);
155        let array = IFrnStringArray::from_impl(source);
156        assert_eq!(array.get_count(), 3);
157        assert_eq!(frn_string_array_to_vec(&array), ["a", "", "ccc"]);
158        assert_eq!(array.get(3).unwrap_err(), HResult::INVALIDARG);
159    }
160
161    #[test]
162    fn c_strings_stop_at_the_first_nul() {
163        assert_eq!(to_c_string("title").as_bytes(), b"title");
164        assert_eq!(to_c_string("a\0b").as_bytes(), b"a");
165        assert_eq!(to_c_string("").as_bytes(), b"");
166    }
167}