Skip to main content

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}