1use std::{
25 collections::BTreeMap,
26 fmt::{Debug, Display},
27 slice,
28};
29
30use nautilus_model::types::fixed::FIXED_PRECISION;
31
32use crate::{
33 NAUTILUS_PLUGIN_ABI_VERSION, PLUGIN_BUILD_ID_VERSION,
34 boundary::{BorrowedStr, Slice},
35 host::HostVTable,
36 surfaces::{
37 actor::ActorVTable, controller::ControllerVTable, custom_data::CustomDataVTable,
38 strategy::StrategyVTable,
39 },
40};
41
42pub type PluginInitFn = unsafe extern "C" fn(host: *const HostVTable) -> *const PluginManifest;
50
51#[repr(C)]
58#[derive(Clone, Copy)]
59pub struct PluginBuildId {
60 pub schema_version: u32,
63
64 pub nautilus_plugin_version: BorrowedStr<'static>,
66
67 pub rustc_version: BorrowedStr<'static>,
70
71 pub target_triple: BorrowedStr<'static>,
73
74 pub build_profile: BorrowedStr<'static>,
76
77 pub precision_mode: BorrowedStr<'static>,
79
80 pub fixed_precision: u8,
82}
83
84impl PluginBuildId {
85 #[must_use]
87 pub const fn current() -> Self {
88 Self {
89 schema_version: PLUGIN_BUILD_ID_VERSION,
90 nautilus_plugin_version: BorrowedStr::from_str(env!("CARGO_PKG_VERSION")),
91 rustc_version: BorrowedStr::from_str(env!("NAUTILUS_PLUGIN_BUILD_RUSTC_VERSION")),
92 target_triple: BorrowedStr::from_str(env!("NAUTILUS_PLUGIN_BUILD_TARGET")),
93 build_profile: BorrowedStr::from_str(env!("NAUTILUS_PLUGIN_BUILD_PROFILE")),
94 precision_mode: BorrowedStr::from_str(compiled_precision_mode()),
95 fixed_precision: FIXED_PRECISION,
96 }
97 }
98}
99
100#[must_use]
102pub const fn compiled_precision_mode() -> &'static str {
103 if FIXED_PRECISION > 9 {
104 "high-precision"
105 } else {
106 "standard"
107 }
108}
109
110#[derive(Clone, Debug, Default, PartialEq, Eq)]
112pub struct PluginManifestValidationErrors {
113 messages: Vec<String>,
114}
115
116impl PluginManifestValidationErrors {
117 #[must_use]
119 pub fn is_empty(&self) -> bool {
120 self.messages.is_empty()
121 }
122
123 #[must_use]
125 pub fn messages(&self) -> &[String] {
126 &self.messages
127 }
128
129 fn push(&mut self, message: impl Into<String>) {
130 self.messages.push(message.into());
131 }
132}
133
134impl Display for PluginManifestValidationErrors {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 for (index, message) in self.messages.iter().enumerate() {
137 if index > 0 {
138 write!(f, "; ")?;
139 }
140 write!(f, "{message}")?;
141 }
142 Ok(())
143 }
144}
145
146impl std::error::Error for PluginManifestValidationErrors {}
147
148#[repr(C)]
154pub struct PluginManifest {
155 pub abi_version: u32,
158
159 pub plugin_name: BorrowedStr<'static>,
161
162 pub plugin_vendor: BorrowedStr<'static>,
164
165 pub plugin_version: BorrowedStr<'static>,
167
168 pub build_id: PluginBuildId,
170
171 pub custom_data: Slice<'static, CustomDataRegistration>,
173
174 pub actors: Slice<'static, ActorRegistration>,
176
177 pub strategies: Slice<'static, StrategyRegistration>,
179
180 pub controllers: Slice<'static, ControllerRegistration>,
182 }
187
188impl PluginManifest {
189 #[must_use]
191 pub fn matches_compiled_abi(&self) -> bool {
192 self.abi_version == NAUTILUS_PLUGIN_ABI_VERSION
193 }
194
195 pub fn validate(&self) -> Result<(), PluginManifestValidationErrors> {
206 let mut errors = PluginManifestValidationErrors::default();
207
208 validate_required_str("plugin_name", self.plugin_name, &mut errors);
209 validate_optional_str("plugin_vendor", self.plugin_vendor, &mut errors);
210 validate_required_str("plugin_version", self.plugin_version, &mut errors);
211 validate_build_id(&self.build_id, &mut errors);
212 validate_registrations(self, &mut errors);
213
214 if errors.is_empty() {
215 Ok(())
216 } else {
217 Err(errors)
218 }
219 }
220}
221
222fn validate_build_id(build_id: &PluginBuildId, errors: &mut PluginManifestValidationErrors) {
223 if build_id.schema_version != PLUGIN_BUILD_ID_VERSION {
224 errors.push(format!(
225 "build_id.schema_version {} does not match supported schema {}",
226 build_id.schema_version, PLUGIN_BUILD_ID_VERSION
227 ));
228 return;
229 }
230
231 validate_optional_str(
232 "build_id.nautilus_plugin_version",
233 build_id.nautilus_plugin_version,
234 errors,
235 );
236 validate_optional_str("build_id.rustc_version", build_id.rustc_version, errors);
237 validate_optional_str("build_id.target_triple", build_id.target_triple, errors);
238 validate_optional_str("build_id.build_profile", build_id.build_profile, errors);
239 if let Some(precision_mode) =
240 validate_required_str("build_id.precision_mode", build_id.precision_mode, errors)
241 {
242 let expected = compiled_precision_mode();
243 if precision_mode != expected {
244 errors.push(format!(
245 "build_id.precision_mode '{precision_mode}' does not match host precision mode '{expected}'"
246 ));
247 }
248 }
249
250 if build_id.fixed_precision != FIXED_PRECISION {
251 errors.push(format!(
252 "build_id.fixed_precision {} does not match host fixed precision {}",
253 build_id.fixed_precision, FIXED_PRECISION
254 ));
255 }
256}
257
258macro_rules! validate_vtable_slots {
259 ($location:expr, $type_name:expr, $vtable:expr, $errors:expr, [$($slot:ident),+ $(,)?]) => {
260 $(
261 validate_vtable_slot(
262 $location,
263 $type_name,
264 stringify!($slot),
265 $vtable.$slot.is_some(),
266 $errors,
267 );
268 )+
269 };
270}
271
272fn validate_registrations(manifest: &PluginManifest, errors: &mut PluginManifestValidationErrors) {
273 let mut seen_type_names = BTreeMap::<String, String>::new();
274
275 if let Some(entries) = validate_slice("custom_data", &manifest.custom_data, errors) {
276 for (index, entry) in entries.iter().enumerate() {
277 let location = format!("custom_data[{index}]");
278 let type_name = validate_type_name(&location, entry.type_name, errors);
279 validate_unique_type_name(&mut seen_type_names, &location, type_name, errors);
280 if entry.vtable.is_null() {
281 errors.push(format!("{location}.vtable must not be null"));
282 } else {
283 validate_custom_data_vtable(&location, type_name, entry.vtable, errors);
284 }
285 }
286 }
287
288 if let Some(entries) = validate_slice("actors", &manifest.actors, errors) {
289 for (index, entry) in entries.iter().enumerate() {
290 let location = format!("actors[{index}]");
291 let type_name = validate_type_name(&location, entry.type_name, errors);
292 validate_unique_type_name(&mut seen_type_names, &location, type_name, errors);
293 if entry.vtable.is_null() {
294 errors.push(format!("{location}.vtable must not be null"));
295 } else {
296 validate_actor_vtable(&location, type_name, entry.vtable, errors);
297 }
298 }
299 }
300
301 if let Some(entries) = validate_slice("strategies", &manifest.strategies, errors) {
302 for (index, entry) in entries.iter().enumerate() {
303 let location = format!("strategies[{index}]");
304 let type_name = validate_type_name(&location, entry.type_name, errors);
305 validate_unique_type_name(&mut seen_type_names, &location, type_name, errors);
306 if entry.vtable.is_null() {
307 errors.push(format!("{location}.vtable must not be null"));
308 } else {
309 validate_strategy_vtable(&location, type_name, entry.vtable, errors);
310 }
311 }
312 }
313
314 if let Some(entries) = validate_slice("controllers", &manifest.controllers, errors) {
315 for (index, entry) in entries.iter().enumerate() {
316 let location = format!("controllers[{index}]");
317 let type_name = validate_type_name(&location, entry.type_name, errors);
318 validate_unique_type_name(&mut seen_type_names, &location, type_name, errors);
319 if entry.vtable.is_null() {
320 errors.push(format!("{location}.vtable must not be null"));
321 } else {
322 validate_controller_vtable(&location, type_name, entry.vtable, errors);
323 }
324 }
325 }
326}
327
328fn validate_custom_data_vtable(
329 location: &str,
330 type_name: Option<&str>,
331 vtable: *const CustomDataVTable,
332 errors: &mut PluginManifestValidationErrors,
333) {
334 let vtable = unsafe { &*vtable };
337 validate_vtable_slots!(
338 location,
339 type_name,
340 vtable,
341 errors,
342 [
343 type_name,
344 schema_ipc,
345 from_json,
346 encode_batch,
347 decode_batch,
348 ts_event,
349 ts_init,
350 to_json,
351 clone_handle,
352 drop_handle,
353 eq_handles,
354 ]
355 );
356}
357
358fn validate_actor_vtable(
359 location: &str,
360 type_name: Option<&str>,
361 vtable: *const ActorVTable,
362 errors: &mut PluginManifestValidationErrors,
363) {
364 let vtable = unsafe { &*vtable };
367 validate_vtable_slots!(
368 location,
369 type_name,
370 vtable,
371 errors,
372 [
373 create,
374 drop_handle,
375 type_name,
376 on_start,
377 on_stop,
378 on_resume,
379 on_reset,
380 on_dispose,
381 on_degrade,
382 on_fault,
383 on_time_event,
384 on_data,
385 on_instrument,
386 on_book_deltas,
387 on_book,
388 on_quote,
389 on_trade,
390 on_bar,
391 on_mark_price,
392 on_index_price,
393 on_funding_rate,
394 on_option_greeks,
395 on_option_chain,
396 on_instrument_status,
397 on_instrument_close,
398 on_order_filled,
399 on_order_canceled,
400 on_signal,
401 on_historical_book_deltas,
402 on_historical_book_depth,
403 on_historical_quotes,
404 on_historical_trades,
405 on_historical_bars,
406 on_historical_mark_prices,
407 on_historical_index_prices,
408 on_historical_funding_rates,
409 ]
410 );
411}
412
413fn validate_strategy_vtable(
414 location: &str,
415 type_name: Option<&str>,
416 vtable: *const StrategyVTable,
417 errors: &mut PluginManifestValidationErrors,
418) {
419 let vtable = unsafe { &*vtable };
422 validate_vtable_slots!(
423 location,
424 type_name,
425 vtable,
426 errors,
427 [
428 create,
429 drop_handle,
430 type_name,
431 on_start,
432 on_stop,
433 on_resume,
434 on_reset,
435 on_dispose,
436 on_degrade,
437 on_fault,
438 on_time_event,
439 on_data,
440 on_instrument,
441 on_book_deltas,
442 on_book,
443 on_quote,
444 on_trade,
445 on_bar,
446 on_mark_price,
447 on_index_price,
448 on_funding_rate,
449 on_option_greeks,
450 on_option_chain,
451 on_instrument_status,
452 on_instrument_close,
453 on_signal,
454 on_order_initialized,
455 on_order_submitted,
456 on_order_accepted,
457 on_order_rejected,
458 on_order_filled,
459 on_order_canceled,
460 on_order_expired,
461 on_order_triggered,
462 on_order_denied,
463 on_order_emulated,
464 on_order_released,
465 on_order_pending_update,
466 on_order_pending_cancel,
467 on_order_modify_rejected,
468 on_order_cancel_rejected,
469 on_order_updated,
470 on_position_opened,
471 on_position_changed,
472 on_position_closed,
473 on_market_exit,
474 on_historical_book_deltas,
475 on_historical_book_depth,
476 on_historical_quotes,
477 on_historical_trades,
478 on_historical_bars,
479 on_historical_mark_prices,
480 on_historical_index_prices,
481 on_historical_funding_rates,
482 ]
483 );
484}
485
486fn validate_controller_vtable(
487 location: &str,
488 type_name: Option<&str>,
489 vtable: *const ControllerVTable,
490 errors: &mut PluginManifestValidationErrors,
491) {
492 let vtable = unsafe { &*vtable };
495 validate_vtable_slots!(
496 location,
497 type_name,
498 vtable,
499 errors,
500 [
501 prepare,
502 create,
503 drop_handle,
504 type_name,
505 on_start,
506 on_stop,
507 on_resume,
508 on_reset,
509 on_dispose,
510 on_degrade,
511 on_fault,
512 on_time_event,
513 ]
514 );
515}
516
517fn validate_vtable_slot(
518 location: &str,
519 type_name: Option<&str>,
520 slot: &str,
521 is_present: bool,
522 errors: &mut PluginManifestValidationErrors,
523) {
524 if is_present {
525 return;
526 }
527
528 let type_name = match type_name {
529 Some("") | None => "<unknown>",
530 Some(value) => value,
531 };
532 errors.push(format!(
533 "{location} type '{type_name}' vtable.{slot} must not be null"
534 ));
535}
536
537fn validate_type_name<'a>(
538 location: &str,
539 value: BorrowedStr<'a>,
540 errors: &mut PluginManifestValidationErrors,
541) -> Option<&'a str> {
542 validate_required_str(&format!("{location}.type_name"), value, errors)
543}
544
545fn validate_unique_type_name(
546 seen_type_names: &mut BTreeMap<String, String>,
547 location: &str,
548 type_name: Option<&str>,
549 errors: &mut PluginManifestValidationErrors,
550) {
551 let Some(type_name) = type_name else {
552 return;
553 };
554
555 if type_name.is_empty() {
556 return;
557 }
558
559 if let Some(first_location) = seen_type_names.get(type_name) {
560 errors.push(format!(
561 "type name '{type_name}' appears in both {first_location} and {location}"
562 ));
563 } else {
564 seen_type_names.insert(type_name.to_string(), location.to_string());
565 }
566}
567
568fn validate_slice<'a, T>(
569 field: &str,
570 value: &Slice<'a, T>,
571 errors: &mut PluginManifestValidationErrors,
572) -> Option<&'a [T]> {
573 if value.len == 0 {
574 return Some(&[]);
575 }
576
577 if value.ptr.is_null() {
578 errors.push(format!(
579 "{field} has null pointer with non-zero length {}",
580 value.len
581 ));
582 return None;
583 }
584
585 Some(unsafe { slice::from_raw_parts(value.ptr, value.len) })
588}
589
590fn validate_required_str<'a>(
591 field: &str,
592 value: BorrowedStr<'a>,
593 errors: &mut PluginManifestValidationErrors,
594) -> Option<&'a str> {
595 let text = validate_optional_str(field, value, errors)?;
596 if text.is_empty() {
597 errors.push(format!("{field} must not be empty"));
598 }
599 Some(text)
600}
601
602fn validate_optional_str<'a>(
603 field: &str,
604 value: BorrowedStr<'a>,
605 errors: &mut PluginManifestValidationErrors,
606) -> Option<&'a str> {
607 if value.len == 0 {
608 return Some("");
609 }
610
611 if value.ptr.is_null() {
612 errors.push(format!(
613 "{field} has null pointer with non-zero length {}",
614 value.len
615 ));
616 return None;
617 }
618
619 let bytes = unsafe { slice::from_raw_parts(value.ptr, value.len) };
622 match std::str::from_utf8(bytes) {
623 Ok(text) => Some(text),
624 Err(e) => {
625 errors.push(format!("{field} is not valid UTF-8: {e}"));
626 None
627 }
628 }
629}
630
631#[repr(C)]
633pub struct CustomDataRegistration {
634 pub type_name: BorrowedStr<'static>,
636 pub vtable: *const CustomDataVTable,
638}
639
640unsafe impl Send for CustomDataRegistration {}
642unsafe impl Sync for CustomDataRegistration {}
644
645#[repr(C)]
647pub struct ActorRegistration {
648 pub type_name: BorrowedStr<'static>,
650 pub vtable: *const ActorVTable,
652}
653
654unsafe impl Send for ActorRegistration {}
656unsafe impl Sync for ActorRegistration {}
658
659#[repr(C)]
661pub struct StrategyRegistration {
662 pub type_name: BorrowedStr<'static>,
664 pub vtable: *const StrategyVTable,
666}
667
668unsafe impl Send for StrategyRegistration {}
670unsafe impl Sync for StrategyRegistration {}
672
673#[repr(C)]
675pub struct ControllerRegistration {
676 pub type_name: BorrowedStr<'static>,
678 pub vtable: *const ControllerVTable,
680}
681
682unsafe impl Send for ControllerRegistration {}
684unsafe impl Sync for ControllerRegistration {}
686
687#[cfg(feature = "host")]
692#[derive(Clone, Copy)]
693pub struct ValidatedPluginManifest<'a> {
694 manifest: &'a PluginManifest,
695}
696
697#[cfg(feature = "host")]
698impl Debug for ValidatedPluginManifest<'_> {
699 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
700 f.debug_struct(stringify!(ValidatedPluginManifest))
701 .field("plugin_name", &self.plugin_name())
702 .finish()
703 }
704}
705
706#[cfg(feature = "host")]
707impl<'a> ValidatedPluginManifest<'a> {
708 pub fn new(manifest: &'a PluginManifest) -> Result<Self, PluginManifestValidationErrors> {
714 manifest.validate()?;
715 Ok(Self { manifest })
716 }
717
718 #[must_use]
720 pub fn manifest(self) -> &'a PluginManifest {
721 self.manifest
722 }
723
724 #[must_use]
726 pub fn plugin_name(self) -> &'static str {
727 unsafe { self.manifest.plugin_name.as_str() }
730 }
731
732 #[must_use]
734 pub fn custom_data(self) -> impl ExactSizeIterator<Item = ValidatedCustomDataRegistration> {
735 unsafe { self.manifest.custom_data.as_slice() }
737 .iter()
738 .map(ValidatedCustomDataRegistration::from_validated_registration)
739 }
740
741 #[must_use]
743 pub fn actors(self) -> impl ExactSizeIterator<Item = ValidatedActorRegistration> {
744 unsafe { self.manifest.actors.as_slice() }
746 .iter()
747 .map(ValidatedActorRegistration::from_validated_registration)
748 }
749
750 #[must_use]
752 pub fn strategies(self) -> impl ExactSizeIterator<Item = ValidatedStrategyRegistration> {
753 unsafe { self.manifest.strategies.as_slice() }
755 .iter()
756 .map(ValidatedStrategyRegistration::from_validated_registration)
757 }
758
759 #[must_use]
761 pub fn controllers(self) -> impl ExactSizeIterator<Item = ValidatedControllerRegistration> {
762 unsafe { self.manifest.controllers.as_slice() }
764 .iter()
765 .map(ValidatedControllerRegistration::from_validated_registration)
766 }
767}
768
769#[cfg(feature = "host")]
771#[derive(Clone, Copy, Debug, PartialEq, Eq)]
772pub struct ValidatedCustomDataRegistration {
773 type_name: &'static str,
774 vtable: ValidatedCustomDataVTable,
775}
776
777#[cfg(feature = "host")]
778impl ValidatedCustomDataRegistration {
779 fn from_validated_registration(registration: &CustomDataRegistration) -> Self {
780 Self {
781 type_name: unsafe { registration.type_name.as_str() },
784 vtable: ValidatedCustomDataVTable::from_validated_ptr(registration.vtable),
785 }
786 }
787
788 #[must_use]
790 pub fn type_name(self) -> &'static str {
791 self.type_name
792 }
793
794 #[must_use]
796 pub fn vtable(self) -> ValidatedCustomDataVTable {
797 self.vtable
798 }
799}
800
801#[cfg(feature = "host")]
803#[derive(Clone, Copy, Debug, PartialEq, Eq)]
804pub struct ValidatedActorRegistration {
805 type_name: &'static str,
806 vtable: ValidatedActorVTable,
807}
808
809#[cfg(feature = "host")]
810impl ValidatedActorRegistration {
811 fn from_validated_registration(registration: &ActorRegistration) -> Self {
812 Self {
813 type_name: unsafe { registration.type_name.as_str() },
816 vtable: ValidatedActorVTable::from_validated_ptr(registration.vtable),
817 }
818 }
819
820 #[must_use]
822 pub fn type_name(self) -> &'static str {
823 self.type_name
824 }
825
826 #[must_use]
828 pub fn vtable(self) -> ValidatedActorVTable {
829 self.vtable
830 }
831}
832
833#[cfg(feature = "host")]
835#[derive(Clone, Copy, Debug, PartialEq, Eq)]
836pub struct ValidatedStrategyRegistration {
837 type_name: &'static str,
838 vtable: ValidatedStrategyVTable,
839}
840
841#[cfg(feature = "host")]
842impl ValidatedStrategyRegistration {
843 fn from_validated_registration(registration: &StrategyRegistration) -> Self {
844 Self {
845 type_name: unsafe { registration.type_name.as_str() },
848 vtable: ValidatedStrategyVTable::from_validated_ptr(registration.vtable),
849 }
850 }
851
852 #[must_use]
854 pub fn type_name(self) -> &'static str {
855 self.type_name
856 }
857
858 #[must_use]
860 pub fn vtable(self) -> ValidatedStrategyVTable {
861 self.vtable
862 }
863}
864
865#[cfg(feature = "host")]
867#[derive(Clone, Copy, Debug, PartialEq, Eq)]
868pub struct ValidatedControllerRegistration {
869 type_name: &'static str,
870 vtable: ValidatedControllerVTable,
871}
872
873#[cfg(feature = "host")]
874impl ValidatedControllerRegistration {
875 fn from_validated_registration(registration: &ControllerRegistration) -> Self {
876 Self {
877 type_name: unsafe { registration.type_name.as_str() },
880 vtable: ValidatedControllerVTable::from_validated_ptr(registration.vtable),
881 }
882 }
883
884 #[must_use]
886 pub fn type_name(self) -> &'static str {
887 self.type_name
888 }
889
890 #[must_use]
892 pub fn vtable(self) -> ValidatedControllerVTable {
893 self.vtable
894 }
895}
896
897#[cfg(feature = "host")]
899#[derive(Clone, Copy, Debug, PartialEq, Eq)]
900pub struct ValidatedCustomDataVTable {
901 ptr: std::ptr::NonNull<CustomDataVTable>,
902}
903
904#[cfg(feature = "host")]
905impl ValidatedCustomDataVTable {
906 fn from_validated_ptr(ptr: *const CustomDataVTable) -> Self {
907 Self {
908 ptr: std::ptr::NonNull::new(ptr.cast_mut())
909 .expect("validated manifest stores non-null CustomDataVTable"),
910 }
911 }
912
913 #[must_use]
920 pub unsafe fn from_raw_unchecked(ptr: *const CustomDataVTable) -> Self {
921 Self::from_validated_ptr(ptr)
922 }
923
924 #[must_use]
926 pub fn as_ptr(self) -> *const CustomDataVTable {
927 self.ptr.as_ptr()
928 }
929}
930
931#[cfg(feature = "host")]
933unsafe impl Send for ValidatedCustomDataVTable {}
934#[cfg(feature = "host")]
936unsafe impl Sync for ValidatedCustomDataVTable {}
937
938#[cfg(feature = "host")]
940#[derive(Clone, Copy, Debug, PartialEq, Eq)]
941pub struct ValidatedActorVTable {
942 ptr: std::ptr::NonNull<ActorVTable>,
943}
944
945#[cfg(feature = "host")]
946impl ValidatedActorVTable {
947 fn from_validated_ptr(ptr: *const ActorVTable) -> Self {
948 Self {
949 ptr: std::ptr::NonNull::new(ptr.cast_mut())
950 .expect("validated manifest stores non-null ActorVTable"),
951 }
952 }
953
954 #[must_use]
961 pub unsafe fn from_raw_unchecked(ptr: *const ActorVTable) -> Self {
962 Self::from_validated_ptr(ptr)
963 }
964
965 #[must_use]
967 pub fn as_ptr(self) -> *const ActorVTable {
968 self.ptr.as_ptr()
969 }
970}
971
972#[cfg(feature = "host")]
974unsafe impl Send for ValidatedActorVTable {}
975#[cfg(feature = "host")]
977unsafe impl Sync for ValidatedActorVTable {}
978
979#[cfg(feature = "host")]
981#[derive(Clone, Copy, Debug, PartialEq, Eq)]
982pub struct ValidatedStrategyVTable {
983 ptr: std::ptr::NonNull<StrategyVTable>,
984}
985
986#[cfg(feature = "host")]
987impl ValidatedStrategyVTable {
988 fn from_validated_ptr(ptr: *const StrategyVTable) -> Self {
989 Self {
990 ptr: std::ptr::NonNull::new(ptr.cast_mut())
991 .expect("validated manifest stores non-null StrategyVTable"),
992 }
993 }
994
995 #[must_use]
1002 pub unsafe fn from_raw_unchecked(ptr: *const StrategyVTable) -> Self {
1003 Self::from_validated_ptr(ptr)
1004 }
1005
1006 #[must_use]
1008 pub fn as_ptr(self) -> *const StrategyVTable {
1009 self.ptr.as_ptr()
1010 }
1011}
1012
1013#[cfg(feature = "host")]
1015unsafe impl Send for ValidatedStrategyVTable {}
1016#[cfg(feature = "host")]
1018unsafe impl Sync for ValidatedStrategyVTable {}
1019
1020#[cfg(feature = "host")]
1022#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1023pub struct ValidatedControllerVTable {
1024 ptr: std::ptr::NonNull<ControllerVTable>,
1025}
1026
1027#[cfg(feature = "host")]
1028impl ValidatedControllerVTable {
1029 fn from_validated_ptr(ptr: *const ControllerVTable) -> Self {
1030 Self {
1031 ptr: std::ptr::NonNull::new(ptr.cast_mut())
1032 .expect("validated manifest stores non-null ControllerVTable"),
1033 }
1034 }
1035
1036 #[must_use]
1043 pub unsafe fn from_raw_unchecked(ptr: *const ControllerVTable) -> Self {
1044 Self::from_validated_ptr(ptr)
1045 }
1046
1047 #[must_use]
1049 pub fn as_ptr(self) -> *const ControllerVTable {
1050 self.ptr.as_ptr()
1051 }
1052}
1053
1054#[cfg(feature = "host")]
1056unsafe impl Send for ValidatedControllerVTable {}
1057#[cfg(feature = "host")]
1059unsafe impl Sync for ValidatedControllerVTable {}
1060
1061#[cfg(test)]
1062mod tests {
1063 use std::sync::LazyLock;
1064
1065 use rstest::rstest;
1066
1067 use super::*;
1068
1069 #[derive(Clone, PartialEq)]
1070 struct ManifestTestTick;
1071
1072 impl crate::surfaces::custom_data::PluginCustomData for ManifestTestTick {
1073 const TYPE_NAME: &'static str = "ManifestTestTick";
1074
1075 fn ts_event(&self) -> u64 {
1076 0
1077 }
1078
1079 fn ts_init(&self) -> u64 {
1080 0
1081 }
1082
1083 fn to_json(&self) -> anyhow::Result<Vec<u8>> {
1084 Ok(Vec::new())
1085 }
1086
1087 fn from_json(_payload: &[u8]) -> anyhow::Result<Self> {
1088 Ok(Self)
1089 }
1090
1091 fn schema_ipc() -> anyhow::Result<Vec<u8>> {
1092 Ok(Vec::new())
1093 }
1094
1095 fn encode_batch(_items: &[&Self]) -> anyhow::Result<Vec<u8>> {
1096 Ok(Vec::new())
1097 }
1098
1099 fn decode_batch(
1100 _ipc_bytes: &[u8],
1101 _metadata: &[(String, String)],
1102 ) -> anyhow::Result<Vec<Self>> {
1103 Ok(Vec::new())
1104 }
1105 }
1106
1107 struct ManifestTestActor;
1108
1109 impl crate::surfaces::actor::PluginActor for ManifestTestActor {
1110 const TYPE_NAME: &'static str = "ManifestTestActor";
1111
1112 fn new(
1113 _host: *const HostVTable,
1114 _ctx: *const crate::host::HostContext,
1115 _config_json: &str,
1116 ) -> Self {
1117 Self
1118 }
1119 }
1120
1121 struct ManifestTestStrategy;
1122
1123 impl crate::surfaces::strategy::PluginStrategy for ManifestTestStrategy {
1124 const TYPE_NAME: &'static str = "ManifestTestStrategy";
1125
1126 fn new(
1127 _host: *const HostVTable,
1128 _ctx: *const crate::host::HostContext,
1129 _config_json: &str,
1130 ) -> Self {
1131 Self
1132 }
1133 }
1134
1135 struct ManifestTestController;
1136
1137 impl crate::surfaces::controller::PluginController for ManifestTestController {
1138 const TYPE_NAME: &'static str = "ManifestTestController";
1139
1140 fn new(
1141 _host: *const crate::host::ControllerHostVTable,
1142 _ctx: *const crate::host::ControllerHostContext,
1143 _config_json: &str,
1144 ) -> Self {
1145 Self
1146 }
1147 }
1148
1149 static VALID_CUSTOM_DATA: LazyLock<[CustomDataRegistration; 1]> = LazyLock::new(|| {
1150 [CustomDataRegistration {
1151 type_name: BorrowedStr::from_str("TestTick"),
1152 vtable: crate::surfaces::custom_data::custom_data_vtable::<ManifestTestTick>(),
1153 }]
1154 });
1155 static VALID_ACTORS: LazyLock<[ActorRegistration; 1]> = LazyLock::new(|| {
1156 [ActorRegistration {
1157 type_name: BorrowedStr::from_str("TestActor"),
1158 vtable: crate::surfaces::actor::actor_vtable::<ManifestTestActor>(),
1159 }]
1160 });
1161 static VALID_STRATEGIES: LazyLock<[StrategyRegistration; 1]> = LazyLock::new(|| {
1162 [StrategyRegistration {
1163 type_name: BorrowedStr::from_str("TestStrategy"),
1164 vtable: crate::surfaces::strategy::strategy_vtable::<ManifestTestStrategy>(),
1165 }]
1166 });
1167 static VALID_CONTROLLERS: LazyLock<[ControllerRegistration; 1]> = LazyLock::new(|| {
1168 [ControllerRegistration {
1169 type_name: BorrowedStr::from_str("TestController"),
1170 vtable: crate::surfaces::controller::controller_vtable::<ManifestTestController>(),
1171 }]
1172 });
1173 static DUPLICATE_CUSTOM_DATA: LazyLock<[CustomDataRegistration; 1]> = LazyLock::new(|| {
1174 [CustomDataRegistration {
1175 type_name: BorrowedStr::from_str("DuplicateType"),
1176 vtable: crate::surfaces::custom_data::custom_data_vtable::<ManifestTestTick>(),
1177 }]
1178 });
1179 static DUPLICATE_ACTORS: LazyLock<[ActorRegistration; 1]> = LazyLock::new(|| {
1180 [ActorRegistration {
1181 type_name: BorrowedStr::from_str("DuplicateType"),
1182 vtable: crate::surfaces::actor::actor_vtable::<ManifestTestActor>(),
1183 }]
1184 });
1185 static DUPLICATE_ACTORS_SAME_SLICE: LazyLock<[ActorRegistration; 2]> = LazyLock::new(|| {
1186 [
1187 ActorRegistration {
1188 type_name: BorrowedStr::from_str("DuplicateActor"),
1189 vtable: crate::surfaces::actor::actor_vtable::<ManifestTestActor>(),
1190 },
1191 ActorRegistration {
1192 type_name: BorrowedStr::from_str("DuplicateActor"),
1193 vtable: crate::surfaces::actor::actor_vtable::<ManifestTestActor>(),
1194 },
1195 ]
1196 });
1197 static EMPTY_TYPE_NAME_ACTORS: LazyLock<[ActorRegistration; 1]> = LazyLock::new(|| {
1198 [ActorRegistration {
1199 type_name: BorrowedStr::empty(),
1200 vtable: crate::surfaces::actor::actor_vtable::<ManifestTestActor>(),
1201 }]
1202 });
1203 static NULL_VTABLE_CUSTOM_DATA: [CustomDataRegistration; 1] = [CustomDataRegistration {
1204 type_name: BorrowedStr::from_str("NullVTableType"),
1205 vtable: std::ptr::null(),
1206 }];
1207 static INVALID_UTF8: [u8; 1] = [0xff];
1208
1209 fn custom_data_registration(
1210 type_name: &'static str,
1211 vtable: *const CustomDataVTable,
1212 ) -> Slice<'static, CustomDataRegistration> {
1213 let entries = Box::leak(Box::new([CustomDataRegistration {
1214 type_name: BorrowedStr::from_str(type_name),
1215 vtable,
1216 }]));
1217 Slice::from_slice(entries)
1218 }
1219
1220 fn actor_registration(
1221 type_name: &'static str,
1222 vtable: *const ActorVTable,
1223 ) -> Slice<'static, ActorRegistration> {
1224 let entries = Box::leak(Box::new([ActorRegistration {
1225 type_name: BorrowedStr::from_str(type_name),
1226 vtable,
1227 }]));
1228 Slice::from_slice(entries)
1229 }
1230
1231 fn strategy_registration(
1232 type_name: &'static str,
1233 vtable: *const StrategyVTable,
1234 ) -> Slice<'static, StrategyRegistration> {
1235 let entries = Box::leak(Box::new([StrategyRegistration {
1236 type_name: BorrowedStr::from_str(type_name),
1237 vtable,
1238 }]));
1239 Slice::from_slice(entries)
1240 }
1241
1242 fn controller_registration(
1243 type_name: &'static str,
1244 vtable: *const ControllerVTable,
1245 ) -> Slice<'static, ControllerRegistration> {
1246 let entries = Box::leak(Box::new([ControllerRegistration {
1247 type_name: BorrowedStr::from_str(type_name),
1248 vtable,
1249 }]));
1250 Slice::from_slice(entries)
1251 }
1252
1253 fn custom_data_vtable_missing_schema_ipc() -> *const CustomDataVTable {
1254 let valid = crate::surfaces::custom_data::custom_data_vtable::<ManifestTestTick>();
1255 let valid = unsafe { &*valid };
1257 let vtable = Box::leak(Box::new(CustomDataVTable {
1258 type_name: valid.type_name,
1259 schema_ipc: None,
1260 from_json: valid.from_json,
1261 encode_batch: valid.encode_batch,
1262 decode_batch: valid.decode_batch,
1263 ts_event: valid.ts_event,
1264 ts_init: valid.ts_init,
1265 to_json: valid.to_json,
1266 clone_handle: valid.clone_handle,
1267 drop_handle: valid.drop_handle,
1268 eq_handles: valid.eq_handles,
1269 }));
1270 std::ptr::from_ref(&*vtable)
1271 }
1272
1273 fn custom_data_vtable_missing_type_name() -> *const CustomDataVTable {
1274 let valid = crate::surfaces::custom_data::custom_data_vtable::<ManifestTestTick>();
1275 let valid = unsafe { &*valid };
1277 let vtable = Box::leak(Box::new(CustomDataVTable {
1278 type_name: None,
1279 schema_ipc: valid.schema_ipc,
1280 from_json: valid.from_json,
1281 encode_batch: valid.encode_batch,
1282 decode_batch: valid.decode_batch,
1283 ts_event: valid.ts_event,
1284 ts_init: valid.ts_init,
1285 to_json: valid.to_json,
1286 clone_handle: valid.clone_handle,
1287 drop_handle: valid.drop_handle,
1288 eq_handles: valid.eq_handles,
1289 }));
1290 std::ptr::from_ref(&*vtable)
1291 }
1292
1293 fn actor_vtable_missing_on_quote_and_on_book() -> *const ActorVTable {
1294 let valid = crate::surfaces::actor::actor_vtable::<ManifestTestActor>();
1295 let valid = unsafe { &*valid };
1297 let vtable = Box::leak(Box::new(ActorVTable {
1298 create: valid.create,
1299 drop_handle: valid.drop_handle,
1300 type_name: valid.type_name,
1301 on_start: valid.on_start,
1302 on_stop: valid.on_stop,
1303 on_resume: valid.on_resume,
1304 on_reset: valid.on_reset,
1305 on_dispose: valid.on_dispose,
1306 on_degrade: valid.on_degrade,
1307 on_fault: valid.on_fault,
1308 on_time_event: valid.on_time_event,
1309 on_data: valid.on_data,
1310 on_instrument: valid.on_instrument,
1311 on_book_deltas: valid.on_book_deltas,
1312 on_book: None,
1313 on_quote: None,
1314 on_trade: valid.on_trade,
1315 on_bar: valid.on_bar,
1316 on_mark_price: valid.on_mark_price,
1317 on_index_price: valid.on_index_price,
1318 on_funding_rate: valid.on_funding_rate,
1319 on_option_greeks: valid.on_option_greeks,
1320 on_option_chain: valid.on_option_chain,
1321 on_instrument_status: valid.on_instrument_status,
1322 on_instrument_close: valid.on_instrument_close,
1323 on_order_filled: valid.on_order_filled,
1324 on_order_canceled: valid.on_order_canceled,
1325 on_signal: valid.on_signal,
1326 on_historical_book_deltas: valid.on_historical_book_deltas,
1327 on_historical_book_depth: valid.on_historical_book_depth,
1328 on_historical_quotes: valid.on_historical_quotes,
1329 on_historical_trades: valid.on_historical_trades,
1330 on_historical_bars: valid.on_historical_bars,
1331 on_historical_mark_prices: valid.on_historical_mark_prices,
1332 on_historical_index_prices: valid.on_historical_index_prices,
1333 on_historical_funding_rates: valid.on_historical_funding_rates,
1334 }));
1335 std::ptr::from_ref(&*vtable)
1336 }
1337
1338 fn actor_vtable_missing_create() -> *const ActorVTable {
1339 let valid = crate::surfaces::actor::actor_vtable::<ManifestTestActor>();
1340 let valid = unsafe { &*valid };
1342 let vtable = Box::leak(Box::new(ActorVTable {
1343 create: None,
1344 drop_handle: valid.drop_handle,
1345 type_name: valid.type_name,
1346 on_start: valid.on_start,
1347 on_stop: valid.on_stop,
1348 on_resume: valid.on_resume,
1349 on_reset: valid.on_reset,
1350 on_dispose: valid.on_dispose,
1351 on_degrade: valid.on_degrade,
1352 on_fault: valid.on_fault,
1353 on_time_event: valid.on_time_event,
1354 on_data: valid.on_data,
1355 on_instrument: valid.on_instrument,
1356 on_book_deltas: valid.on_book_deltas,
1357 on_book: valid.on_book,
1358 on_quote: valid.on_quote,
1359 on_trade: valid.on_trade,
1360 on_bar: valid.on_bar,
1361 on_mark_price: valid.on_mark_price,
1362 on_index_price: valid.on_index_price,
1363 on_funding_rate: valid.on_funding_rate,
1364 on_option_greeks: valid.on_option_greeks,
1365 on_option_chain: valid.on_option_chain,
1366 on_instrument_status: valid.on_instrument_status,
1367 on_instrument_close: valid.on_instrument_close,
1368 on_order_filled: valid.on_order_filled,
1369 on_order_canceled: valid.on_order_canceled,
1370 on_signal: valid.on_signal,
1371 on_historical_book_deltas: valid.on_historical_book_deltas,
1372 on_historical_book_depth: valid.on_historical_book_depth,
1373 on_historical_quotes: valid.on_historical_quotes,
1374 on_historical_trades: valid.on_historical_trades,
1375 on_historical_bars: valid.on_historical_bars,
1376 on_historical_mark_prices: valid.on_historical_mark_prices,
1377 on_historical_index_prices: valid.on_historical_index_prices,
1378 on_historical_funding_rates: valid.on_historical_funding_rates,
1379 }));
1380 std::ptr::from_ref(&*vtable)
1381 }
1382
1383 fn strategy_vtable_missing_on_book_and_on_position_closed() -> *const StrategyVTable {
1384 let valid = crate::surfaces::strategy::strategy_vtable::<ManifestTestStrategy>();
1385 let valid = unsafe { &*valid };
1387 let vtable = Box::leak(Box::new(StrategyVTable {
1388 create: valid.create,
1389 drop_handle: valid.drop_handle,
1390 type_name: valid.type_name,
1391 on_start: valid.on_start,
1392 on_stop: valid.on_stop,
1393 on_resume: valid.on_resume,
1394 on_reset: valid.on_reset,
1395 on_dispose: valid.on_dispose,
1396 on_degrade: valid.on_degrade,
1397 on_fault: valid.on_fault,
1398 on_time_event: valid.on_time_event,
1399 on_data: valid.on_data,
1400 on_instrument: valid.on_instrument,
1401 on_book_deltas: valid.on_book_deltas,
1402 on_book: None,
1403 on_quote: valid.on_quote,
1404 on_trade: valid.on_trade,
1405 on_bar: valid.on_bar,
1406 on_mark_price: valid.on_mark_price,
1407 on_index_price: valid.on_index_price,
1408 on_funding_rate: valid.on_funding_rate,
1409 on_option_greeks: valid.on_option_greeks,
1410 on_option_chain: valid.on_option_chain,
1411 on_instrument_status: valid.on_instrument_status,
1412 on_instrument_close: valid.on_instrument_close,
1413 on_signal: valid.on_signal,
1414 on_order_initialized: valid.on_order_initialized,
1415 on_order_submitted: valid.on_order_submitted,
1416 on_order_accepted: valid.on_order_accepted,
1417 on_order_rejected: valid.on_order_rejected,
1418 on_order_filled: valid.on_order_filled,
1419 on_order_canceled: valid.on_order_canceled,
1420 on_order_expired: valid.on_order_expired,
1421 on_order_triggered: valid.on_order_triggered,
1422 on_order_denied: valid.on_order_denied,
1423 on_order_emulated: valid.on_order_emulated,
1424 on_order_released: valid.on_order_released,
1425 on_order_pending_update: valid.on_order_pending_update,
1426 on_order_pending_cancel: valid.on_order_pending_cancel,
1427 on_order_modify_rejected: valid.on_order_modify_rejected,
1428 on_order_cancel_rejected: valid.on_order_cancel_rejected,
1429 on_order_updated: valid.on_order_updated,
1430 on_position_opened: valid.on_position_opened,
1431 on_position_changed: valid.on_position_changed,
1432 on_position_closed: None,
1433 on_market_exit: valid.on_market_exit,
1434 on_historical_book_deltas: valid.on_historical_book_deltas,
1435 on_historical_book_depth: valid.on_historical_book_depth,
1436 on_historical_quotes: valid.on_historical_quotes,
1437 on_historical_trades: valid.on_historical_trades,
1438 on_historical_bars: valid.on_historical_bars,
1439 on_historical_mark_prices: valid.on_historical_mark_prices,
1440 on_historical_index_prices: valid.on_historical_index_prices,
1441 on_historical_funding_rates: valid.on_historical_funding_rates,
1442 }));
1443 std::ptr::from_ref(&*vtable)
1444 }
1445
1446 fn strategy_vtable_missing_drop_handle() -> *const StrategyVTable {
1447 let valid = crate::surfaces::strategy::strategy_vtable::<ManifestTestStrategy>();
1448 let valid = unsafe { &*valid };
1450 let vtable = Box::leak(Box::new(StrategyVTable {
1451 create: valid.create,
1452 drop_handle: None,
1453 type_name: valid.type_name,
1454 on_start: valid.on_start,
1455 on_stop: valid.on_stop,
1456 on_resume: valid.on_resume,
1457 on_reset: valid.on_reset,
1458 on_dispose: valid.on_dispose,
1459 on_degrade: valid.on_degrade,
1460 on_fault: valid.on_fault,
1461 on_time_event: valid.on_time_event,
1462 on_data: valid.on_data,
1463 on_instrument: valid.on_instrument,
1464 on_book_deltas: valid.on_book_deltas,
1465 on_book: valid.on_book,
1466 on_quote: valid.on_quote,
1467 on_trade: valid.on_trade,
1468 on_bar: valid.on_bar,
1469 on_mark_price: valid.on_mark_price,
1470 on_index_price: valid.on_index_price,
1471 on_funding_rate: valid.on_funding_rate,
1472 on_option_greeks: valid.on_option_greeks,
1473 on_option_chain: valid.on_option_chain,
1474 on_instrument_status: valid.on_instrument_status,
1475 on_instrument_close: valid.on_instrument_close,
1476 on_signal: valid.on_signal,
1477 on_order_initialized: valid.on_order_initialized,
1478 on_order_submitted: valid.on_order_submitted,
1479 on_order_accepted: valid.on_order_accepted,
1480 on_order_rejected: valid.on_order_rejected,
1481 on_order_filled: valid.on_order_filled,
1482 on_order_canceled: valid.on_order_canceled,
1483 on_order_expired: valid.on_order_expired,
1484 on_order_triggered: valid.on_order_triggered,
1485 on_order_denied: valid.on_order_denied,
1486 on_order_emulated: valid.on_order_emulated,
1487 on_order_released: valid.on_order_released,
1488 on_order_pending_update: valid.on_order_pending_update,
1489 on_order_pending_cancel: valid.on_order_pending_cancel,
1490 on_order_modify_rejected: valid.on_order_modify_rejected,
1491 on_order_cancel_rejected: valid.on_order_cancel_rejected,
1492 on_order_updated: valid.on_order_updated,
1493 on_position_opened: valid.on_position_opened,
1494 on_position_changed: valid.on_position_changed,
1495 on_position_closed: valid.on_position_closed,
1496 on_market_exit: valid.on_market_exit,
1497 on_historical_book_deltas: valid.on_historical_book_deltas,
1498 on_historical_book_depth: valid.on_historical_book_depth,
1499 on_historical_quotes: valid.on_historical_quotes,
1500 on_historical_trades: valid.on_historical_trades,
1501 on_historical_bars: valid.on_historical_bars,
1502 on_historical_mark_prices: valid.on_historical_mark_prices,
1503 on_historical_index_prices: valid.on_historical_index_prices,
1504 on_historical_funding_rates: valid.on_historical_funding_rates,
1505 }));
1506 std::ptr::from_ref(&*vtable)
1507 }
1508
1509 fn controller_vtable_missing_prepare() -> *const ControllerVTable {
1510 let valid = crate::surfaces::controller::controller_vtable::<ManifestTestController>();
1511 let valid = unsafe { &*valid };
1513 let vtable = Box::leak(Box::new(ControllerVTable {
1514 prepare: None,
1515 create: valid.create,
1516 drop_handle: valid.drop_handle,
1517 type_name: valid.type_name,
1518 on_start: valid.on_start,
1519 on_stop: valid.on_stop,
1520 on_resume: valid.on_resume,
1521 on_reset: valid.on_reset,
1522 on_dispose: valid.on_dispose,
1523 on_degrade: valid.on_degrade,
1524 on_fault: valid.on_fault,
1525 on_time_event: valid.on_time_event,
1526 }));
1527 std::ptr::from_ref(&*vtable)
1528 }
1529
1530 fn valid_manifest() -> PluginManifest {
1531 PluginManifest {
1532 abi_version: NAUTILUS_PLUGIN_ABI_VERSION,
1533 plugin_name: BorrowedStr::from_str("test"),
1534 plugin_vendor: BorrowedStr::from_str("nautech"),
1535 plugin_version: BorrowedStr::from_str("0.0.0"),
1536 build_id: PluginBuildId::current(),
1537 custom_data: Slice::empty(),
1538 actors: Slice::empty(),
1539 strategies: Slice::empty(),
1540 controllers: Slice::empty(),
1541 }
1542 }
1543
1544 #[rstest]
1545 fn current_build_id_carries_compile_time_metadata() {
1546 let id = PluginBuildId::current();
1547
1548 assert_eq!(id.schema_version, PLUGIN_BUILD_ID_VERSION);
1549 assert_eq!(
1551 unsafe { id.nautilus_plugin_version.as_str() },
1552 env!("CARGO_PKG_VERSION")
1553 );
1554 assert!(!unsafe { id.target_triple.as_str() }.is_empty());
1556 assert!(!unsafe { id.build_profile.as_str() }.is_empty());
1558 assert_eq!(
1560 unsafe { id.precision_mode.as_str() },
1561 compiled_precision_mode()
1562 );
1563 assert_eq!(id.fixed_precision, FIXED_PRECISION);
1564 }
1565
1566 #[rstest]
1567 fn empty_manifest_matches_compiled_abi() {
1568 let m = valid_manifest();
1569 assert!(m.matches_compiled_abi());
1570 m.validate().expect("empty plug-point manifest is valid");
1571 }
1572
1573 #[rstest]
1574 #[case::off_by_one(NAUTILUS_PLUGIN_ABI_VERSION.wrapping_add(1))]
1575 #[case::previous_v3(3)]
1576 #[case::zero(0)]
1577 #[case::max(u32::MAX)]
1578 fn mismatched_manifest_rejects(#[case] abi: u32) {
1579 let m = PluginManifest {
1580 abi_version: abi,
1581 ..valid_manifest()
1582 };
1583 assert!(!m.matches_compiled_abi());
1584 }
1585
1586 #[rstest]
1587 fn validate_accepts_manifest_with_all_plug_point_families() {
1588 let m = PluginManifest {
1589 custom_data: Slice::from_slice(&*VALID_CUSTOM_DATA),
1590 actors: Slice::from_slice(&*VALID_ACTORS),
1591 strategies: Slice::from_slice(&*VALID_STRATEGIES),
1592 controllers: Slice::from_slice(&*VALID_CONTROLLERS),
1593 ..valid_manifest()
1594 };
1595
1596 m.validate()
1597 .expect("well-formed plug-point registrations are valid");
1598 }
1599
1600 #[cfg(feature = "host")]
1601 #[rstest]
1602 fn validated_manifest_exposes_wrapped_registrations() {
1603 let m = PluginManifest {
1604 custom_data: Slice::from_slice(&*VALID_CUSTOM_DATA),
1605 actors: Slice::from_slice(&*VALID_ACTORS),
1606 strategies: Slice::from_slice(&*VALID_STRATEGIES),
1607 controllers: Slice::from_slice(&*VALID_CONTROLLERS),
1608 ..valid_manifest()
1609 };
1610
1611 let manifest = ValidatedPluginManifest::new(&m)
1612 .expect("well-formed plug-point registrations are valid");
1613 let custom_data = manifest.custom_data().next().expect("custom data entry");
1614 let actor = manifest.actors().next().expect("actor entry");
1615 let strategy = manifest.strategies().next().expect("strategy entry");
1616 let controller = manifest.controllers().next().expect("controller entry");
1617
1618 assert_eq!(manifest.plugin_name(), "test");
1619 assert_eq!(custom_data.type_name(), "TestTick");
1620 assert_eq!(actor.type_name(), "TestActor");
1621 assert_eq!(strategy.type_name(), "TestStrategy");
1622 assert_eq!(controller.type_name(), "TestController");
1623 assert_eq!(custom_data.vtable().as_ptr(), VALID_CUSTOM_DATA[0].vtable);
1624 assert_eq!(actor.vtable().as_ptr(), VALID_ACTORS[0].vtable);
1625 assert_eq!(strategy.vtable().as_ptr(), VALID_STRATEGIES[0].vtable);
1626 assert_eq!(controller.vtable().as_ptr(), VALID_CONTROLLERS[0].vtable);
1627 }
1628
1629 #[rstest]
1630 fn validate_rejects_empty_required_manifest_identifiers() {
1631 let m = PluginManifest {
1632 plugin_name: BorrowedStr::empty(),
1633 plugin_version: BorrowedStr::empty(),
1634 ..valid_manifest()
1635 };
1636
1637 let errors = m.validate().expect_err("empty identifiers are invalid");
1638
1639 assert!(
1640 errors
1641 .messages()
1642 .iter()
1643 .any(|message| message == "plugin_name must not be empty")
1644 );
1645 assert!(
1646 errors
1647 .messages()
1648 .iter()
1649 .any(|message| message == "plugin_version must not be empty")
1650 );
1651 }
1652
1653 #[rstest]
1654 fn validate_rejects_mismatched_build_id_schema() {
1655 let m = PluginManifest {
1656 build_id: PluginBuildId {
1657 schema_version: PLUGIN_BUILD_ID_VERSION + 1,
1658 ..PluginBuildId::current()
1659 },
1660 ..valid_manifest()
1661 };
1662
1663 let errors = m
1664 .validate()
1665 .expect_err("mismatched build-id schema is invalid");
1666
1667 let expected = format!(
1668 "build_id.schema_version {} does not match supported schema {}",
1669 PLUGIN_BUILD_ID_VERSION + 1,
1670 PLUGIN_BUILD_ID_VERSION
1671 );
1672 assert!(errors.to_string().contains(&expected));
1673 }
1674
1675 #[rstest]
1676 fn validate_rejects_mismatched_precision_mode() {
1677 let precision_mode = if compiled_precision_mode() == "high-precision" {
1678 "standard"
1679 } else {
1680 "high-precision"
1681 };
1682 let fixed_precision = if FIXED_PRECISION > 9 { 9 } else { 16 };
1683 let m = PluginManifest {
1684 build_id: PluginBuildId {
1685 precision_mode: BorrowedStr::from_str(precision_mode),
1686 fixed_precision,
1687 ..PluginBuildId::current()
1688 },
1689 ..valid_manifest()
1690 };
1691
1692 let errors = m
1693 .validate()
1694 .expect_err("mismatched precision mode is invalid");
1695
1696 let rendered = errors.to_string();
1697 assert!(rendered.contains(&format!(
1698 "build_id.precision_mode '{precision_mode}' does not match host precision mode '{}'",
1699 compiled_precision_mode()
1700 )));
1701 assert!(rendered.contains(&format!(
1702 "build_id.fixed_precision {fixed_precision} does not match host fixed precision {FIXED_PRECISION}"
1703 )));
1704 }
1705
1706 #[rstest]
1707 fn validate_rejects_empty_type_name_duplicate_type_name_and_null_vtable() {
1708 let m = PluginManifest {
1709 custom_data: Slice::from_slice(&*DUPLICATE_CUSTOM_DATA),
1710 actors: Slice::from_slice(&*DUPLICATE_ACTORS),
1711 strategies: Slice::from_slice(&*VALID_STRATEGIES),
1712 ..valid_manifest()
1713 };
1714
1715 let errors = m.validate().expect_err("duplicate type name is invalid");
1716 assert!(
1717 errors
1718 .to_string()
1719 .contains("type name 'DuplicateType' appears in both custom_data[0] and actors[0]")
1720 );
1721
1722 let m = PluginManifest {
1723 actors: Slice::from_slice(&*EMPTY_TYPE_NAME_ACTORS),
1724 ..valid_manifest()
1725 };
1726 let errors = m.validate().expect_err("empty type name is invalid");
1727 assert!(
1728 errors
1729 .messages()
1730 .iter()
1731 .any(|message| message == "actors[0].type_name must not be empty")
1732 );
1733
1734 let m = PluginManifest {
1735 custom_data: Slice::from_slice(&NULL_VTABLE_CUSTOM_DATA),
1736 ..valid_manifest()
1737 };
1738 let errors = m.validate().expect_err("null vtable is invalid");
1739 assert!(
1740 errors
1741 .messages()
1742 .iter()
1743 .any(|message| message == "custom_data[0].vtable must not be null")
1744 );
1745 }
1746
1747 #[rstest]
1748 fn validate_rejects_null_required_vtable_slots() {
1749 let m = PluginManifest {
1750 custom_data: custom_data_registration(
1751 "BadTick",
1752 custom_data_vtable_missing_schema_ipc(),
1753 ),
1754 actors: actor_registration("BadActor", actor_vtable_missing_on_quote_and_on_book()),
1755 strategies: strategy_registration(
1756 "BadStrategy",
1757 strategy_vtable_missing_on_book_and_on_position_closed(),
1758 ),
1759 controllers: controller_registration(
1760 "BadController",
1761 controller_vtable_missing_prepare(),
1762 ),
1763 ..valid_manifest()
1764 };
1765
1766 let errors = m
1767 .validate()
1768 .expect_err("null required vtable slots are invalid");
1769 let rendered = errors.to_string();
1770
1771 assert!(
1772 rendered.contains("custom_data[0] type 'BadTick' vtable.schema_ipc must not be null")
1773 );
1774 assert!(rendered.contains("actors[0] type 'BadActor' vtable.on_book must not be null"));
1775 assert!(rendered.contains("actors[0] type 'BadActor' vtable.on_quote must not be null"));
1776 assert!(
1777 rendered.contains("strategies[0] type 'BadStrategy' vtable.on_book must not be null")
1778 );
1779 assert!(rendered.contains(
1780 "strategies[0] type 'BadStrategy' vtable.on_position_closed must not be null"
1781 ));
1782 assert!(
1783 rendered
1784 .contains("controllers[0] type 'BadController' vtable.prepare must not be null")
1785 );
1786 }
1787
1788 #[rstest]
1789 fn validate_rejects_null_identity_constructor_and_drop_vtable_slots() {
1790 let m = PluginManifest {
1791 custom_data: custom_data_registration(
1792 "MissingTypeNameTick",
1793 custom_data_vtable_missing_type_name(),
1794 ),
1795 actors: actor_registration("MissingCreateActor", actor_vtable_missing_create()),
1796 strategies: strategy_registration(
1797 "MissingDropStrategy",
1798 strategy_vtable_missing_drop_handle(),
1799 ),
1800 ..valid_manifest()
1801 };
1802
1803 let errors = m
1804 .validate()
1805 .expect_err("identity, constructor, and drop slots are required");
1806 let rendered = errors.to_string();
1807
1808 assert!(rendered.contains(
1809 "custom_data[0] type 'MissingTypeNameTick' vtable.type_name must not be null"
1810 ));
1811 assert!(
1812 rendered.contains("actors[0] type 'MissingCreateActor' vtable.create must not be null")
1813 );
1814 assert!(rendered.contains(
1815 "strategies[0] type 'MissingDropStrategy' vtable.drop_handle must not be null"
1816 ));
1817 }
1818
1819 #[rstest]
1820 fn validate_rejects_duplicate_type_names_within_one_plug_point_slice() {
1821 let m = PluginManifest {
1822 actors: Slice::from_slice(&*DUPLICATE_ACTORS_SAME_SLICE),
1823 ..valid_manifest()
1824 };
1825
1826 let errors = m
1827 .validate()
1828 .expect_err("duplicate type names in one slice are invalid");
1829
1830 assert!(
1831 errors
1832 .to_string()
1833 .contains("type name 'DuplicateActor' appears in both actors[0] and actors[1]")
1834 );
1835 }
1836
1837 #[rstest]
1838 fn validate_rejects_malformed_raw_string_and_slice_descriptors() {
1839 let mut plugin_name = BorrowedStr::empty();
1840 plugin_name.ptr = INVALID_UTF8.as_ptr();
1841 plugin_name.len = INVALID_UTF8.len();
1842
1843 let mut plugin_vendor = BorrowedStr::empty();
1844 plugin_vendor.len = 1;
1845
1846 let mut custom_data: Slice<'static, CustomDataRegistration> = Slice::empty();
1847 custom_data.len = 1;
1848
1849 let m = PluginManifest {
1850 plugin_name,
1851 plugin_vendor,
1852 custom_data,
1853 ..valid_manifest()
1854 };
1855
1856 let errors = m
1857 .validate()
1858 .expect_err("malformed raw descriptors are invalid");
1859 let rendered = errors.to_string();
1860
1861 assert!(rendered.contains("plugin_name is not valid UTF-8"));
1862 assert!(rendered.contains("plugin_vendor has null pointer with non-zero length 1"));
1863 assert!(rendered.contains("custom_data has null pointer with non-zero length 1"));
1864 }
1865}