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}