1use std::{
17 collections::hash_map::DefaultHasher,
18 hash::{Hash, Hasher},
19};
20
21use nautilus_core::{UnixNanos, ffi::abort_on_panic};
22
23use crate::{
24 data::depth::{DEPTH10_LEN, OrderBookDepth},
25 ffi::data::order::BookOrderFfi,
26 identifiers::InstrumentId,
27};
28
29#[repr(C)]
31#[derive(Clone, Copy, Debug)]
32pub struct OrderBookDepth10Ffi {
33 pub instrument_id: InstrumentId,
34 pub bids: [BookOrderFfi; DEPTH10_LEN],
35 pub asks: [BookOrderFfi; DEPTH10_LEN],
36 pub bid_counts: [u32; DEPTH10_LEN],
37 pub ask_counts: [u32; DEPTH10_LEN],
38 pub flags: u8,
39 pub sequence: u64,
40 pub ts_event: UnixNanos,
41 pub ts_init: UnixNanos,
42}
43
44impl From<OrderBookDepth10Ffi> for OrderBookDepth {
45 fn from(value: OrderBookDepth10Ffi) -> Self {
46 Self {
47 instrument_id: value.instrument_id,
48 bids: value.bids.map(Into::into).into(),
49 asks: value.asks.map(Into::into).into(),
50 bid_counts: value.bid_counts.into(),
51 ask_counts: value.ask_counts.into(),
52 flags: value.flags,
53 sequence: value.sequence,
54 ts_event: value.ts_event,
55 ts_init: value.ts_init,
56 }
57 }
58}
59
60impl TryFrom<OrderBookDepth> for OrderBookDepth10Ffi {
61 type Error = anyhow::Error;
62
63 fn try_from(value: OrderBookDepth) -> Result<Self, Self::Error> {
64 anyhow::ensure!(
65 [
66 value.bids.len(),
67 value.asks.len(),
68 value.bid_counts.len(),
69 value.ask_counts.len()
70 ]
71 .into_iter()
72 .all(|len| len == DEPTH10_LEN),
73 "The legacy depth FFI requires exactly ten levels per side"
74 );
75 Ok(Self {
76 instrument_id: value.instrument_id,
77 bids: std::array::from_fn(|i| value.bids[i].into()),
78 asks: std::array::from_fn(|i| value.asks[i].into()),
79 bid_counts: std::array::from_fn(|i| value.bid_counts[i]),
80 ask_counts: std::array::from_fn(|i| value.ask_counts[i]),
81 flags: value.flags,
82 sequence: value.sequence,
83 ts_event: value.ts_event,
84 ts_init: value.ts_init,
85 })
86 }
87}
88
89#[unsafe(no_mangle)]
99#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
100pub unsafe extern "C" fn orderbook_depth10_new(
101 instrument_id: InstrumentId,
102 bids_ptr: *const BookOrderFfi,
103 asks_ptr: *const BookOrderFfi,
104 bid_counts_ptr: *const u32,
105 ask_counts_ptr: *const u32,
106 flags: u8,
107 sequence: u64,
108 ts_event: UnixNanos,
109 ts_init: UnixNanos,
110) -> OrderBookDepth10Ffi {
111 abort_on_panic(|| {
112 assert!(!bids_ptr.is_null());
115 assert!(!asks_ptr.is_null());
116 assert!(!bid_counts_ptr.is_null());
117 assert!(!ask_counts_ptr.is_null());
118
119 let bids_slice = unsafe { std::slice::from_raw_parts(bids_ptr, DEPTH10_LEN) };
120 let asks_slice = unsafe { std::slice::from_raw_parts(asks_ptr, DEPTH10_LEN) };
121 let bids: [BookOrderFfi; DEPTH10_LEN] = bids_slice.try_into().expect("Slice length != 10");
122 let asks: [BookOrderFfi; DEPTH10_LEN] = asks_slice.try_into().expect("Slice length != 10");
123
124 let bid_counts_slice = unsafe { std::slice::from_raw_parts(bid_counts_ptr, DEPTH10_LEN) };
125 let ask_counts_slice = unsafe { std::slice::from_raw_parts(ask_counts_ptr, DEPTH10_LEN) };
126 let bid_counts: [u32; DEPTH10_LEN] =
127 bid_counts_slice.try_into().expect("Slice length != 10");
128 let ask_counts: [u32; DEPTH10_LEN] =
129 ask_counts_slice.try_into().expect("Slice length != 10");
130
131 OrderBookDepth10Ffi {
132 instrument_id,
133 bids,
134 asks,
135 bid_counts,
136 ask_counts,
137 flags,
138 sequence,
139 ts_event,
140 ts_init,
141 }
142 })
143}
144
145#[unsafe(no_mangle)]
146#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
147pub extern "C" fn orderbook_depth10_clone(depth: &OrderBookDepth10Ffi) -> OrderBookDepth10Ffi {
148 *depth
149}
150
151#[unsafe(no_mangle)]
152pub extern "C" fn orderbook_depth10_eq(lhs: &OrderBookDepth10Ffi, rhs: &OrderBookDepth10Ffi) -> u8 {
153 u8::from(OrderBookDepth::from(*lhs) == OrderBookDepth::from(*rhs))
154}
155
156#[unsafe(no_mangle)]
157pub extern "C" fn orderbook_depth10_hash(delta: &OrderBookDepth10Ffi) -> u64 {
158 let mut hasher = DefaultHasher::new();
159 OrderBookDepth::from(*delta).hash(&mut hasher);
160 hasher.finish()
161}
162
163#[unsafe(no_mangle)]
164pub extern "C" fn orderbook_depth10_bids_array(depth: &OrderBookDepth10Ffi) -> *const BookOrderFfi {
165 depth.bids.as_ptr()
166}
167
168#[unsafe(no_mangle)]
169pub extern "C" fn orderbook_depth10_asks_array(depth: &OrderBookDepth10Ffi) -> *const BookOrderFfi {
170 depth.asks.as_ptr()
171}
172
173#[unsafe(no_mangle)]
174pub extern "C" fn orderbook_depth10_bid_counts_array(depth: &OrderBookDepth10Ffi) -> *const u32 {
175 depth.bid_counts.as_ptr()
176}
177
178#[unsafe(no_mangle)]
179pub extern "C" fn orderbook_depth10_ask_counts_array(depth: &OrderBookDepth10Ffi) -> *const u32 {
180 depth.ask_counts.as_ptr()
181}
182
183#[cfg(test)]
184mod tests {
185 use rstest::rstest;
186
187 use super::*;
188 use crate::data::{BookOrder, stubs::stub_depth10};
189
190 #[rstest]
191 #[case::populated(10, 10)]
192 #[case::padded(3, 6)]
193 #[case::empty(0, 0)]
194 fn legacy_constructor_preserves_slots_and_metadata(
195 #[case] bid_levels: usize,
196 #[case] ask_levels: usize,
197 ) {
198 let depth = stub_depth10();
199 let bids: [BookOrderFfi; DEPTH10_LEN] = std::array::from_fn(|i| {
200 if i < bid_levels {
201 depth.bids[i].into()
202 } else {
203 BookOrder::default().into()
204 }
205 });
206 let asks: [BookOrderFfi; DEPTH10_LEN] = std::array::from_fn(|i| {
207 if i < ask_levels {
208 depth.asks[i].into()
209 } else {
210 BookOrder::default().into()
211 }
212 });
213 let bid_counts = std::array::from_fn::<_, DEPTH10_LEN, _>(|i| i as u32 + 11);
214 let ask_counts = std::array::from_fn::<_, DEPTH10_LEN, _>(|i| i as u32 + 31);
215
216 let actual = unsafe {
218 orderbook_depth10_new(
219 depth.instrument_id,
220 bids.as_ptr(),
221 asks.as_ptr(),
222 bid_counts.as_ptr(),
223 ask_counts.as_ptr(),
224 17,
225 23,
226 UnixNanos::from(41),
227 UnixNanos::from(43),
228 )
229 };
230
231 assert_eq!(actual.instrument_id, depth.instrument_id);
232 for (actual, expected) in actual
233 .bids
234 .into_iter()
235 .chain(actual.asks)
236 .zip(bids.into_iter().chain(asks))
237 {
238 assert_eq!(
239 (
240 actual.side,
241 actual.price.raw,
242 actual.price.precision,
243 actual.size.raw,
244 actual.size.precision,
245 actual.order_id
246 ),
247 (
248 expected.side,
249 expected.price.raw,
250 expected.price.precision,
251 expected.size.raw,
252 expected.size.precision,
253 expected.order_id
254 ),
255 );
256 }
257 assert_eq!(actual.bid_counts, bid_counts);
258 assert_eq!(actual.ask_counts, ask_counts);
259 assert_eq!(
260 (
261 actual.flags,
262 actual.sequence,
263 actual.ts_event,
264 actual.ts_init
265 ),
266 (17, 23, UnixNanos::from(41), UnixNanos::from(43))
267 );
268 }
269
270 #[rstest]
271 fn legacy_depth_conversion_preserves_all_fields() {
272 let mut depth = stub_depth10();
273 depth.flags = 31;
274 depth.sequence = 23;
275 depth.ts_event = UnixNanos::from(41);
276 depth.ts_init = UnixNanos::from(43);
277 depth.bid_counts = (1..=10).collect();
278 depth.ask_counts = (11..=20).collect();
279 let ffi = OrderBookDepth10Ffi::try_from(depth.clone()).unwrap();
280 assert_eq!(ffi.instrument_id, depth.instrument_id);
281 assert_eq!(
282 ffi.bids.map(BookOrder::from).as_slice(),
283 depth.bids.as_slice()
284 );
285 assert_eq!(
286 ffi.asks.map(BookOrder::from).as_slice(),
287 depth.asks.as_slice()
288 );
289 assert_eq!(ffi.bid_counts.as_slice(), depth.bid_counts.as_slice());
290 assert_eq!(ffi.ask_counts.as_slice(), depth.ask_counts.as_slice());
291 assert_eq!(
292 (ffi.flags, ffi.sequence, ffi.ts_event, ffi.ts_init),
293 (31, 23, UnixNanos::from(41), UnixNanos::from(43))
294 );
295 assert_eq!(OrderBookDepth::from(ffi), depth);
296 }
297
298 #[rstest]
299 #[case(0)]
300 #[case(9)]
301 #[case(11)]
302 fn legacy_depth_conversion_rejects_other_depths(#[case] levels: usize) {
303 let mut depth = stub_depth10();
304 depth.bids.resize(levels, depth.bids[0]);
305 depth.bid_counts.resize(levels, 1);
306 let error = OrderBookDepth10Ffi::try_from(depth).unwrap_err();
307 assert_eq!(
308 error.to_string(),
309 "The legacy depth FFI requires exactly ten levels per side"
310 );
311 }
312}