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}