nautilus_core/ffi/string.rs
1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3// https://poseitrader.io
4//
5// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6// You may not use this file except in compliance with the License.
7// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::{
17 ffi::{CStr, CString, c_char},
18 str,
19};
20
21#[cfg(feature = "python")]
22use pyo3::{Bound, Python, ffi};
23use ustr::Ustr;
24
25#[cfg(feature = "python")]
26/// Returns an owned string from a valid Python object pointer.
27///
28/// # Safety
29///
30/// Assumes `ptr` is borrowed from a valid Python UTF-8 `str`.
31///
32/// # Panics
33///
34/// Panics if `ptr` is null.
35#[must_use]
36pub unsafe fn pystr_to_string(ptr: *mut ffi::PyObject) -> String {
37 assert!(!ptr.is_null(), "`ptr` was NULL");
38 Python::with_gil(|py| unsafe { Bound::from_borrowed_ptr(py, ptr).to_string() })
39}
40
41/// Convert a C string pointer into an owned `String`.
42///
43/// # Safety
44///
45/// Assumes `ptr` is a valid C string pointer.
46///
47/// # Panics
48///
49/// Panics if `ptr` is null.
50#[must_use]
51pub unsafe fn cstr_to_ustr(ptr: *const c_char) -> Ustr {
52 assert!(!ptr.is_null(), "`ptr` was NULL");
53 let cstr = unsafe { CStr::from_ptr(ptr) };
54 Ustr::from(cstr.to_str().expect("CStr::from_ptr failed"))
55}
56
57/// Convert a C string pointer into bytes.
58///
59/// # Safety
60///
61/// - Assumes `ptr` is a valid C string pointer.
62/// - The returned slice is only valid while the original C string remains allocated.
63/// - Caller must ensure the C string outlives any usage of the returned slice.
64///
65/// # Panics
66///
67/// Panics if `ptr` is null.
68///
69/// # Note
70///
71/// This function is designed for immediate consumption within FFI calls.
72/// Do not store the returned slice for use beyond the current function scope.
73#[must_use]
74pub unsafe fn cstr_to_bytes(ptr: *const c_char) -> &'static [u8] {
75 assert!(!ptr.is_null(), "`ptr` was NULL");
76 let cstr = unsafe { CStr::from_ptr(ptr) };
77 cstr.to_bytes()
78}
79
80/// Convert a C string pointer into an owned `Option<Ustr>`.
81///
82/// # Safety
83///
84/// Assumes `ptr` is a valid C string pointer or NULL.
85///
86/// # Panics
87///
88/// Panics if `ptr` is null.
89#[must_use]
90pub unsafe fn optional_cstr_to_ustr(ptr: *const c_char) -> Option<Ustr> {
91 if ptr.is_null() {
92 None
93 } else {
94 Some(unsafe { cstr_to_ustr(ptr) })
95 }
96}
97
98/// Convert a C string pointer into a string slice.
99///
100/// # Safety
101///
102/// - Assumes `ptr` is a valid C string pointer.
103/// - The returned slice is only valid while the original C string remains allocated.
104/// - Caller must ensure the C string outlives any usage of the returned slice.
105///
106/// # Panics
107///
108/// Panics if `ptr` is null or contains invalid UTF-8.
109///
110/// # Note
111///
112/// This function is designed for immediate consumption within FFI calls.
113/// Do not store the returned slice for use beyond the current function scope.
114#[must_use]
115pub unsafe fn cstr_as_str(ptr: *const c_char) -> &'static str {
116 assert!(!ptr.is_null(), "`ptr` was NULL");
117 let cstr = unsafe { CStr::from_ptr(ptr) };
118 cstr.to_str().expect("CStr::from_ptr failed")
119}
120
121/// Convert a C string pointer into an `Option<&str>`.
122///
123/// # Safety
124///
125/// - Assumes `ptr` is a valid C string pointer or NULL.
126/// - The returned slice is only valid while the original C string remains allocated.
127/// - Caller must ensure the C string outlives any usage of the returned slice.
128///
129/// # Panics
130///
131/// Panics if `ptr` is not null but contains invalid UTF-8.
132///
133/// # Note
134///
135/// This function is designed for immediate consumption within FFI calls.
136/// Do not store the returned slice for use beyond the current function scope.
137#[must_use]
138pub unsafe fn optional_cstr_to_str(ptr: *const c_char) -> Option<&'static str> {
139 if ptr.is_null() {
140 None
141 } else {
142 Some(unsafe { cstr_as_str(ptr) })
143 }
144}
145
146/// Create a C string pointer to newly allocated memory from a [`&str`].
147///
148/// # Panics
149///
150/// Panics if the input string contains interior null bytes.
151#[must_use]
152pub fn str_to_cstr(s: &str) -> *const c_char {
153 CString::new(s).expect("CString::new failed").into_raw()
154}
155
156/// Drops the C string memory at the pointer.
157///
158/// # Safety
159///
160/// Assumes `ptr` is a valid C string pointer.
161///
162/// # Panics
163///
164/// Panics if `ptr` is null.
165#[unsafe(no_mangle)]
166pub unsafe extern "C" fn cstr_drop(ptr: *const c_char) {
167 assert!(!ptr.is_null(), "`ptr` was NULL");
168 let cstring = unsafe { CString::from_raw(ptr.cast_mut()) };
169 drop(cstring);
170}
171
172////////////////////////////////////////////////////////////////////////////////
173// Tests
174////////////////////////////////////////////////////////////////////////////////
175#[cfg(test)]
176mod tests {
177 #[cfg(feature = "python")]
178 use pyo3::types::PyString;
179 use rstest::*;
180
181 use super::*;
182
183 #[cfg(feature = "python")]
184 #[cfg_attr(miri, ignore)]
185 #[rstest]
186 fn test_pystr_to_string() {
187 pyo3::prepare_freethreaded_python();
188 // Create a valid Python object pointer
189 let ptr = Python::with_gil(|py| PyString::new(py, "test string1").as_ptr());
190 let result = unsafe { pystr_to_string(ptr) };
191 assert_eq!(result, "test string1");
192 }
193
194 #[cfg(feature = "python")]
195 #[rstest]
196 #[should_panic(expected = "`ptr` was NULL")]
197 fn test_pystr_to_string_with_null_ptr() {
198 // Create a null Python object pointer
199 let ptr: *mut ffi::PyObject = std::ptr::null_mut();
200 unsafe {
201 let _ = pystr_to_string(ptr);
202 };
203 }
204
205 #[rstest]
206 fn test_cstr_to_str() {
207 // Create a valid C string pointer
208 let c_string = CString::new("test string2").expect("CString::new failed");
209 let ptr = c_string.as_ptr();
210 let result = unsafe { cstr_as_str(ptr) };
211 assert_eq!(result, "test string2");
212 }
213
214 #[rstest]
215 fn test_cstr_to_vec() {
216 // Create a valid C string pointer
217 let sample_c_string = CString::new("Hello, world!").expect("CString::new failed");
218 let cstr_ptr = sample_c_string.as_ptr();
219 let result = unsafe { cstr_to_bytes(cstr_ptr) };
220 assert_eq!(result, b"Hello, world!");
221 assert_eq!(result.len(), 13);
222 }
223
224 #[rstest]
225 #[should_panic(expected = "`ptr` was NULL")]
226 fn test_cstr_to_vec_with_null_ptr() {
227 // Create a null C string pointer
228 let ptr: *const c_char = std::ptr::null();
229 unsafe {
230 let _ = cstr_to_bytes(ptr);
231 };
232 }
233
234 #[rstest]
235 fn test_optional_cstr_to_str_with_null_ptr() {
236 // Call optional_cstr_to_str with null pointer
237 let ptr = std::ptr::null();
238 let result = unsafe { optional_cstr_to_str(ptr) };
239 assert!(result.is_none());
240 }
241
242 #[rstest]
243 fn test_optional_cstr_to_str_with_valid_ptr() {
244 // Create a valid C string
245 let input_str = "hello world";
246 let c_str = CString::new(input_str).expect("CString::new failed");
247 let result = unsafe { optional_cstr_to_str(c_str.as_ptr()) };
248 assert!(result.is_some());
249 assert_eq!(result.unwrap(), input_str);
250 }
251
252 #[rstest]
253 fn test_string_to_cstr() {
254 let s = "test string";
255 let c_str_ptr = str_to_cstr(s);
256 let c_str = unsafe { CStr::from_ptr(c_str_ptr) };
257 let result = c_str.to_str().expect("CStr::from_ptr failed");
258 assert_eq!(result, s);
259 }
260
261 #[rstest]
262 fn test_cstr_drop() {
263 let c_string = CString::new("test string3").expect("CString::new failed");
264 let ptr = c_string.into_raw(); // <-- pointer _must_ be obtained this way
265 unsafe { cstr_drop(ptr) };
266 }
267}