1#![allow(unsafe_code)]
24
25use crate::{
26 NAUTILUS_PLUGIN_ABI_VERSION,
27 boundary::{BorrowedStr, OwnedBytes, PluginResult, Slice},
28 surfaces::commands::{
29 CancelAllOrdersHandle, CancelOrderHandle, CancelOrdersHandle, CloseAllPositionsHandle,
30 ClosePositionHandle, ModifyOrderHandle, QueryAccountHandle, QueryOrderHandle,
31 SubmitOrderHandle, SubmitOrderListHandle,
32 },
33};
34
35#[repr(u8)]
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum HostLogLevel {
40 Error = 1,
41 Warn = 2,
42 Info = 3,
43 Debug = 4,
44 Trace = 5,
45}
46
47#[repr(C)]
55pub struct HostContext {
56 _opaque: [u8; 0],
57}
58
59#[repr(C)]
64pub struct ControllerHostContext {
65 _opaque: [u8; 0],
66}
67
68#[repr(C)]
74pub struct ControllerHostVTable {
75 pub abi_version: u32,
77
78 pub create_plugin_strategy: unsafe extern "C" fn(
80 ctx: *const ControllerHostContext,
81 request_json: BorrowedStr<'_>,
82 ) -> PluginResult<OwnedBytes>,
83
84 pub start_strategy: unsafe extern "C" fn(
86 ctx: *const ControllerHostContext,
87 request_json: BorrowedStr<'_>,
88 ) -> PluginResult<OwnedBytes>,
89
90 pub stop_strategy: unsafe extern "C" fn(
92 ctx: *const ControllerHostContext,
93 request_json: BorrowedStr<'_>,
94 ) -> PluginResult<OwnedBytes>,
95
96 pub exit_market: unsafe extern "C" fn(
98 ctx: *const ControllerHostContext,
99 request_json: BorrowedStr<'_>,
100 ) -> PluginResult<OwnedBytes>,
101
102 pub remove_strategy: unsafe extern "C" fn(
104 ctx: *const ControllerHostContext,
105 request_json: BorrowedStr<'_>,
106 ) -> PluginResult<OwnedBytes>,
107
108 pub instrument_exists: unsafe extern "C" fn(
110 ctx: *const ControllerHostContext,
111 request_json: BorrowedStr<'_>,
112 ) -> PluginResult<OwnedBytes>,
113
114 pub log: unsafe extern "C" fn(
116 ctx: *const ControllerHostContext,
117 request_json: BorrowedStr<'_>,
118 ) -> PluginResult<OwnedBytes>,
119
120 pub clock_now_ns: unsafe extern "C" fn(
122 ctx: *const ControllerHostContext,
123 request_json: BorrowedStr<'_>,
124 ) -> PluginResult<OwnedBytes>,
125}
126
127impl ControllerHostVTable {
128 #[must_use]
130 pub fn matches_compiled_abi(&self) -> bool {
131 self.abi_version == NAUTILUS_PLUGIN_ABI_VERSION
132 }
133}
134
135unsafe impl Send for ControllerHostVTable {}
138unsafe impl Sync for ControllerHostVTable {}
140
141#[repr(C)]
148pub struct HostVTable {
149 pub abi_version: u32,
151
152 pub clock_now_ns: unsafe extern "C" fn() -> u64,
154
155 pub log: unsafe extern "C" fn(
159 level: HostLogLevel,
160 target: BorrowedStr<'_>,
161 message: BorrowedStr<'_>,
162 ),
163
164 pub cache_instrument: unsafe extern "C" fn(
168 ctx: *const HostContext,
169 instrument_id: BorrowedStr<'_>,
170 ) -> PluginResult<OwnedBytes>,
171
172 pub cache_account: unsafe extern "C" fn(
176 ctx: *const HostContext,
177 account_id: BorrowedStr<'_>,
178 ) -> PluginResult<OwnedBytes>,
179
180 pub cache_order: unsafe extern "C" fn(
184 ctx: *const HostContext,
185 client_order_id: BorrowedStr<'_>,
186 ) -> PluginResult<OwnedBytes>,
187
188 pub cache_position: unsafe extern "C" fn(
192 ctx: *const HostContext,
193 position_id: BorrowedStr<'_>,
194 ) -> PluginResult<OwnedBytes>,
195
196 pub cache_orders_for_strategy: unsafe extern "C" fn(
200 ctx: *const HostContext,
201 strategy_id: BorrowedStr<'_>,
202 ) -> PluginResult<OwnedBytes>,
203
204 pub cache_positions_for_strategy: unsafe extern "C" fn(
208 ctx: *const HostContext,
209 strategy_id: BorrowedStr<'_>,
210 ) -> PluginResult<OwnedBytes>,
211
212 pub subscribe_quotes: unsafe extern "C" fn(
214 ctx: *const HostContext,
215 instrument_id: BorrowedStr<'_>,
216 client_id: BorrowedStr<'_>,
217 params_json: BorrowedStr<'_>,
218 ) -> PluginResult<()>,
219
220 pub unsubscribe_quotes: unsafe extern "C" fn(
222 ctx: *const HostContext,
223 instrument_id: BorrowedStr<'_>,
224 client_id: BorrowedStr<'_>,
225 params_json: BorrowedStr<'_>,
226 ) -> PluginResult<()>,
227
228 pub subscribe_trades: unsafe extern "C" fn(
230 ctx: *const HostContext,
231 instrument_id: BorrowedStr<'_>,
232 client_id: BorrowedStr<'_>,
233 params_json: BorrowedStr<'_>,
234 ) -> PluginResult<()>,
235
236 pub unsubscribe_trades: unsafe extern "C" fn(
238 ctx: *const HostContext,
239 instrument_id: BorrowedStr<'_>,
240 client_id: BorrowedStr<'_>,
241 params_json: BorrowedStr<'_>,
242 ) -> PluginResult<()>,
243
244 pub subscribe_bars: unsafe extern "C" fn(
246 ctx: *const HostContext,
247 bar_type: BorrowedStr<'_>,
248 client_id: BorrowedStr<'_>,
249 params_json: BorrowedStr<'_>,
250 ) -> PluginResult<()>,
251
252 pub unsubscribe_bars: unsafe extern "C" fn(
254 ctx: *const HostContext,
255 bar_type: BorrowedStr<'_>,
256 client_id: BorrowedStr<'_>,
257 params_json: BorrowedStr<'_>,
258 ) -> PluginResult<()>,
259
260 pub subscribe_book_deltas: unsafe extern "C" fn(
265 ctx: *const HostContext,
266 instrument_id: BorrowedStr<'_>,
267 book_type: u8,
268 depth: usize,
269 client_id: BorrowedStr<'_>,
270 managed: u8,
271 params_json: BorrowedStr<'_>,
272 ) -> PluginResult<()>,
273
274 pub unsubscribe_book_deltas: unsafe extern "C" fn(
276 ctx: *const HostContext,
277 instrument_id: BorrowedStr<'_>,
278 client_id: BorrowedStr<'_>,
279 params_json: BorrowedStr<'_>,
280 ) -> PluginResult<()>,
281
282 pub subscribe_book_at_interval: unsafe extern "C" fn(
287 ctx: *const HostContext,
288 instrument_id: BorrowedStr<'_>,
289 book_type: u8,
290 depth: usize,
291 interval_ms: usize,
292 client_id: BorrowedStr<'_>,
293 params_json: BorrowedStr<'_>,
294 ) -> PluginResult<()>,
295
296 pub unsubscribe_book_at_interval: unsafe extern "C" fn(
300 ctx: *const HostContext,
301 instrument_id: BorrowedStr<'_>,
302 interval_ms: usize,
303 client_id: BorrowedStr<'_>,
304 params_json: BorrowedStr<'_>,
305 ) -> PluginResult<()>,
306
307 pub msgbus_publish: unsafe extern "C" fn(
311 ctx: *const HostContext,
312 topic: BorrowedStr<'_>,
313 payload: Slice<'_, u8>,
314 ) -> PluginResult<()>,
315
316 pub set_time_alert: unsafe extern "C" fn(
318 ctx: *const HostContext,
319 name: BorrowedStr<'_>,
320 alert_time_ns: u64,
321 allow_past: u8,
322 ) -> PluginResult<()>,
323
324 pub set_timer: unsafe extern "C" fn(
328 ctx: *const HostContext,
329 name: BorrowedStr<'_>,
330 interval_ns: u64,
331 start_time_ns: u64,
332 stop_time_ns: u64,
333 allow_past: u8,
334 fire_immediately: u8,
335 ) -> PluginResult<()>,
336
337 pub cancel_timer:
339 unsafe extern "C" fn(ctx: *const HostContext, name: BorrowedStr<'_>) -> PluginResult<()>,
340
341 pub submit_order: unsafe extern "C" fn(
350 ctx: *const HostContext,
351 command: *const SubmitOrderHandle,
352 ) -> PluginResult<()>,
353
354 pub cancel_order: unsafe extern "C" fn(
362 ctx: *const HostContext,
363 command: *const CancelOrderHandle,
364 ) -> PluginResult<()>,
365
366 pub modify_order: unsafe extern "C" fn(
374 ctx: *const HostContext,
375 command: *const ModifyOrderHandle,
376 ) -> PluginResult<()>,
377
378 pub submit_order_list: unsafe extern "C" fn(
386 ctx: *const HostContext,
387 command: *const SubmitOrderListHandle,
388 ) -> PluginResult<()>,
389
390 pub cancel_orders: unsafe extern "C" fn(
396 ctx: *const HostContext,
397 command: *const CancelOrdersHandle,
398 ) -> PluginResult<()>,
399
400 pub cancel_all_orders: unsafe extern "C" fn(
408 ctx: *const HostContext,
409 command: *const CancelAllOrdersHandle,
410 ) -> PluginResult<()>,
411
412 pub close_position: unsafe extern "C" fn(
421 ctx: *const HostContext,
422 command: *const ClosePositionHandle,
423 ) -> PluginResult<()>,
424
425 pub close_all_positions: unsafe extern "C" fn(
434 ctx: *const HostContext,
435 command: *const CloseAllPositionsHandle,
436 ) -> PluginResult<()>,
437
438 pub query_account: unsafe extern "C" fn(
447 ctx: *const HostContext,
448 command: *const QueryAccountHandle,
449 ) -> PluginResult<()>,
450
451 pub query_order: unsafe extern "C" fn(
460 ctx: *const HostContext,
461 command: *const QueryOrderHandle,
462 ) -> PluginResult<()>,
463}
464
465impl HostVTable {
466 #[must_use]
471 pub fn matches_compiled_abi(&self) -> bool {
472 self.abi_version == NAUTILUS_PLUGIN_ABI_VERSION
473 }
474
475 pub unsafe fn now_ns(&self) -> u64 {
482 unsafe { (self.clock_now_ns)() }
484 }
485
486 pub unsafe fn log_message(&self, level: HostLogLevel, target: &str, message: &str) {
492 unsafe {
494 (self.log)(
495 level,
496 BorrowedStr::from_str(target),
497 BorrowedStr::from_str(message),
498 );
499 }
500 }
501}
502
503unsafe impl Send for HostVTable {}
506unsafe impl Sync for HostVTable {}
508
509#[cfg(test)]
510mod tests {
511 use std::sync::{
512 Mutex, MutexGuard, OnceLock,
513 atomic::{AtomicU8, AtomicU64, Ordering},
514 };
515
516 use rstest::rstest;
517
518 use super::*;
519 use crate::boundary::{OwnedBytes, PluginResult, Slice};
520
521 static CLOCK_VALUE: AtomicU64 = AtomicU64::new(0);
522 static LOG_LEVEL_OBSERVED: AtomicU8 = AtomicU8::new(0);
523
524 fn shared_state_lock() -> MutexGuard<'static, ()> {
529 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
530 LOCK.get_or_init(|| Mutex::new(()))
531 .lock()
532 .unwrap_or_else(|p| p.into_inner())
533 }
534
535 unsafe extern "C" fn fixed_clock_now_ns() -> u64 {
536 CLOCK_VALUE.load(Ordering::SeqCst)
537 }
538
539 unsafe extern "C" fn recording_log(
540 level: HostLogLevel,
541 _target: BorrowedStr<'_>,
542 _message: BorrowedStr<'_>,
543 ) {
544 LOG_LEVEL_OBSERVED.store(level as u8, Ordering::SeqCst);
545 }
546
547 macro_rules! stub_bytes {
548 ($name:ident) => {
549 unsafe extern "C" fn $name(
550 _ctx: *const HostContext,
551 _a: BorrowedStr<'_>,
552 ) -> PluginResult<OwnedBytes> {
553 PluginResult::Ok(OwnedBytes::empty())
554 }
555 };
556 }
557
558 macro_rules! stub_controller_bytes {
559 ($name:ident) => {
560 unsafe extern "C" fn $name(
561 _ctx: *const ControllerHostContext,
562 _a: BorrowedStr<'_>,
563 ) -> PluginResult<OwnedBytes> {
564 PluginResult::Ok(OwnedBytes::empty())
565 }
566 };
567 }
568
569 macro_rules! stub_unit {
570 ($name:ident, ($($arg:ident : $ty:ty),* $(,)?)) => {
571 unsafe extern "C" fn $name($($arg: $ty),*) -> PluginResult<()> {
572 $(let _ = $arg;)*
573 PluginResult::Ok(())
574 }
575 };
576 }
577
578 stub_bytes!(stub_cache_instrument);
579 stub_bytes!(stub_cache_account);
580 stub_bytes!(stub_cache_order);
581 stub_bytes!(stub_cache_position);
582 stub_bytes!(stub_cache_orders_for_strategy);
583 stub_bytes!(stub_cache_positions_for_strategy);
584 stub_controller_bytes!(stub_controller_create_plugin_strategy);
585 stub_controller_bytes!(stub_controller_start_strategy);
586 stub_controller_bytes!(stub_controller_stop_strategy);
587 stub_controller_bytes!(stub_controller_exit_market);
588 stub_controller_bytes!(stub_controller_remove_strategy);
589 stub_controller_bytes!(stub_controller_instrument_exists);
590 stub_controller_bytes!(stub_controller_log);
591 stub_controller_bytes!(stub_controller_clock_now_ns);
592
593 stub_unit!(
594 stub_subscribe,
595 (
596 ctx: *const HostContext,
597 a: BorrowedStr<'_>,
598 b: BorrowedStr<'_>,
599 c: BorrowedStr<'_>,
600 )
601 );
602 stub_unit!(
603 stub_subscribe_book_deltas,
604 (
605 ctx: *const HostContext,
606 a: BorrowedStr<'_>,
607 t: u8,
608 d: usize,
609 b: BorrowedStr<'_>,
610 m: u8,
611 c: BorrowedStr<'_>,
612 )
613 );
614 stub_unit!(
615 stub_subscribe_book_at_interval,
616 (
617 ctx: *const HostContext,
618 a: BorrowedStr<'_>,
619 t: u8,
620 d: usize,
621 i: usize,
622 b: BorrowedStr<'_>,
623 c: BorrowedStr<'_>,
624 )
625 );
626 stub_unit!(
627 stub_unsubscribe_book_at_interval,
628 (
629 ctx: *const HostContext,
630 a: BorrowedStr<'_>,
631 i: usize,
632 b: BorrowedStr<'_>,
633 c: BorrowedStr<'_>,
634 )
635 );
636 stub_unit!(
637 stub_msgbus_publish,
638 (
639 ctx: *const HostContext,
640 t: BorrowedStr<'_>,
641 p: Slice<'_, u8>,
642 )
643 );
644 stub_unit!(
645 stub_set_time_alert,
646 (
647 ctx: *const HostContext,
648 n: BorrowedStr<'_>,
649 a: u64,
650 p: u8,
651 )
652 );
653 stub_unit!(
654 stub_set_timer,
655 (
656 ctx: *const HostContext,
657 n: BorrowedStr<'_>,
658 i: u64,
659 s: u64,
660 e: u64,
661 p: u8,
662 f: u8,
663 )
664 );
665 stub_unit!(stub_cancel_timer, (ctx: *const HostContext, n: BorrowedStr<'_>));
666 stub_unit!(
667 stub_submit_order,
668 (ctx: *const HostContext, c: *const SubmitOrderHandle)
669 );
670 stub_unit!(
671 stub_cancel_order,
672 (ctx: *const HostContext, c: *const CancelOrderHandle)
673 );
674 stub_unit!(
675 stub_modify_order,
676 (ctx: *const HostContext, c: *const ModifyOrderHandle)
677 );
678 stub_unit!(
679 stub_submit_order_list,
680 (ctx: *const HostContext, c: *const SubmitOrderListHandle)
681 );
682 stub_unit!(
683 stub_cancel_orders,
684 (ctx: *const HostContext, c: *const CancelOrdersHandle)
685 );
686 stub_unit!(
687 stub_cancel_all_orders,
688 (ctx: *const HostContext, c: *const CancelAllOrdersHandle)
689 );
690 stub_unit!(
691 stub_close_position,
692 (ctx: *const HostContext, c: *const ClosePositionHandle)
693 );
694 stub_unit!(
695 stub_close_all_positions,
696 (ctx: *const HostContext, c: *const CloseAllPositionsHandle)
697 );
698 stub_unit!(
699 stub_query_account,
700 (ctx: *const HostContext, c: *const QueryAccountHandle)
701 );
702 stub_unit!(
703 stub_query_order,
704 (ctx: *const HostContext, c: *const QueryOrderHandle)
705 );
706
707 fn build_test_host(abi: u32) -> HostVTable {
708 HostVTable {
709 abi_version: abi,
710 clock_now_ns: fixed_clock_now_ns,
711 log: recording_log,
712 cache_instrument: stub_cache_instrument,
713 cache_account: stub_cache_account,
714 cache_order: stub_cache_order,
715 cache_position: stub_cache_position,
716 cache_orders_for_strategy: stub_cache_orders_for_strategy,
717 cache_positions_for_strategy: stub_cache_positions_for_strategy,
718 subscribe_quotes: stub_subscribe,
719 unsubscribe_quotes: stub_subscribe,
720 subscribe_trades: stub_subscribe,
721 unsubscribe_trades: stub_subscribe,
722 subscribe_bars: stub_subscribe,
723 unsubscribe_bars: stub_subscribe,
724 subscribe_book_deltas: stub_subscribe_book_deltas,
725 unsubscribe_book_deltas: stub_subscribe,
726 subscribe_book_at_interval: stub_subscribe_book_at_interval,
727 unsubscribe_book_at_interval: stub_unsubscribe_book_at_interval,
728 msgbus_publish: stub_msgbus_publish,
729 set_time_alert: stub_set_time_alert,
730 set_timer: stub_set_timer,
731 cancel_timer: stub_cancel_timer,
732 submit_order: stub_submit_order,
733 cancel_order: stub_cancel_order,
734 modify_order: stub_modify_order,
735 submit_order_list: stub_submit_order_list,
736 cancel_orders: stub_cancel_orders,
737 cancel_all_orders: stub_cancel_all_orders,
738 close_position: stub_close_position,
739 close_all_positions: stub_close_all_positions,
740 query_account: stub_query_account,
741 query_order: stub_query_order,
742 }
743 }
744
745 fn build_controller_test_host(abi: u32) -> ControllerHostVTable {
746 ControllerHostVTable {
747 abi_version: abi,
748 create_plugin_strategy: stub_controller_create_plugin_strategy,
749 start_strategy: stub_controller_start_strategy,
750 stop_strategy: stub_controller_stop_strategy,
751 exit_market: stub_controller_exit_market,
752 remove_strategy: stub_controller_remove_strategy,
753 instrument_exists: stub_controller_instrument_exists,
754 log: stub_controller_log,
755 clock_now_ns: stub_controller_clock_now_ns,
756 }
757 }
758
759 #[rstest]
760 fn matches_compiled_abi_accepts_compiled_version() {
761 let host = build_test_host(NAUTILUS_PLUGIN_ABI_VERSION);
762 assert!(host.matches_compiled_abi());
763 }
764
765 #[rstest]
766 fn controller_matches_compiled_abi_accepts_compiled_version() {
767 let host = build_controller_test_host(NAUTILUS_PLUGIN_ABI_VERSION);
768 assert!(host.matches_compiled_abi());
769 }
770
771 #[rstest]
772 #[case::off_by_one(NAUTILUS_PLUGIN_ABI_VERSION.wrapping_add(1))]
773 #[case::zero(0)]
774 #[case::max(u32::MAX)]
775 fn matches_compiled_abi_rejects_mismatch(#[case] abi: u32) {
776 let host = build_test_host(abi);
777 assert!(!host.matches_compiled_abi());
778 }
779
780 #[rstest]
781 #[case::off_by_one(NAUTILUS_PLUGIN_ABI_VERSION.wrapping_add(1))]
782 #[case::zero(0)]
783 #[case::max(u32::MAX)]
784 fn controller_matches_compiled_abi_rejects_mismatch(#[case] abi: u32) {
785 let host = build_controller_test_host(abi);
786 assert!(!host.matches_compiled_abi());
787 }
788
789 #[rstest]
790 fn now_ns_calls_clock_function_pointer() {
791 let _g = shared_state_lock();
792 CLOCK_VALUE.store(42_424_242, Ordering::SeqCst);
793 let host = build_test_host(NAUTILUS_PLUGIN_ABI_VERSION);
794 let n = unsafe { host.now_ns() };
797 assert_eq!(n, 42_424_242);
798 }
799
800 #[rstest]
801 #[case::error(HostLogLevel::Error, 1u8)]
802 #[case::warn(HostLogLevel::Warn, 2)]
803 #[case::info(HostLogLevel::Info, 3)]
804 #[case::debug(HostLogLevel::Debug, 4)]
805 #[case::trace(HostLogLevel::Trace, 5)]
806 fn log_message_invokes_log_with_the_right_level(
807 #[case] level: HostLogLevel,
808 #[case] expected_discriminant: u8,
809 ) {
810 let _g = shared_state_lock();
811 LOG_LEVEL_OBSERVED.store(0, Ordering::SeqCst);
812 let host = build_test_host(NAUTILUS_PLUGIN_ABI_VERSION);
813 unsafe { host.log_message(level, "target", "message") };
815 assert_eq!(
816 LOG_LEVEL_OBSERVED.load(Ordering::SeqCst),
817 expected_discriminant
818 );
819 }
820}