interoptopus/patterns/
string.rs

1//! Raw `*const char` pointer on C-level but ASCII `string` like in languages that support it.
2//!
3//! # Example
4//!
5//! In your library you can accept ASCII strings like this:
6//!
7//! ```
8//! use interoptopus::ffi_function;
9//! use interoptopus::patterns::string::AsciiPointer;
10//!
11//! #[ffi_function]
12//! #[no_mangle]
13//! pub extern "C" fn call_with_string(s: AsciiPointer)  {
14//!     //
15//! # s.as_str().unwrap();
16//! }
17//! ```
18//!
19//! Backends supporting this pattern might generate the equivalent to the following pseudo-code:
20//!
21//! ```csharp
22//! void call_with_string(string s);
23//! ```
24//!
25//! Backends not supporting this pattern, and C FFI, will see the equivalent of the following C code:
26//! ```c
27//! void call_with_string(uint8_t* s);
28//! ```
29//!
30use crate::lang::c::CType;
31use crate::lang::rust::CTypeInfo;
32use crate::patterns::TypePattern;
33use crate::Error;
34use std::ffi::CStr;
35use std::marker::PhantomData;
36use std::option::Option::None;
37use std::os::raw::c_char;
38use std::ptr::null;
39
40static EMPTY: &[u8] = b"\0";
41
42/// Represents a `*const char` on FFI level pointing to an `0x0` terminated ASCII string.
43///
44/// # Antipattern
45///
46/// It's discouraged to use [`FFIOption`](crate::patterns::option::FFIOption) with [`AsciiPointer`]
47/// and some backend might not generate proper bindings (like the C# backend).
48///
49/// Instead use [`AsciiPointer`] alone since it already has a pointer that's `null`able.
50/// In this case, [`AsciiPointer::as_c_str()`] will return [`None`] and [`AsciiPointer::as_str`]
51/// will return an [`Error::Null`].
52#[repr(transparent)]
53#[derive(Debug)]
54pub struct AsciiPointer<'a> {
55    ptr: *const c_char,
56    _phantom: PhantomData<&'a ()>,
57}
58
59impl<'a> Default for AsciiPointer<'a> {
60    fn default() -> Self {
61        Self {
62            ptr: null(),
63            _phantom: Default::default(),
64        }
65    }
66}
67
68impl<'a> AsciiPointer<'a> {
69    pub fn empty() -> Self {
70        Self {
71            ptr: EMPTY.as_ptr().cast(),
72            _phantom: Default::default(),
73        }
74    }
75
76    /// Create an AsciiPointer from a `&[u8]` slice reference.
77    ///
78    /// The parameter `ascii_with_nul` must contain nul (`0x0`), but it does not need to contain nul
79    /// at the end.
80    pub fn from_slice_with_nul(ascii_with_nul: &[u8]) -> Result<Self, Error> {
81        // Check we actually contain one `0x0`.
82        if !ascii_with_nul.contains(&0) {
83            return Err(Error::Ascii);
84        }
85
86        // Can't do this, C# treats ASCII as extended and bytes > 127 might show up, which
87        // is going to be a problem when returning a string we previously accepted.
88        //
89        // Any previous characters must not be extended ASCII.
90        // if ascii_with_nul.iter().any(|x| *x > 127) {
91        //     return Err(Error::Ascii);
92        // }
93
94        Ok(Self {
95            ptr: ascii_with_nul.as_ptr().cast(),
96            _phantom: Default::default(),
97        })
98    }
99
100    /// Create a pointer from a CStr.
101    pub fn from_cstr(cstr: &'a CStr) -> Self {
102        Self {
103            ptr: cstr.as_ptr(),
104            _phantom: Default::default(),
105        }
106    }
107
108    /// Create a [`CStr`] for the pointer.
109    pub fn as_c_str(&self) -> Option<&'a CStr> {
110        if self.ptr.is_null() {
111            None
112        } else {
113            // TODO: Write something about safety
114            unsafe { Some(CStr::from_ptr(self.ptr)) }
115        }
116    }
117
118    /// Attempts to return a Rust `str`.
119    pub fn as_str(&self) -> Result<&'a str, Error> {
120        Ok(self.as_c_str().ok_or(Error::Null)?.to_str()?)
121    }
122}
123
124unsafe impl<'a> CTypeInfo for AsciiPointer<'a> {
125    fn type_info() -> CType {
126        CType::Pattern(TypePattern::AsciiPointer)
127    }
128}
129
130#[cfg(test)]
131mod test {
132    use crate::patterns::string::AsciiPointer;
133    use std::ffi::CString;
134
135    #[test]
136    fn can_create() {
137        let s = "hello world";
138        let cstr = CString::new(s).unwrap();
139
140        let ptr_some = AsciiPointer::from_cstr(&cstr);
141
142        assert_eq!(s, ptr_some.as_str().unwrap());
143    }
144
145    #[test]
146    fn from_slice_with_nul_works() {
147        let s = b"hello\0world";
148        let ptr_some = AsciiPointer::from_slice_with_nul(&s[..]).unwrap();
149
150        assert_eq!("hello", ptr_some.as_str().unwrap());
151    }
152
153    #[test]
154    fn from_slice_with_nul_fails_if_not_nul() {
155        let s = b"hello world";
156        let ptr_some = AsciiPointer::from_slice_with_nul(&s[..]);
157
158        assert!(ptr_some.is_err());
159    }
160}