#![allow(unsafe_code)]
use crate::{
NAUTILUS_PLUGIN_ABI_VERSION,
boundary::{BorrowedStr, OwnedBytes, PluginResult, Slice},
surfaces::commands::{
CancelAllOrdersHandle, CancelOrderHandle, CancelOrdersHandle, CloseAllPositionsHandle,
ClosePositionHandle, ModifyOrderHandle, QueryAccountHandle, QueryOrderHandle,
SubmitOrderHandle, SubmitOrderListHandle,
},
};
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HostLogLevel {
Error = 1,
Warn = 2,
Info = 3,
Debug = 4,
Trace = 5,
}
#[repr(C)]
pub struct HostContext {
_opaque: [u8; 0],
}
#[repr(C)]
pub struct ControllerHostContext {
_opaque: [u8; 0],
}
#[repr(C)]
pub struct ControllerHostVTable {
pub abi_version: u32,
pub create_plugin_strategy: unsafe extern "C" fn(
ctx: *const ControllerHostContext,
request_json: BorrowedStr<'_>,
) -> PluginResult<OwnedBytes>,
pub start_strategy: unsafe extern "C" fn(
ctx: *const ControllerHostContext,
request_json: BorrowedStr<'_>,
) -> PluginResult<OwnedBytes>,
pub stop_strategy: unsafe extern "C" fn(
ctx: *const ControllerHostContext,
request_json: BorrowedStr<'_>,
) -> PluginResult<OwnedBytes>,
pub exit_market: unsafe extern "C" fn(
ctx: *const ControllerHostContext,
request_json: BorrowedStr<'_>,
) -> PluginResult<OwnedBytes>,
pub remove_strategy: unsafe extern "C" fn(
ctx: *const ControllerHostContext,
request_json: BorrowedStr<'_>,
) -> PluginResult<OwnedBytes>,
pub instrument_exists: unsafe extern "C" fn(
ctx: *const ControllerHostContext,
request_json: BorrowedStr<'_>,
) -> PluginResult<OwnedBytes>,
pub log: unsafe extern "C" fn(
ctx: *const ControllerHostContext,
request_json: BorrowedStr<'_>,
) -> PluginResult<OwnedBytes>,
pub clock_now_ns: unsafe extern "C" fn(
ctx: *const ControllerHostContext,
request_json: BorrowedStr<'_>,
) -> PluginResult<OwnedBytes>,
}
impl ControllerHostVTable {
#[must_use]
pub fn matches_compiled_abi(&self) -> bool {
self.abi_version == NAUTILUS_PLUGIN_ABI_VERSION
}
}
unsafe impl Send for ControllerHostVTable {}
unsafe impl Sync for ControllerHostVTable {}
#[repr(C)]
pub struct HostVTable {
pub abi_version: u32,
pub clock_now_ns: unsafe extern "C" fn() -> u64,
pub log: unsafe extern "C" fn(
level: HostLogLevel,
target: BorrowedStr<'_>,
message: BorrowedStr<'_>,
),
pub cache_instrument: unsafe extern "C" fn(
ctx: *const HostContext,
instrument_id: BorrowedStr<'_>,
) -> PluginResult<OwnedBytes>,
pub cache_account: unsafe extern "C" fn(
ctx: *const HostContext,
account_id: BorrowedStr<'_>,
) -> PluginResult<OwnedBytes>,
pub cache_order: unsafe extern "C" fn(
ctx: *const HostContext,
client_order_id: BorrowedStr<'_>,
) -> PluginResult<OwnedBytes>,
pub cache_position: unsafe extern "C" fn(
ctx: *const HostContext,
position_id: BorrowedStr<'_>,
) -> PluginResult<OwnedBytes>,
pub cache_orders_for_strategy: unsafe extern "C" fn(
ctx: *const HostContext,
strategy_id: BorrowedStr<'_>,
) -> PluginResult<OwnedBytes>,
pub cache_positions_for_strategy: unsafe extern "C" fn(
ctx: *const HostContext,
strategy_id: BorrowedStr<'_>,
) -> PluginResult<OwnedBytes>,
pub subscribe_quotes: unsafe extern "C" fn(
ctx: *const HostContext,
instrument_id: BorrowedStr<'_>,
client_id: BorrowedStr<'_>,
params_json: BorrowedStr<'_>,
) -> PluginResult<()>,
pub unsubscribe_quotes: unsafe extern "C" fn(
ctx: *const HostContext,
instrument_id: BorrowedStr<'_>,
client_id: BorrowedStr<'_>,
params_json: BorrowedStr<'_>,
) -> PluginResult<()>,
pub subscribe_trades: unsafe extern "C" fn(
ctx: *const HostContext,
instrument_id: BorrowedStr<'_>,
client_id: BorrowedStr<'_>,
params_json: BorrowedStr<'_>,
) -> PluginResult<()>,
pub unsubscribe_trades: unsafe extern "C" fn(
ctx: *const HostContext,
instrument_id: BorrowedStr<'_>,
client_id: BorrowedStr<'_>,
params_json: BorrowedStr<'_>,
) -> PluginResult<()>,
pub subscribe_bars: unsafe extern "C" fn(
ctx: *const HostContext,
bar_type: BorrowedStr<'_>,
client_id: BorrowedStr<'_>,
params_json: BorrowedStr<'_>,
) -> PluginResult<()>,
pub unsubscribe_bars: unsafe extern "C" fn(
ctx: *const HostContext,
bar_type: BorrowedStr<'_>,
client_id: BorrowedStr<'_>,
params_json: BorrowedStr<'_>,
) -> PluginResult<()>,
pub subscribe_book_deltas: unsafe extern "C" fn(
ctx: *const HostContext,
instrument_id: BorrowedStr<'_>,
book_type: u8,
depth: usize,
client_id: BorrowedStr<'_>,
managed: u8,
params_json: BorrowedStr<'_>,
) -> PluginResult<()>,
pub unsubscribe_book_deltas: unsafe extern "C" fn(
ctx: *const HostContext,
instrument_id: BorrowedStr<'_>,
client_id: BorrowedStr<'_>,
params_json: BorrowedStr<'_>,
) -> PluginResult<()>,
pub subscribe_book_at_interval: unsafe extern "C" fn(
ctx: *const HostContext,
instrument_id: BorrowedStr<'_>,
book_type: u8,
depth: usize,
interval_ms: usize,
client_id: BorrowedStr<'_>,
params_json: BorrowedStr<'_>,
) -> PluginResult<()>,
pub unsubscribe_book_at_interval: unsafe extern "C" fn(
ctx: *const HostContext,
instrument_id: BorrowedStr<'_>,
interval_ms: usize,
client_id: BorrowedStr<'_>,
params_json: BorrowedStr<'_>,
) -> PluginResult<()>,
pub msgbus_publish: unsafe extern "C" fn(
ctx: *const HostContext,
topic: BorrowedStr<'_>,
payload: Slice<'_, u8>,
) -> PluginResult<()>,
pub set_time_alert: unsafe extern "C" fn(
ctx: *const HostContext,
name: BorrowedStr<'_>,
alert_time_ns: u64,
allow_past: u8,
) -> PluginResult<()>,
pub set_timer: unsafe extern "C" fn(
ctx: *const HostContext,
name: BorrowedStr<'_>,
interval_ns: u64,
start_time_ns: u64,
stop_time_ns: u64,
allow_past: u8,
fire_immediately: u8,
) -> PluginResult<()>,
pub cancel_timer:
unsafe extern "C" fn(ctx: *const HostContext, name: BorrowedStr<'_>) -> PluginResult<()>,
pub submit_order: unsafe extern "C" fn(
ctx: *const HostContext,
command: *const SubmitOrderHandle,
) -> PluginResult<()>,
pub cancel_order: unsafe extern "C" fn(
ctx: *const HostContext,
command: *const CancelOrderHandle,
) -> PluginResult<()>,
pub modify_order: unsafe extern "C" fn(
ctx: *const HostContext,
command: *const ModifyOrderHandle,
) -> PluginResult<()>,
pub submit_order_list: unsafe extern "C" fn(
ctx: *const HostContext,
command: *const SubmitOrderListHandle,
) -> PluginResult<()>,
pub cancel_orders: unsafe extern "C" fn(
ctx: *const HostContext,
command: *const CancelOrdersHandle,
) -> PluginResult<()>,
pub cancel_all_orders: unsafe extern "C" fn(
ctx: *const HostContext,
command: *const CancelAllOrdersHandle,
) -> PluginResult<()>,
pub close_position: unsafe extern "C" fn(
ctx: *const HostContext,
command: *const ClosePositionHandle,
) -> PluginResult<()>,
pub close_all_positions: unsafe extern "C" fn(
ctx: *const HostContext,
command: *const CloseAllPositionsHandle,
) -> PluginResult<()>,
pub query_account: unsafe extern "C" fn(
ctx: *const HostContext,
command: *const QueryAccountHandle,
) -> PluginResult<()>,
pub query_order: unsafe extern "C" fn(
ctx: *const HostContext,
command: *const QueryOrderHandle,
) -> PluginResult<()>,
}
impl HostVTable {
#[must_use]
pub fn matches_compiled_abi(&self) -> bool {
self.abi_version == NAUTILUS_PLUGIN_ABI_VERSION
}
pub unsafe fn now_ns(&self) -> u64 {
unsafe { (self.clock_now_ns)() }
}
pub unsafe fn log_message(&self, level: HostLogLevel, target: &str, message: &str) {
unsafe {
(self.log)(
level,
BorrowedStr::from_str(target),
BorrowedStr::from_str(message),
);
}
}
}
unsafe impl Send for HostVTable {}
unsafe impl Sync for HostVTable {}
#[cfg(test)]
mod tests {
use std::sync::{
Mutex, MutexGuard, OnceLock,
atomic::{AtomicU8, AtomicU64, Ordering},
};
use rstest::rstest;
use super::*;
use crate::boundary::{OwnedBytes, PluginResult, Slice};
static CLOCK_VALUE: AtomicU64 = AtomicU64::new(0);
static LOG_LEVEL_OBSERVED: AtomicU8 = AtomicU8::new(0);
fn shared_state_lock() -> MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|p| p.into_inner())
}
unsafe extern "C" fn fixed_clock_now_ns() -> u64 {
CLOCK_VALUE.load(Ordering::SeqCst)
}
unsafe extern "C" fn recording_log(
level: HostLogLevel,
_target: BorrowedStr<'_>,
_message: BorrowedStr<'_>,
) {
LOG_LEVEL_OBSERVED.store(level as u8, Ordering::SeqCst);
}
macro_rules! stub_bytes {
($name:ident) => {
unsafe extern "C" fn $name(
_ctx: *const HostContext,
_a: BorrowedStr<'_>,
) -> PluginResult<OwnedBytes> {
PluginResult::Ok(OwnedBytes::empty())
}
};
}
macro_rules! stub_controller_bytes {
($name:ident) => {
unsafe extern "C" fn $name(
_ctx: *const ControllerHostContext,
_a: BorrowedStr<'_>,
) -> PluginResult<OwnedBytes> {
PluginResult::Ok(OwnedBytes::empty())
}
};
}
macro_rules! stub_unit {
($name:ident, ($($arg:ident : $ty:ty),* $(,)?)) => {
unsafe extern "C" fn $name($($arg: $ty),*) -> PluginResult<()> {
$(let _ = $arg;)*
PluginResult::Ok(())
}
};
}
stub_bytes!(stub_cache_instrument);
stub_bytes!(stub_cache_account);
stub_bytes!(stub_cache_order);
stub_bytes!(stub_cache_position);
stub_bytes!(stub_cache_orders_for_strategy);
stub_bytes!(stub_cache_positions_for_strategy);
stub_controller_bytes!(stub_controller_create_plugin_strategy);
stub_controller_bytes!(stub_controller_start_strategy);
stub_controller_bytes!(stub_controller_stop_strategy);
stub_controller_bytes!(stub_controller_exit_market);
stub_controller_bytes!(stub_controller_remove_strategy);
stub_controller_bytes!(stub_controller_instrument_exists);
stub_controller_bytes!(stub_controller_log);
stub_controller_bytes!(stub_controller_clock_now_ns);
stub_unit!(
stub_subscribe,
(
ctx: *const HostContext,
a: BorrowedStr<'_>,
b: BorrowedStr<'_>,
c: BorrowedStr<'_>,
)
);
stub_unit!(
stub_subscribe_book_deltas,
(
ctx: *const HostContext,
a: BorrowedStr<'_>,
t: u8,
d: usize,
b: BorrowedStr<'_>,
m: u8,
c: BorrowedStr<'_>,
)
);
stub_unit!(
stub_subscribe_book_at_interval,
(
ctx: *const HostContext,
a: BorrowedStr<'_>,
t: u8,
d: usize,
i: usize,
b: BorrowedStr<'_>,
c: BorrowedStr<'_>,
)
);
stub_unit!(
stub_unsubscribe_book_at_interval,
(
ctx: *const HostContext,
a: BorrowedStr<'_>,
i: usize,
b: BorrowedStr<'_>,
c: BorrowedStr<'_>,
)
);
stub_unit!(
stub_msgbus_publish,
(
ctx: *const HostContext,
t: BorrowedStr<'_>,
p: Slice<'_, u8>,
)
);
stub_unit!(
stub_set_time_alert,
(
ctx: *const HostContext,
n: BorrowedStr<'_>,
a: u64,
p: u8,
)
);
stub_unit!(
stub_set_timer,
(
ctx: *const HostContext,
n: BorrowedStr<'_>,
i: u64,
s: u64,
e: u64,
p: u8,
f: u8,
)
);
stub_unit!(stub_cancel_timer, (ctx: *const HostContext, n: BorrowedStr<'_>));
stub_unit!(
stub_submit_order,
(ctx: *const HostContext, c: *const SubmitOrderHandle)
);
stub_unit!(
stub_cancel_order,
(ctx: *const HostContext, c: *const CancelOrderHandle)
);
stub_unit!(
stub_modify_order,
(ctx: *const HostContext, c: *const ModifyOrderHandle)
);
stub_unit!(
stub_submit_order_list,
(ctx: *const HostContext, c: *const SubmitOrderListHandle)
);
stub_unit!(
stub_cancel_orders,
(ctx: *const HostContext, c: *const CancelOrdersHandle)
);
stub_unit!(
stub_cancel_all_orders,
(ctx: *const HostContext, c: *const CancelAllOrdersHandle)
);
stub_unit!(
stub_close_position,
(ctx: *const HostContext, c: *const ClosePositionHandle)
);
stub_unit!(
stub_close_all_positions,
(ctx: *const HostContext, c: *const CloseAllPositionsHandle)
);
stub_unit!(
stub_query_account,
(ctx: *const HostContext, c: *const QueryAccountHandle)
);
stub_unit!(
stub_query_order,
(ctx: *const HostContext, c: *const QueryOrderHandle)
);
fn build_test_host(abi: u32) -> HostVTable {
HostVTable {
abi_version: abi,
clock_now_ns: fixed_clock_now_ns,
log: recording_log,
cache_instrument: stub_cache_instrument,
cache_account: stub_cache_account,
cache_order: stub_cache_order,
cache_position: stub_cache_position,
cache_orders_for_strategy: stub_cache_orders_for_strategy,
cache_positions_for_strategy: stub_cache_positions_for_strategy,
subscribe_quotes: stub_subscribe,
unsubscribe_quotes: stub_subscribe,
subscribe_trades: stub_subscribe,
unsubscribe_trades: stub_subscribe,
subscribe_bars: stub_subscribe,
unsubscribe_bars: stub_subscribe,
subscribe_book_deltas: stub_subscribe_book_deltas,
unsubscribe_book_deltas: stub_subscribe,
subscribe_book_at_interval: stub_subscribe_book_at_interval,
unsubscribe_book_at_interval: stub_unsubscribe_book_at_interval,
msgbus_publish: stub_msgbus_publish,
set_time_alert: stub_set_time_alert,
set_timer: stub_set_timer,
cancel_timer: stub_cancel_timer,
submit_order: stub_submit_order,
cancel_order: stub_cancel_order,
modify_order: stub_modify_order,
submit_order_list: stub_submit_order_list,
cancel_orders: stub_cancel_orders,
cancel_all_orders: stub_cancel_all_orders,
close_position: stub_close_position,
close_all_positions: stub_close_all_positions,
query_account: stub_query_account,
query_order: stub_query_order,
}
}
fn build_controller_test_host(abi: u32) -> ControllerHostVTable {
ControllerHostVTable {
abi_version: abi,
create_plugin_strategy: stub_controller_create_plugin_strategy,
start_strategy: stub_controller_start_strategy,
stop_strategy: stub_controller_stop_strategy,
exit_market: stub_controller_exit_market,
remove_strategy: stub_controller_remove_strategy,
instrument_exists: stub_controller_instrument_exists,
log: stub_controller_log,
clock_now_ns: stub_controller_clock_now_ns,
}
}
#[rstest]
fn matches_compiled_abi_accepts_compiled_version() {
let host = build_test_host(NAUTILUS_PLUGIN_ABI_VERSION);
assert!(host.matches_compiled_abi());
}
#[rstest]
fn controller_matches_compiled_abi_accepts_compiled_version() {
let host = build_controller_test_host(NAUTILUS_PLUGIN_ABI_VERSION);
assert!(host.matches_compiled_abi());
}
#[rstest]
#[case::off_by_one(NAUTILUS_PLUGIN_ABI_VERSION.wrapping_add(1))]
#[case::zero(0)]
#[case::max(u32::MAX)]
fn matches_compiled_abi_rejects_mismatch(#[case] abi: u32) {
let host = build_test_host(abi);
assert!(!host.matches_compiled_abi());
}
#[rstest]
#[case::off_by_one(NAUTILUS_PLUGIN_ABI_VERSION.wrapping_add(1))]
#[case::zero(0)]
#[case::max(u32::MAX)]
fn controller_matches_compiled_abi_rejects_mismatch(#[case] abi: u32) {
let host = build_controller_test_host(abi);
assert!(!host.matches_compiled_abi());
}
#[rstest]
fn now_ns_calls_clock_function_pointer() {
let _g = shared_state_lock();
CLOCK_VALUE.store(42_424_242, Ordering::SeqCst);
let host = build_test_host(NAUTILUS_PLUGIN_ABI_VERSION);
let n = unsafe { host.now_ns() };
assert_eq!(n, 42_424_242);
}
#[rstest]
#[case::error(HostLogLevel::Error, 1u8)]
#[case::warn(HostLogLevel::Warn, 2)]
#[case::info(HostLogLevel::Info, 3)]
#[case::debug(HostLogLevel::Debug, 4)]
#[case::trace(HostLogLevel::Trace, 5)]
fn log_message_invokes_log_with_the_right_level(
#[case] level: HostLogLevel,
#[case] expected_discriminant: u8,
) {
let _g = shared_state_lock();
LOG_LEVEL_OBSERVED.store(0, Ordering::SeqCst);
let host = build_test_host(NAUTILUS_PLUGIN_ABI_VERSION);
unsafe { host.log_message(level, "target", "message") };
assert_eq!(
LOG_LEVEL_OBSERVED.load(Ordering::SeqCst),
expected_discriminant
);
}
}