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
39use crate::ffi::abort_on_panic;
40
41/// `CVec` is a C compatible struct that stores an opaque pointer to a block of
42/// memory, it's length and the capacity of the vector it was allocated from.
43///
44/// # Safety
45///
46/// Changing the values here may lead to undefined behavior when the memory is dropped.
47#[repr(C)]
48#[derive(Clone, Copy, Debug)]
49pub struct CVec {
50 /// Opaque pointer to block of memory storing elements to access the
51 /// elements cast it to the underlying type.
52 pub ptr: *mut c_void,
53 /// The number of elements in the block.
54 pub len: usize,
55 /// The capacity of vector from which it was allocated.
56 /// Used when deallocating the memory
57 pub cap: usize,
58}
59
60// SAFETY: CVec is marked as Send to satisfy PyO3's PyCapsule requirements, which need
61// to transfer ownership across the Python/Rust boundary. However, CVec contains raw
62// pointers and is only safe to use in single-threaded contexts or with external
63// synchronization guarantees.
64//
65// The Send impl is required for:
66// 1. PyO3's PyCapsule::new_with_destructor which has a Send bound
67// 2. Transferring CVec ownership to Python (which runs on a single GIL-protected thread)
68//
69// IMPORTANT: Do not send CVec instances across threads without ensuring:
70// - The underlying data type T is itself Send + Sync
71// - Proper external synchronization (e.g., mutex) protects concurrent access
72// - The CVec is consumed on the same thread where it will be reconstructed
73//
74// In practice, CVec usage in this codebase is confined to the Python FFI boundary
75// where the Python GIL provides the necessary synchronization.
76unsafe impl Send for CVec {}
77
78impl CVec {
79 /// Returns an empty [`CVec`].
80 ///
81 /// This is primarily useful for constructing a sentinel value that represents the
82 /// absence of data when crossing the FFI boundary.
83 #[must_use]
84 pub const fn empty() -> Self {
85 Self {
86 // Explicitly type cast the pointer to some type to satisfy the
87 // compiler. Since the pointer is null it works for any type.
88 ptr: null::<bool>() as *mut c_void,
89 len: 0,
90 cap: 0,
91 }
92 }
93}
94
95/// Consumes and leaks the Vec, returning a mutable pointer to the contents as
96/// a [`CVec`]. The memory has been leaked and now exists for the lifetime of the
97/// program unless dropped manually.
98/// Note: drop the memory by reconstructing the vec using `from_raw_parts` method
99/// as shown in the test below.
100impl<T> From<Vec<T>> for CVec {
101 fn from(mut data: Vec<T>) -> Self {
102 if data.is_empty() {
103 Self::empty()
104 } else {
105 let len = data.len();
106 let cap = data.capacity();
107 let ptr = data.as_mut_ptr();
108 std::mem::forget(data);
109 Self {
110 ptr: ptr.cast::<std::ffi::c_void>(),
111 len,
112 cap,
113 }
114 }
115 }
116}
117
118impl Display for CVec {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 write!(
121 f,
122 "CVec {{ ptr: {:?}, len: {}, cap: {} }}",
123 self.ptr, self.len, self.cap,
124 )
125 }
126}
127
128////////////////////////////////////////////////////////////////////////////////
129// C API
130////////////////////////////////////////////////////////////////////////////////
131
132/// Construct a new *empty* [`CVec`] value for use as initialiser/sentinel in foreign code.
133#[cfg(feature = "ffi")]
134#[unsafe(no_mangle)]
135pub extern "C" fn cvec_new() -> CVec {
136 abort_on_panic(CVec::empty)
137}
138
139#[cfg(test)]
140mod tests {
141 use rstest::*;
142
143 use super::CVec;
144
145 /// Access values from a vector converted into a [`CVec`].
146 #[rstest]
147 #[allow(unused_assignments)]
148 fn access_values_test() {
149 let test_data = vec![1_u64, 2, 3];
150 let mut vec_len = 0;
151 let mut vec_cap = 0;
152 let cvec: CVec = {
153 let data = test_data.clone();
154 vec_len = data.len();
155 vec_cap = data.capacity();
156 data.into()
157 };
158
159 let CVec { ptr, len, cap } = cvec;
160 assert_eq!(len, vec_len);
161 assert_eq!(cap, vec_cap);
162
163 let data = ptr.cast::<u64>();
164 unsafe {
165 assert_eq!(*data, test_data[0]);
166 assert_eq!(*data.add(1), test_data[1]);
167 assert_eq!(*data.add(2), test_data[2]);
168 }
169
170 unsafe {
171 // reconstruct the struct and drop the memory to deallocate
172 let _ = Vec::from_raw_parts(ptr.cast::<u64>(), len, cap);
173 }
174 }
175
176 /// After deallocating the vector the block of memory may not
177 /// contain the same values.
178 #[rstest]
179 #[ignore = "Flaky on some platforms"]
180 fn drop_test() {
181 let test_data = vec![1, 2, 3];
182 let cvec: CVec = {
183 let data = test_data.clone();
184 data.into()
185 };
186
187 let CVec { ptr, len, cap } = cvec;
188 let data = ptr.cast::<u64>();
189
190 unsafe {
191 let data: Vec<u64> = Vec::from_raw_parts(ptr.cast::<u64>(), len, cap);
192 drop(data);
193 }
194
195 unsafe {
196 assert_ne!(*data, test_data[0]);
197 assert_ne!(*data.add(1), test_data[1]);
198 assert_ne!(*data.add(2), test_data[2]);
199 }
200 }
201
202 /// An empty vector gets converted to a null pointer wrapped in a [`CVec`].
203 #[rstest]
204 fn empty_vec_should_give_null_ptr() {
205 let data: Vec<u64> = vec![];
206 let cvec: CVec = data.into();
207 assert_eq!(cvec.ptr.cast::<u64>(), std::ptr::null_mut::<u64>());
208 }
209}