nautilus_core/ffi/string.rs
1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3// https://nautechsystems.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
39use crate::ffi::abort_on_panic;
40
41#[cfg(feature = "python")]
42/// Returns an owned string from a valid Python object pointer.
43///
44/// # Safety
45///
46/// Assumes `ptr` is borrowed from a valid Python UTF-8 `str`.
47///
48/// # Panics
49///
50/// Panics if `ptr` is null.
51#[must_use]
52pub unsafe fn pystr_to_string(ptr: *mut ffi::PyObject) -> String {
53 assert!(!ptr.is_null(), "`ptr` was NULL");
54 Python::attach(|py| unsafe { Bound::from_borrowed_ptr(py, ptr).to_string() })
55}
56
57/// Convert a C string pointer into an owned `String`.
58///
59/// # Safety
60///
61/// Assumes `ptr` is a valid C string pointer.
62///
63/// # Panics
64///
65/// Panics if `ptr` is null.
66#[must_use]
67pub unsafe fn cstr_to_ustr(ptr: *const c_char) -> Ustr {
68 assert!(!ptr.is_null(), "`ptr` was NULL");
69 let cstr = unsafe { CStr::from_ptr(ptr) };
70 Ustr::from(cstr.to_str().expect("CStr::from_ptr failed"))
71}
72
73/// Convert a C string pointer into a borrowed byte slice.
74///
75/// # Safety
76///
77/// - Assumes `ptr` is a valid, null-terminated UTF-8 C string pointer.
78/// - The returned slice borrows the underlying allocation; callers must ensure the
79/// C buffer outlives every use of the slice.
80///
81/// # Panics
82///
83/// Panics if `ptr` is null.
84#[must_use]
85pub unsafe fn cstr_to_bytes<'a>(ptr: *const c_char) -> &'a [u8] {
86 assert!(!ptr.is_null(), "`ptr` was NULL");
87 let cstr = unsafe { CStr::from_ptr(ptr) };
88 cstr.to_bytes()
89}
90
91/// Convert a C string pointer into an owned `Option<Ustr>`.
92///
93/// # Safety
94///
95/// Assumes `ptr` is a valid C string pointer or NULL.
96///
97/// # Panics
98///
99/// Panics if `ptr` is null.
100#[must_use]
101pub unsafe fn optional_cstr_to_ustr(ptr: *const c_char) -> Option<Ustr> {
102 if ptr.is_null() {
103 None
104 } else {
105 Some(unsafe { cstr_to_ustr(ptr) })
106 }
107}
108
109/// Convert a C string pointer into a borrowed string slice.
110///
111/// # Safety
112///
113/// - Assumes `ptr` is a valid, null-terminated UTF-8 C string pointer.
114/// - The returned `&str` borrows the underlying allocation; callers must ensure the
115/// C buffer outlives every use of the string slice.
116///
117/// # Panics
118///
119/// Panics if `ptr` is null or contains invalid UTF-8.
120#[must_use]
121pub unsafe fn cstr_as_str<'a>(ptr: *const c_char) -> &'a str {
122 assert!(!ptr.is_null(), "`ptr` was NULL");
123 let cstr = unsafe { CStr::from_ptr(ptr) };
124 cstr.to_str().expect("C string contains invalid UTF-8")
125}
126
127/// Convert an optional C string pointer into `Option<&str>`.
128///
129/// # Safety
130///
131/// - Assumes `ptr` is a valid, null-terminated UTF-8 C string pointer or NULL.
132/// - Any borrowed string must not outlive the underlying allocation.
133///
134/// # Panics
135///
136/// Panics if `ptr` is not null but contains invalid UTF-8.
137#[must_use]
138pub unsafe fn optional_cstr_to_str<'a>(ptr: *const c_char) -> Option<&'a 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 abort_on_panic(|| {
168 assert!(!ptr.is_null(), "`ptr` was NULL");
169 let cstring = unsafe { CString::from_raw(ptr.cast_mut()) };
170 drop(cstring);
171 })
172}
173
174////////////////////////////////////////////////////////////////////////////////
175// Tests
176////////////////////////////////////////////////////////////////////////////////
177#[cfg(test)]
178mod tests {
179 #[cfg(feature = "python")]
180 use pyo3::types::PyString;
181 use rstest::*;
182
183 use super::*;
184
185 #[cfg(feature = "python")]
186 #[cfg_attr(miri, ignore)]
187 #[rstest]
188 fn test_pystr_to_string() {
189 Python::initialize();
190 // Create a valid Python object pointer
191 let ptr = Python::attach(|py| PyString::new(py, "test string1").as_ptr());
192 let result = unsafe { pystr_to_string(ptr) };
193 assert_eq!(result, "test string1");
194 }
195
196 #[cfg(feature = "python")]
197 #[rstest]
198 #[should_panic(expected = "`ptr` was NULL")]
199 fn test_pystr_to_string_with_null_ptr() {
200 // Create a null Python object pointer
201 let ptr: *mut ffi::PyObject = std::ptr::null_mut();
202 unsafe {
203 let _ = pystr_to_string(ptr);
204 };
205 }
206
207 #[rstest]
208 fn test_cstr_as_str() {
209 // Create a valid C string pointer
210 let c_string = CString::new("test string2").expect("CString::new failed");
211 let ptr = c_string.as_ptr();
212 let result = unsafe { cstr_as_str(ptr) };
213 assert_eq!(result, "test string2");
214 }
215
216 #[rstest]
217 fn test_cstr_to_bytes() {
218 // Create a valid C string
219 let sample_c_string = CString::new("Hello, world!").expect("CString::new failed");
220 let cstr_ptr = sample_c_string.as_ptr();
221 let result = unsafe { cstr_to_bytes(cstr_ptr) };
222 assert_eq!(result, b"Hello, world!");
223 assert_eq!(result.len(), 13);
224 }
225
226 #[rstest]
227 #[should_panic(expected = "`ptr` was NULL")]
228 fn test_cstr_to_bytes_with_null_ptr() {
229 // Create a null C string pointer
230 let ptr: *const c_char = std::ptr::null();
231 unsafe {
232 let _ = cstr_to_bytes(ptr);
233 };
234 }
235
236 #[rstest]
237 fn test_optional_cstr_to_str_with_null_ptr() {
238 // Call optional_cstr_to_str with null pointer
239 let ptr = std::ptr::null();
240 let result = unsafe { optional_cstr_to_str(ptr) };
241 assert!(result.is_none());
242 }
243
244 #[rstest]
245 fn test_optional_cstr_to_str_with_valid_ptr() {
246 // Create a valid C string
247 let input_str = "hello world";
248 let c_str = CString::new(input_str).expect("CString::new failed");
249 let result = unsafe { optional_cstr_to_str(c_str.as_ptr()) };
250 assert!(result.is_some());
251 assert_eq!(result.unwrap(), input_str);
252 }
253
254 #[rstest]
255 fn test_string_to_cstr() {
256 let s = "test string";
257 let c_str_ptr = str_to_cstr(s);
258 let c_str = unsafe { CStr::from_ptr(c_str_ptr) };
259 let result = c_str.to_str().expect("CStr::from_ptr failed");
260 assert_eq!(result, s);
261 }
262
263 #[rstest]
264 fn test_cstr_drop() {
265 let c_string = CString::new("test string3").expect("CString::new failed");
266 let ptr = c_string.into_raw(); // <-- pointer _must_ be obtained this way
267 unsafe { cstr_drop(ptr) };
268 }
269}