nautilus_core/ffi/
cvec.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 transferring heap-allocated Rust `Vec<T>` values across an FFI boundary.
17//!
18//! The primary abstraction offered by this module is `CVec`, a C-compatible struct that stores
19//! a raw pointer (`ptr`) together with the vector’s logical `len` and `cap`.  By moving the
20//! allocation metadata into a plain `repr(C)` type we allow the memory created by Rust to be
21//! owned, inspected, and ultimately freed by foreign code (or vice-versa) without introducing
22//! undefined behaviour.
23//!
24//! Only a very small API surface is exposed to C:
25//!
26//! * `cvec_new` – create an empty `CVec` sentinel that can be returned to foreign code.
27//!
28//! De-allocation is intentionally **not** provided via a generic helper. Instead each FFI module
29//! must expose its own *type-specific* `vec_*_drop` function which reconstructs the original
30//! `Vec<T>` with [`Vec::from_raw_parts`] and allows it to drop. This avoids the size-mismatch risk
31//! that a one-size-fits-all `cvec_drop` had in the past.
32//!
33//! All other manipulation happens on the Rust side before relinquishing ownership.  This keeps the
34//! rules for memory safety straightforward: foreign callers must treat the memory region pointed
35//! to by `ptr` as **opaque** and interact with it solely through the functions provided here.
36
37use std::{ffi::c_void, fmt::Display, ptr::null};
38
39/// `CVec` is a C compatible struct that stores an opaque pointer to a block of
40/// memory, it's length and the capacity of the vector it was allocated from.
41///
42/// # Safety
43///
44/// Changing the values here may lead to undefined behavior when the memory is dropped.
45#[repr(C)]
46#[derive(Clone, Copy, Debug)]
47pub struct CVec {
48    /// Opaque pointer to block of memory storing elements to access the
49    /// elements cast it to the underlying type.
50    pub ptr: *mut c_void,
51    /// The number of elements in the block.
52    pub len: usize,
53    /// The capacity of vector from which it was allocated.
54    /// Used when deallocating the memory
55    pub cap: usize,
56}
57
58/// Empty derivation for Send to satisfy `pyclass` requirements
59/// however this is only designed for single threaded use for now
60unsafe impl Send for CVec {}
61
62impl CVec {
63    /// Returns an empty [`CVec`].
64    ///
65    /// This is primarily useful for constructing a sentinel value that represents the
66    /// absence of data when crossing the FFI boundary.
67    #[must_use]
68    pub const fn empty() -> Self {
69        Self {
70            // Explicitly type cast the pointer to some type to satisfy the
71            // compiler. Since the pointer is null it works for any type.
72            ptr: null::<bool>() as *mut c_void,
73            len: 0,
74            cap: 0,
75        }
76    }
77}
78
79/// Consumes and leaks the Vec, returning a mutable pointer to the contents as
80/// a [`CVec`]. The memory has been leaked and now exists for the lifetime of the
81/// program unless dropped manually.
82/// Note: drop the memory by reconstructing the vec using `from_raw_parts` method
83/// as shown in the test below.
84impl<T> From<Vec<T>> for CVec {
85    fn from(mut data: Vec<T>) -> Self {
86        if data.is_empty() {
87            Self::empty()
88        } else {
89            let len = data.len();
90            let cap = data.capacity();
91            let ptr = data.as_mut_ptr();
92            std::mem::forget(data);
93            Self {
94                ptr: ptr.cast::<std::ffi::c_void>(),
95                len,
96                cap,
97            }
98        }
99    }
100}
101
102impl Display for CVec {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        write!(
105            f,
106            "CVec {{ ptr: {:?}, len: {}, cap: {} }}",
107            self.ptr, self.len, self.cap,
108        )
109    }
110}
111
112////////////////////////////////////////////////////////////////////////////////
113// C API
114////////////////////////////////////////////////////////////////////////////////
115
116/// Construct a new *empty* [`CVec`] value for use as initialiser/sentinel in foreign code.
117#[cfg(feature = "ffi")]
118#[unsafe(no_mangle)]
119pub const extern "C" fn cvec_new() -> CVec {
120    CVec::empty()
121}
122
123#[cfg(test)]
124mod tests {
125    use rstest::*;
126
127    use super::CVec;
128
129    /// Access values from a vector converted into a [`CVec`].
130    #[rstest]
131    #[allow(unused_assignments)]
132    fn access_values_test() {
133        let test_data = vec![1_u64, 2, 3];
134        let mut vec_len = 0;
135        let mut vec_cap = 0;
136        let cvec: CVec = {
137            let data = test_data.clone();
138            vec_len = data.len();
139            vec_cap = data.capacity();
140            data.into()
141        };
142
143        let CVec { ptr, len, cap } = cvec;
144        assert_eq!(len, vec_len);
145        assert_eq!(cap, vec_cap);
146
147        let data = ptr.cast::<u64>();
148        unsafe {
149            assert_eq!(*data, test_data[0]);
150            assert_eq!(*data.add(1), test_data[1]);
151            assert_eq!(*data.add(2), test_data[2]);
152        }
153
154        unsafe {
155            // reconstruct the struct and drop the memory to deallocate
156            let _ = Vec::from_raw_parts(ptr.cast::<u64>(), len, cap);
157        }
158    }
159
160    /// After deallocating the vector the block of memory may not
161    /// contain the same values.
162    #[rstest]
163    #[ignore = "Flaky on some platforms"]
164    fn drop_test() {
165        let test_data = vec![1, 2, 3];
166        let cvec: CVec = {
167            let data = test_data.clone();
168            data.into()
169        };
170
171        let CVec { ptr, len, cap } = cvec;
172        let data = ptr.cast::<u64>();
173
174        unsafe {
175            let data: Vec<u64> = Vec::from_raw_parts(ptr.cast::<u64>(), len, cap);
176            drop(data);
177        }
178
179        unsafe {
180            assert_ne!(*data, test_data[0]);
181            assert_ne!(*data.add(1), test_data[1]);
182            assert_ne!(*data.add(2), test_data[2]);
183        }
184    }
185
186    /// An empty vector gets converted to a null pointer wrapped in a [`CVec`].
187    #[rstest]
188    fn empty_vec_should_give_null_ptr() {
189        let data: Vec<u64> = vec![];
190        let cvec: CVec = data.into();
191        assert_eq!(cvec.ptr.cast::<u64>(), std::ptr::null_mut::<u64>());
192    }
193}