nautilus_core/ffi/cvec.rs
1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2026 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 behavior.
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 deallocator. 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::NonNull};
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, its 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(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
60impl CVec {
61 /// Returns an empty [`CVec`].
62 ///
63 /// This is primarily useful for constructing a sentinel value that represents the
64 /// absence of data when crossing the FFI boundary.
65 ///
66 /// Uses a dangling pointer (like `Vec::new()`) rather than null to satisfy
67 /// `Vec::from_raw_parts` preconditions when the `CVec` is later dropped.
68 #[must_use]
69 pub fn empty() -> Self {
70 Self {
71 ptr: NonNull::<u8>::dangling().as_ptr().cast::<c_void>(),
72 len: 0,
73 cap: 0,
74 }
75 }
76
77 /// Reconstructs and consumes the Rust vector represented by this value.
78 ///
79 /// # Safety
80 ///
81 /// For non-zero capacity, `ptr`, `len`, and `cap` must describe exactly one live allocation
82 /// originally created by `Vec<T>` with `len` initialized elements. The allocation must not be
83 /// accessed or reconstructed again after this call.
84 ///
85 /// # Panics
86 ///
87 /// Panics if `len > cap`, `len != 0` when `cap == 0`, or a non-empty allocation has a null
88 /// pointer.
89 #[must_use]
90 pub unsafe fn into_vec<T>(self) -> Vec<T> {
91 assert!(
92 self.len <= self.cap,
93 "CVec::into_vec: len ({}) > cap ({})",
94 self.len,
95 self.cap
96 );
97
98 if self.cap == 0 {
99 assert_eq!(
100 self.len, 0,
101 "CVec::into_vec: zero capacity with non-zero len ({})",
102 self.len
103 );
104 return Vec::new();
105 }
106
107 assert!(
108 !self.ptr.is_null(),
109 "CVec::into_vec: null ptr with non-zero cap ({})",
110 self.cap
111 );
112 debug_assert!(self.ptr.cast::<T>().is_aligned());
113 debug_assert!(
114 self.cap
115 .checked_mul(std::mem::size_of::<T>())
116 .is_some_and(|bytes| isize::try_from(bytes).is_ok())
117 );
118
119 unsafe { Vec::from_raw_parts(self.ptr.cast::<T>(), self.len, self.cap) }
120 }
121
122 /// Borrows the initialized elements represented by this value.
123 ///
124 /// # Safety
125 ///
126 /// For non-zero length, `ptr` must point to `len` initialized, properly aligned `T` values
127 /// that remain valid and are not mutated for the returned slice's lifetime.
128 ///
129 /// # Panics
130 ///
131 /// Panics if `len > cap` or a non-empty slice has a null pointer.
132 #[must_use]
133 pub unsafe fn as_slice<T>(&self) -> &[T] {
134 assert!(
135 self.len <= self.cap,
136 "CVec::as_slice: len ({}) > cap ({})",
137 self.len,
138 self.cap
139 );
140
141 if self.len == 0 {
142 return &[];
143 }
144
145 assert!(
146 !self.ptr.is_null(),
147 "CVec::as_slice: null ptr with non-zero len ({})",
148 self.len
149 );
150 debug_assert!(self.ptr.cast::<T>().is_aligned());
151 debug_assert!(
152 self.len
153 .checked_mul(std::mem::size_of::<T>())
154 .is_some_and(|bytes| isize::try_from(bytes).is_ok())
155 );
156
157 unsafe { std::slice::from_raw_parts(self.ptr.cast::<T>(), self.len) }
158 }
159}
160
161/// Consumes and leaks the Vec, returning a mutable pointer to the contents as
162/// a [`CVec`]. The memory has been leaked and now exists for the lifetime of the
163/// program unless dropped manually.
164/// Note: drop the memory by reconstructing the vec using `from_raw_parts` method
165/// as shown in the test below.
166impl<T> From<Vec<T>> for CVec {
167 fn from(data: Vec<T>) -> Self {
168 if data.is_empty() {
169 Self::empty()
170 } else {
171 let (ptr, len, cap) = data.into_raw_parts();
172
173 Self {
174 ptr: ptr.cast::<std::ffi::c_void>(),
175 len,
176 cap,
177 }
178 }
179 }
180}
181
182impl Display for CVec {
183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184 write!(
185 f,
186 "CVec(ptr={:?}, len={}, cap={})",
187 self.ptr, self.len, self.cap,
188 )
189 }
190}
191
192////////////////////////////////////////////////////////////////////////////////
193// C API
194////////////////////////////////////////////////////////////////////////////////
195
196/// Construct a new *empty* [`CVec`] value for use as initialiser/sentinel in foreign code.
197#[cfg(feature = "ffi")]
198#[unsafe(no_mangle)]
199pub extern "C" fn cvec_new() -> CVec {
200 abort_on_panic(CVec::empty)
201}
202
203#[cfg(test)]
204mod tests {
205 use std::sync::{
206 Arc,
207 atomic::{AtomicUsize, Ordering},
208 };
209
210 use rstest::*;
211
212 use super::CVec;
213
214 /// Access values from a vector converted into a [`CVec`].
215 #[rstest]
216 #[allow(unused_assignments)]
217 fn access_values_test() {
218 let test_data = vec![1_u64, 2, 3];
219 let mut vec_len = 0;
220 let mut vec_cap = 0;
221 let cvec: CVec = {
222 let data = test_data.clone();
223 vec_len = data.len();
224 vec_cap = data.capacity();
225 data.into()
226 };
227
228 assert_eq!(cvec.len, vec_len);
229 assert_eq!(cvec.cap, vec_cap);
230
231 let data = unsafe { cvec.into_vec::<u64>() };
232 assert_eq!(data, test_data);
233 }
234
235 /// An empty vector gets converted to a dangling (non-null) pointer in a [`CVec`].
236 #[rstest]
237 fn empty_vec_should_give_dangling_ptr() {
238 let data: Vec<u64> = vec![];
239 let cvec: CVec = data.into();
240 assert!(!cvec.ptr.is_null());
241 assert_eq!(cvec.len, 0);
242 assert_eq!(cvec.cap, 0);
243 }
244
245 #[rstest]
246 fn display_uses_key_value_fields() {
247 let cvec = CVec::empty();
248
249 assert_eq!(
250 cvec.to_string(),
251 format!("CVec(ptr={:?}, len=0, cap=0)", cvec.ptr)
252 );
253 }
254
255 #[repr(align(64))]
256 struct Aligned;
257
258 #[rstest]
259 #[case(CVec::empty())]
260 #[case(Vec::<u64>::new().into())]
261 fn empty_into_vec_does_not_inspect_pointer(#[case] cvec: CVec) {
262 let values = unsafe { cvec.into_vec::<u64>() };
263 assert!(values.is_empty());
264 }
265
266 #[rstest]
267 fn aligned_empty_into_vec_does_not_reconstruct_pointer() {
268 let values = unsafe { CVec::empty().into_vec::<Aligned>() };
269 assert!(values.is_empty());
270 }
271
272 #[rstest]
273 fn aligned_empty_as_slice_does_not_inspect_pointer() {
274 let cvec = CVec::empty();
275 let values = unsafe { cvec.as_slice::<Aligned>() };
276 assert!(values.is_empty());
277 }
278
279 #[rstest]
280 fn non_empty_into_vec_round_trips_and_drops_once() {
281 struct DropCounter(Arc<AtomicUsize>);
282
283 impl Drop for DropCounter {
284 fn drop(&mut self) {
285 self.0.fetch_add(1, Ordering::SeqCst);
286 }
287 }
288
289 let drops = Arc::new(AtomicUsize::new(0));
290 let cvec: CVec = vec![DropCounter(Arc::clone(&drops))].into();
291 let values = unsafe { cvec.into_vec::<DropCounter>() };
292
293 assert_eq!(drops.load(Ordering::SeqCst), 0);
294 drop(values);
295 assert_eq!(drops.load(Ordering::SeqCst), 1);
296 }
297
298 #[rstest]
299 fn as_slice_borrows_without_consuming_caller_storage() {
300 let values = vec![1_u64, 2, 3];
301 let cvec = CVec {
302 ptr: values.as_ptr().cast_mut().cast(),
303 len: values.len(),
304 cap: values.capacity(),
305 };
306
307 let borrowed = unsafe { cvec.as_slice::<u64>() };
308
309 assert_eq!(borrowed, values);
310 assert_eq!(values, [1, 2, 3]);
311 }
312}