nautilus_core/ffi/cvec.rs
1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3// https://poseitrader.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 vector representation.
27//! * `cvec_drop` – free a vector that was obtained from Rust.
28//!
29//! All other manipulation happens on the Rust side before relinquishing ownership. This keeps the
30//! rules for memory safety straightforward: foreign callers must treat the memory region pointed
31//! to by `ptr` as *opaque* and interact with it solely through the functions provided here.
32
33use std::{ffi::c_void, fmt::Display, ptr::null};
34
35/// `CVec` is a C compatible struct that stores an opaque pointer to a block of
36/// memory, it's length and the capacity of the vector it was allocated from.
37///
38/// # Safety
39///
40/// Changing the values here may lead to undefined behavior when the memory is dropped.
41#[repr(C)]
42#[derive(Clone, Copy, Debug)]
43pub struct CVec {
44 /// Opaque pointer to block of memory storing elements to access the
45 /// elements cast it to the underlying type.
46 pub ptr: *mut c_void,
47 /// The number of elements in the block.
48 pub len: usize,
49 /// The capacity of vector from which it was allocated.
50 /// Used when deallocating the memory
51 pub cap: usize,
52}
53
54/// Empty derivation for Send to satisfy `pyclass` requirements
55/// however this is only designed for single threaded use for now
56unsafe impl Send for CVec {}
57
58impl CVec {
59 /// Returns an empty [`CVec`].
60 ///
61 /// This is primarily useful for constructing a sentinel value that represents the
62 /// absence of data when crossing the FFI boundary.
63 #[must_use]
64 pub const fn empty() -> Self {
65 Self {
66 // Explicitly type cast the pointer to some type to satisfy the
67 // compiler. Since the pointer is null it works for any type.
68 ptr: null::<bool>() as *mut c_void,
69 len: 0,
70 cap: 0,
71 }
72 }
73}
74
75/// Consumes and leaks the Vec, returning a mutable pointer to the contents as
76/// a [`CVec`]. The memory has been leaked and now exists for the lifetime of the
77/// program unless dropped manually.
78/// Note: drop the memory by reconstructing the vec using `from_raw_parts` method
79/// as shown in the test below.
80impl<T> From<Vec<T>> for CVec {
81 fn from(mut data: Vec<T>) -> Self {
82 if data.is_empty() {
83 Self::empty()
84 } else {
85 let len = data.len();
86 let cap = data.capacity();
87 let ptr = data.as_mut_ptr();
88 std::mem::forget(data);
89 Self {
90 ptr: ptr.cast::<std::ffi::c_void>(),
91 len,
92 cap,
93 }
94 }
95 }
96}
97
98impl Display for CVec {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 write!(
101 f,
102 "CVec {{ ptr: {:?}, len: {}, cap: {} }}",
103 self.ptr, self.len, self.cap,
104 )
105 }
106}
107
108////////////////////////////////////////////////////////////////////////////////
109// C API
110////////////////////////////////////////////////////////////////////////////////
111/// Free the heap allocation represented by `cvec`.
112///
113/// # Safety
114///
115/// The pointer **must** either originate from the Rust side through the `From<Vec<T>>`
116/// implementation or be the return value of one of the exported functions in this module. It is
117/// undefined behaviour to pass an arbitrary or already-freed pointer.
118#[cfg(feature = "ffi")]
119#[unsafe(no_mangle)]
120pub extern "C" fn cvec_drop(cvec: CVec) {
121 let CVec { ptr, len, cap } = cvec;
122
123 // SAFETY: CVec currently only supports u8 data through FFI.
124 // The generic From<Vec<T>> implementation should only be used internally
125 // where the caller ensures proper type-matched deallocation.
126 // For FFI boundaries, we standardize on u8 to avoid type confusion.
127 let data: Vec<u8> = unsafe { Vec::from_raw_parts(ptr.cast::<u8>(), len, cap) };
128 drop(data); // Memory freed here
129}
130
131/// Construct a new *empty* [`CVec`] value for use as initialiser/sentinel in foreign code.
132#[cfg(feature = "ffi")]
133#[unsafe(no_mangle)]
134pub const extern "C" fn cvec_new() -> CVec {
135 CVec::empty()
136}
137
138#[cfg(test)]
139mod tests {
140 use rstest::*;
141
142 use super::CVec;
143
144 /// Access values from a vector converted into a [`CVec`].
145 #[rstest]
146 #[allow(unused_assignments)]
147 fn access_values_test() {
148 let test_data = vec![1_u64, 2, 3];
149 let mut vec_len = 0;
150 let mut vec_cap = 0;
151 let cvec: CVec = {
152 let data = test_data.clone();
153 vec_len = data.len();
154 vec_cap = data.capacity();
155 data.into()
156 };
157
158 let CVec { ptr, len, cap } = cvec;
159 assert_eq!(len, vec_len);
160 assert_eq!(cap, vec_cap);
161
162 let data = ptr.cast::<u64>();
163 unsafe {
164 assert_eq!(*data, test_data[0]);
165 assert_eq!(*data.add(1), test_data[1]);
166 assert_eq!(*data.add(2), test_data[2]);
167 }
168
169 unsafe {
170 // reconstruct the struct and drop the memory to deallocate
171 let _ = Vec::from_raw_parts(ptr.cast::<u64>(), len, cap);
172 }
173 }
174
175 /// After deallocating the vector the block of memory may not
176 /// contain the same values.
177 #[rstest]
178 #[ignore = "Flaky on some platforms"]
179 fn drop_test() {
180 let test_data = vec![1, 2, 3];
181 let cvec: CVec = {
182 let data = test_data.clone();
183 data.into()
184 };
185
186 let CVec { ptr, len, cap } = cvec;
187 let data = ptr.cast::<u64>();
188
189 unsafe {
190 let data: Vec<u64> = Vec::from_raw_parts(ptr.cast::<u64>(), len, cap);
191 drop(data);
192 }
193
194 unsafe {
195 assert_ne!(*data, test_data[0]);
196 assert_ne!(*data.add(1), test_data[1]);
197 assert_ne!(*data.add(2), test_data[2]);
198 }
199 }
200
201 /// An empty vector gets converted to a null pointer wrapped in a [`CVec`].
202 #[rstest]
203 fn empty_vec_should_give_null_ptr() {
204 let data: Vec<u64> = vec![];
205 let cvec: CVec = data.into();
206 assert_eq!(cvec.ptr.cast::<u64>(), std::ptr::null_mut::<u64>());
207 }
208}