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(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
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
78/// Consumes and leaks the Vec, returning a mutable pointer to the contents as
79/// a [`CVec`]. The memory has been leaked and now exists for the lifetime of the
80/// program unless dropped manually.
81/// Note: drop the memory by reconstructing the vec using `from_raw_parts` method
82/// as shown in the test below.
83impl<T> From<Vec<T>> for CVec {
84    fn from(mut data: Vec<T>) -> Self {
85        if data.is_empty() {
86            Self::empty()
87        } else {
88            let len = data.len();
89            let cap = data.capacity();
90            let ptr = data.as_mut_ptr();
91            #[allow(
92                clippy::mem_forget,
93                reason = "intentional ownership transfer to C; matching CVec::drop reclaims via Vec::from_raw_parts"
94            )]
95            std::mem::forget(data);
96            Self {
97                ptr: ptr.cast::<std::ffi::c_void>(),
98                len,
99                cap,
100            }
101        }
102    }
103}
104
105impl Display for CVec {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        write!(
108            f,
109            "CVec {{ ptr: {:?}, len: {}, cap: {} }}",
110            self.ptr, self.len, self.cap,
111        )
112    }
113}
114
115////////////////////////////////////////////////////////////////////////////////
116// C API
117////////////////////////////////////////////////////////////////////////////////
118
119/// Construct a new *empty* [`CVec`] value for use as initialiser/sentinel in foreign code.
120#[cfg(feature = "ffi")]
121#[unsafe(no_mangle)]
122pub extern "C" fn cvec_new() -> CVec {
123    abort_on_panic(CVec::empty)
124}
125
126#[cfg(test)]
127mod tests {
128    use rstest::*;
129
130    use super::CVec;
131
132    /// Access values from a vector converted into a [`CVec`].
133    #[rstest]
134    #[allow(unused_assignments)]
135    fn access_values_test() {
136        let test_data = vec![1_u64, 2, 3];
137        let mut vec_len = 0;
138        let mut vec_cap = 0;
139        let cvec: CVec = {
140            let data = test_data.clone();
141            vec_len = data.len();
142            vec_cap = data.capacity();
143            data.into()
144        };
145
146        let CVec { ptr, len, cap } = cvec;
147        assert_eq!(len, vec_len);
148        assert_eq!(cap, vec_cap);
149
150        let data = ptr.cast::<u64>();
151        // SAFETY: data points to a valid Vec<u64> of length 3 owned by `cvec`
152        #[allow(
153            clippy::multiple_unsafe_ops_per_block,
154            reason = "test asserts on three pointer reads in sequence"
155        )]
156        unsafe {
157            assert_eq!(*data, test_data[0]);
158            assert_eq!(*data.add(1), test_data[1]);
159            assert_eq!(*data.add(2), test_data[2]);
160        }
161
162        unsafe {
163            // reconstruct the struct and drop the memory to deallocate
164            let _ = Vec::from_raw_parts(ptr.cast::<u64>(), len, cap);
165        }
166    }
167
168    /// An empty vector gets converted to a dangling (non-null) pointer in a [`CVec`].
169    #[rstest]
170    fn empty_vec_should_give_dangling_ptr() {
171        let data: Vec<u64> = vec![];
172        let cvec: CVec = data.into();
173        assert!(!cvec.ptr.is_null());
174        assert_eq!(cvec.len, 0);
175        assert_eq!(cvec.cap, 0);
176    }
177}