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
16//! Utilities for safely moving UTF-8 strings across the FFI boundary.
17//!
18//! Interoperability between Rust and C/C++/Python often requires raw pointers to *null terminated*
19//! strings.  This module provides convenience helpers that:
20//!
21//! * Convert raw `*const c_char` pointers to Rust [`String`], [`&str`], byte slices, or
22//!   `ustr::Ustr` values.
23//! * Perform the inverse conversion when Rust needs to hand ownership of a string to foreign
24//!   code.
25//!
26//! The majority of these functions are marked `unsafe` because they accept raw pointers and rely
27//! on the caller to uphold basic invariants (pointer validity, lifetime, UTF-8 correctness).  Each
28//! function documents the specific safety requirements.
29
30use std::{
31    ffi::{CStr, CString, c_char},
32    str,
33};
34
35#[cfg(feature = "python")]
36use pyo3::{Bound, Python, ffi};
37use ustr::Ustr;
38
39#[cfg(feature = "python")]
40/// Returns an owned string from a valid Python object pointer.
41///
42/// # Safety
43///
44/// Assumes `ptr` is borrowed from a valid Python UTF-8 `str`.
45///
46/// # Panics
47///
48/// Panics if `ptr` is null.
49#[must_use]
50pub unsafe fn pystr_to_string(ptr: *mut ffi::PyObject) -> String {
51    assert!(!ptr.is_null(), "`ptr` was NULL");
52    Python::with_gil(|py| unsafe { Bound::from_borrowed_ptr(py, ptr).to_string() })
53}
54
55/// Convert a C string pointer into an owned `String`.
56///
57/// # Safety
58///
59/// Assumes `ptr` is a valid C string pointer.
60///
61/// # Panics
62///
63/// Panics if `ptr` is null.
64#[must_use]
65pub unsafe fn cstr_to_ustr(ptr: *const c_char) -> Ustr {
66    assert!(!ptr.is_null(), "`ptr` was NULL");
67    let cstr = unsafe { CStr::from_ptr(ptr) };
68    Ustr::from(cstr.to_str().expect("CStr::from_ptr failed"))
69}
70
71/// Convert a C string pointer into bytes.
72///
73/// # Safety
74///
75/// - Assumes `ptr` is a valid C string pointer.
76/// - The returned slice is only valid while the original C string remains allocated.
77/// - Caller must ensure the C string outlives any usage of the returned slice.
78///
79/// The actual lifetime is tied to the C string's allocation lifetime.
80/// This is acceptable because this function is only used for immediate
81/// consumption within FFI call boundaries where the C string remains valid.
82///
83/// This function is designed for immediate consumption within FFI calls.
84/// Do not store the returned slice for use beyond the current function scope.
85///
86/// # Panics
87///
88/// Panics if `ptr` is null.
89#[must_use]
90pub unsafe fn cstr_to_bytes(ptr: *const c_char) -> &'static [u8] {
91    assert!(!ptr.is_null(), "`ptr` was NULL");
92    let cstr = unsafe { CStr::from_ptr(ptr) };
93    cstr.to_bytes()
94}
95
96/// Convert a C string pointer into an owned `Option<Ustr>`.
97///
98/// # Safety
99///
100/// Assumes `ptr` is a valid C string pointer or NULL.
101///
102/// # Panics
103///
104/// Panics if `ptr` is null.
105#[must_use]
106pub unsafe fn optional_cstr_to_ustr(ptr: *const c_char) -> Option<Ustr> {
107    if ptr.is_null() {
108        None
109    } else {
110        Some(unsafe { cstr_to_ustr(ptr) })
111    }
112}
113
114/// Convert a C string pointer into a string slice.
115///
116/// # Safety
117///
118/// - Assumes `ptr` is a valid C string pointer.
119/// - The returned slice is only valid while the original C string remains allocated.
120/// - Caller must ensure the C string outlives any usage of the returned slice.
121///
122/// The actual lifetime is tied to the C string's allocation lifetime.
123/// This is acceptable because this function is only used for immediate
124/// consumption within FFI call boundaries where the C string remains valid.
125///
126/// This function is designed for immediate consumption within FFI calls.
127/// Do not store the returned slice for use beyond the current function scope.
128///
129/// # Panics
130///
131/// Panics if `ptr` is null or contains invalid UTF-8.
132#[must_use]
133pub unsafe fn cstr_as_str(ptr: *const c_char) -> &'static str {
134    assert!(!ptr.is_null(), "`ptr` was NULL");
135    let cstr = unsafe { CStr::from_ptr(ptr) };
136    cstr.to_str().expect("CStr::from_ptr failed")
137}
138
139/// Convert a C string pointer into an `Option<&str>`.
140///
141/// # Safety
142///
143/// - Assumes `ptr` is a valid C string pointer or NULL.
144/// - The returned slice is only valid while the original C string remains allocated.
145/// - Caller must ensure the C string outlives any usage of the returned slice.
146///
147/// The actual lifetime is tied to the C string's allocation lifetime.
148/// This is acceptable because this function is only used for immediate
149/// consumption within FFI call boundaries where the C string remains valid.
150///
151/// This function is designed for immediate consumption within FFI calls.
152/// Do not store the returned slice for use beyond the current function scope.
153///
154/// # Panics
155///
156/// Panics if `ptr` is not null but contains invalid UTF-8.
157#[must_use]
158pub unsafe fn optional_cstr_to_str(ptr: *const c_char) -> Option<&'static str> {
159    if ptr.is_null() {
160        None
161    } else {
162        Some(unsafe { cstr_as_str(ptr) })
163    }
164}
165
166/// Create a C string pointer to newly allocated memory from a [`&str`].
167///
168/// # Panics
169///
170/// Panics if the input string contains interior null bytes.
171#[must_use]
172pub fn str_to_cstr(s: &str) -> *const c_char {
173    CString::new(s).expect("CString::new failed").into_raw()
174}
175
176/// Drops the C string memory at the pointer.
177///
178/// # Safety
179///
180/// Assumes `ptr` is a valid C string pointer.
181///
182/// # Panics
183///
184/// Panics if `ptr` is null.
185#[unsafe(no_mangle)]
186pub unsafe extern "C" fn cstr_drop(ptr: *const c_char) {
187    assert!(!ptr.is_null(), "`ptr` was NULL");
188    let cstring = unsafe { CString::from_raw(ptr.cast_mut()) };
189    drop(cstring);
190}
191
192////////////////////////////////////////////////////////////////////////////////
193// Tests
194////////////////////////////////////////////////////////////////////////////////
195#[cfg(test)]
196mod tests {
197    #[cfg(feature = "python")]
198    use pyo3::types::PyString;
199    use rstest::*;
200
201    use super::*;
202
203    #[cfg(feature = "python")]
204    #[cfg_attr(miri, ignore)]
205    #[rstest]
206    fn test_pystr_to_string() {
207        pyo3::prepare_freethreaded_python();
208        // Create a valid Python object pointer
209        let ptr = Python::with_gil(|py| PyString::new(py, "test string1").as_ptr());
210        let result = unsafe { pystr_to_string(ptr) };
211        assert_eq!(result, "test string1");
212    }
213
214    #[cfg(feature = "python")]
215    #[rstest]
216    #[should_panic(expected = "`ptr` was NULL")]
217    fn test_pystr_to_string_with_null_ptr() {
218        // Create a null Python object pointer
219        let ptr: *mut ffi::PyObject = std::ptr::null_mut();
220        unsafe {
221            let _ = pystr_to_string(ptr);
222        };
223    }
224
225    #[rstest]
226    fn test_cstr_to_str() {
227        // Create a valid C string pointer
228        let c_string = CString::new("test string2").expect("CString::new failed");
229        let ptr = c_string.as_ptr();
230        let result = unsafe { cstr_as_str(ptr) };
231        assert_eq!(result, "test string2");
232    }
233
234    #[rstest]
235    fn test_cstr_to_vec() {
236        // Create a valid C string pointer
237        let sample_c_string = CString::new("Hello, world!").expect("CString::new failed");
238        let cstr_ptr = sample_c_string.as_ptr();
239        let result = unsafe { cstr_to_bytes(cstr_ptr) };
240        assert_eq!(result, b"Hello, world!");
241        assert_eq!(result.len(), 13);
242    }
243
244    #[rstest]
245    #[should_panic(expected = "`ptr` was NULL")]
246    fn test_cstr_to_vec_with_null_ptr() {
247        // Create a null C string pointer
248        let ptr: *const c_char = std::ptr::null();
249        unsafe {
250            let _ = cstr_to_bytes(ptr);
251        };
252    }
253
254    #[rstest]
255    fn test_optional_cstr_to_str_with_null_ptr() {
256        // Call optional_cstr_to_str with null pointer
257        let ptr = std::ptr::null();
258        let result = unsafe { optional_cstr_to_str(ptr) };
259        assert!(result.is_none());
260    }
261
262    #[rstest]
263    fn test_optional_cstr_to_str_with_valid_ptr() {
264        // Create a valid C string
265        let input_str = "hello world";
266        let c_str = CString::new(input_str).expect("CString::new failed");
267        let result = unsafe { optional_cstr_to_str(c_str.as_ptr()) };
268        assert!(result.is_some());
269        assert_eq!(result.unwrap(), input_str);
270    }
271
272    #[rstest]
273    fn test_string_to_cstr() {
274        let s = "test string";
275        let c_str_ptr = str_to_cstr(s);
276        let c_str = unsafe { CStr::from_ptr(c_str_ptr) };
277        let result = c_str.to_str().expect("CStr::from_ptr failed");
278        assert_eq!(result, s);
279    }
280
281    #[rstest]
282    fn test_cstr_drop() {
283        let c_string = CString::new("test string3").expect("CString::new failed");
284        let ptr = c_string.into_raw(); // <-- pointer _must_ be obtained this way
285        unsafe { cstr_drop(ptr) };
286    }
287}