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 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::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(mut data: Vec<T>) -> Self {
168 if data.is_empty() {
169 Self::empty()
170 } else {
171 let len = data.len();
172 let cap = data.capacity();
173 let ptr = data.as_mut_ptr();
174 #[allow(
175 clippy::mem_forget,
176 reason = "intentional ownership transfer to C; matching CVec::drop reclaims via Vec::from_raw_parts"
177 )]
178 std::mem::forget(data);
179 Self {
180 ptr: ptr.cast::<std::ffi::c_void>(),
181 len,
182 cap,
183 }
184 }
185 }
186}
187
188impl Display for CVec {
189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 write!(
191 f,
192 "CVec {{ ptr: {:?}, len: {}, cap: {} }}",
193 self.ptr, self.len, self.cap,
194 )
195 }
196}
197
198////////////////////////////////////////////////////////////////////////////////
199// C API
200////////////////////////////////////////////////////////////////////////////////
201
202/// Construct a new *empty* [`CVec`] value for use as initialiser/sentinel in foreign code.
203#[cfg(feature = "ffi")]
204#[unsafe(no_mangle)]
205pub extern "C" fn cvec_new() -> CVec {
206 abort_on_panic(CVec::empty)
207}
208
209#[cfg(test)]
210mod tests {
211 use std::sync::{
212 Arc,
213 atomic::{AtomicUsize, Ordering},
214 };
215
216 use rstest::*;
217
218 use super::CVec;
219
220 /// Access values from a vector converted into a [`CVec`].
221 #[rstest]
222 #[allow(unused_assignments)]
223 fn access_values_test() {
224 let test_data = vec![1_u64, 2, 3];
225 let mut vec_len = 0;
226 let mut vec_cap = 0;
227 let cvec: CVec = {
228 let data = test_data.clone();
229 vec_len = data.len();
230 vec_cap = data.capacity();
231 data.into()
232 };
233
234 assert_eq!(cvec.len, vec_len);
235 assert_eq!(cvec.cap, vec_cap);
236
237 let data = unsafe { cvec.into_vec::<u64>() };
238 assert_eq!(data, test_data);
239 }
240
241 /// An empty vector gets converted to a dangling (non-null) pointer in a [`CVec`].
242 #[rstest]
243 fn empty_vec_should_give_dangling_ptr() {
244 let data: Vec<u64> = vec![];
245 let cvec: CVec = data.into();
246 assert!(!cvec.ptr.is_null());
247 assert_eq!(cvec.len, 0);
248 assert_eq!(cvec.cap, 0);
249 }
250
251 #[repr(align(64))]
252 struct Aligned;
253
254 #[rstest]
255 #[case(CVec::empty())]
256 #[case(Vec::<u64>::new().into())]
257 fn empty_into_vec_does_not_inspect_pointer(#[case] cvec: CVec) {
258 let values = unsafe { cvec.into_vec::<u64>() };
259 assert!(values.is_empty());
260 }
261
262 #[rstest]
263 fn aligned_empty_into_vec_does_not_reconstruct_pointer() {
264 let values = unsafe { CVec::empty().into_vec::<Aligned>() };
265 assert!(values.is_empty());
266 }
267
268 #[rstest]
269 fn aligned_empty_as_slice_does_not_inspect_pointer() {
270 let cvec = CVec::empty();
271 let values = unsafe { cvec.as_slice::<Aligned>() };
272 assert!(values.is_empty());
273 }
274
275 #[rstest]
276 fn non_empty_into_vec_round_trips_and_drops_once() {
277 struct DropCounter(Arc<AtomicUsize>);
278
279 impl Drop for DropCounter {
280 fn drop(&mut self) {
281 self.0.fetch_add(1, Ordering::SeqCst);
282 }
283 }
284
285 let drops = Arc::new(AtomicUsize::new(0));
286 let cvec: CVec = vec![DropCounter(Arc::clone(&drops))].into();
287 let values = unsafe { cvec.into_vec::<DropCounter>() };
288
289 assert_eq!(drops.load(Ordering::SeqCst), 0);
290 drop(values);
291 assert_eq!(drops.load(Ordering::SeqCst), 1);
292 }
293
294 #[rstest]
295 fn as_slice_borrows_without_consuming_caller_storage() {
296 let values = vec![1_u64, 2, 3];
297 let cvec = CVec {
298 ptr: values.as_ptr().cast_mut().cast(),
299 len: values.len(),
300 cap: values.capacity(),
301 };
302
303 let borrowed = unsafe { cvec.as_slice::<u64>() };
304
305 assert_eq!(borrowed, values);
306 assert_eq!(values, [1, 2, 3]);
307 }
308}