Skip to main content

nautilus_plugin/
manifest.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.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//! Static manifest a plug-in returns from `nautilus_plugin_init`.
17//!
18//! The manifest enumerates every plug-point contribution the cdylib provides
19//! and points at the per-type vtables. The current unreleased surface ships
20//! custom-data, actor, and strategy plug-point families. Future released
21//! revisions should add new `Slice` fields to [`PluginManifest`] without
22//! removing existing ones.
23
24use 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
42/// Signature of the single `extern "C"` entry symbol every plug-in exports
43/// under the name [`crate::NAUTILUS_PLUGIN_INIT_SYMBOL`].
44///
45/// The host calls this once at load time with a pointer to its `HostVTable`.
46/// The plug-in returns a pointer to its `'static` [`PluginManifest`], or null
47/// to signal load failure. The host reports null as `LoadError::NullManifest`
48/// with the plug-in path.
49pub type PluginInitFn = unsafe extern "C" fn(host: *const HostVTable) -> *const PluginManifest;
50
51/// Versioned build identifier carried by [`PluginManifest`].
52///
53/// The fields identify the Nautilus plug-in crate and build environment that
54/// produced the manifest. The host validates the precision mode because it
55/// changes model type layout across the plug-in boundary. Other build fields
56/// remain diagnostic.
57#[repr(C)]
58#[derive(Clone, Copy)]
59pub struct PluginBuildId {
60    /// Build identifier schema version. Must equal
61    /// [`PLUGIN_BUILD_ID_VERSION`] for the fields below.
62    pub schema_version: u32,
63
64    /// Version of the `nautilus-plugin` crate used to build the plug-in.
65    pub nautilus_plugin_version: BorrowedStr<'static>,
66
67    /// Rust compiler version reported by `rustc --version`, or empty when it
68    /// was unavailable to the build script.
69    pub rustc_version: BorrowedStr<'static>,
70
71    /// Cargo target triple, or empty when Cargo did not expose one.
72    pub target_triple: BorrowedStr<'static>,
73
74    /// Cargo build profile, or empty when Cargo did not expose one.
75    pub build_profile: BorrowedStr<'static>,
76
77    /// Model fixed-point precision mode used to build the plug-in.
78    pub precision_mode: BorrowedStr<'static>,
79
80    /// Maximum fixed-point decimal precision used to build the plug-in.
81    pub fixed_precision: u8,
82}
83
84impl PluginBuildId {
85    /// Returns the build identifier for the compiled `nautilus-plugin` crate.
86    #[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/// Returns the model precision mode compiled into this crate.
101#[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/// Manifest validation failures collected by the host loader.
111#[derive(Clone, Debug, Default, PartialEq, Eq)]
112pub struct PluginManifestValidationErrors {
113    messages: Vec<String>,
114}
115
116impl PluginManifestValidationErrors {
117    /// Returns whether validation found no failures.
118    #[must_use]
119    pub fn is_empty(&self) -> bool {
120        self.messages.is_empty()
121    }
122
123    /// Returns the validation failure messages in deterministic order.
124    #[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/// The static, process-lifetime manifest a plug-in returns from
149/// `nautilus_plugin_init`.
150///
151/// Every `Slice` here borrows from `'static` storage in the plug-in's
152/// cdylib. The host treats the entire manifest as immutable.
153#[repr(C)]
154pub struct PluginManifest {
155    /// ABI version. Must equal [`NAUTILUS_PLUGIN_ABI_VERSION`] or the host
156    /// refuses to load the plug-in.
157    pub abi_version: u32,
158
159    /// Short machine-readable plug-in name (e.g. `"my-momentum"`).
160    pub plugin_name: BorrowedStr<'static>,
161
162    /// Free-form vendor or author string.
163    pub plugin_vendor: BorrowedStr<'static>,
164
165    /// Plug-in version (typically the crate's `CARGO_PKG_VERSION`).
166    pub plugin_version: BorrowedStr<'static>,
167
168    /// Versioned build identifier for diagnostics.
169    pub build_id: PluginBuildId,
170
171    /// Custom-data registrations contributed by this plug-in.
172    pub custom_data: Slice<'static, CustomDataRegistration>,
173
174    /// Actor registrations contributed by this plug-in.
175    pub actors: Slice<'static, ActorRegistration>,
176
177    /// Strategy registrations contributed by this plug-in.
178    pub strategies: Slice<'static, StrategyRegistration>,
179
180    /// Controller registrations contributed by this plug-in.
181    pub controllers: Slice<'static, ControllerRegistration>,
182    // Future plug-point slices land here and require rebuilding plug-ins:
183    //   pub indicators: Slice<'static, IndicatorRegistration>,
184    //   pub fill_models: Slice<'static, FillModelRegistration>,
185    //   ...
186}
187
188impl PluginManifest {
189    /// Returns whether this manifest is compatible with the compiled-in ABI.
190    #[must_use]
191    pub fn matches_compiled_abi(&self) -> bool {
192        self.abi_version == NAUTILUS_PLUGIN_ABI_VERSION
193    }
194
195    /// Validates manifest invariants the host relies on before registration.
196    ///
197    /// This does not decide plug-in compatibility beyond the explicit ABI,
198    /// build-id schema, and fixed-point precision mode. The remaining build-id
199    /// content stays diagnostic; empty compiler, target, and profile strings do
200    /// not make a manifest invalid.
201    ///
202    /// # Errors
203    ///
204    /// Returns every structural problem found in the manifest.
205    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    // SAFETY: caller checked the vtable pointer is non-null. Validation only
335    // reads nullable function-pointer slots and never invokes plug-in code.
336    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    // SAFETY: caller checked the vtable pointer is non-null. Validation only
365    // reads nullable function-pointer slots and never invokes plug-in code.
366    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    // SAFETY: caller checked the vtable pointer is non-null. Validation only
420    // reads nullable function-pointer slots and never invokes plug-in code.
421    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    // SAFETY: caller checked the vtable pointer is non-null. Validation only
493    // reads nullable function-pointer slots and never invokes plug-in code.
494    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    // SAFETY: the manifest contract requires a non-null slice pointer with
586    // `len` elements to point at process-lifetime storage in the plug-in.
587    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    // SAFETY: the manifest contract requires borrowed strings to point at
620    // process-lifetime storage in the plug-in.
621    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/// Registration entry for one custom-data type.
632#[repr(C)]
633pub struct CustomDataRegistration {
634    /// Canonical type name; must match the `type_name` returned by the vtable.
635    pub type_name: BorrowedStr<'static>,
636    /// Pointer to the static vtable for this type.
637    pub vtable: *const CustomDataVTable,
638}
639
640/// SAFETY: the pointer is `'static` and immutable for the process lifetime.
641unsafe impl Send for CustomDataRegistration {}
642/// SAFETY: see above.
643unsafe impl Sync for CustomDataRegistration {}
644
645/// Registration entry for one plug-in actor type.
646#[repr(C)]
647pub struct ActorRegistration {
648    /// Canonical type name; must match the `type_name` returned by the vtable.
649    pub type_name: BorrowedStr<'static>,
650    /// Pointer to the static vtable for this actor type.
651    pub vtable: *const ActorVTable,
652}
653
654/// SAFETY: the pointer is `'static` and immutable for the process lifetime.
655unsafe impl Send for ActorRegistration {}
656/// SAFETY: see above.
657unsafe impl Sync for ActorRegistration {}
658
659/// Registration entry for one plug-in strategy type.
660#[repr(C)]
661pub struct StrategyRegistration {
662    /// Canonical type name; must match the `type_name` returned by the vtable.
663    pub type_name: BorrowedStr<'static>,
664    /// Pointer to the static vtable for this strategy type.
665    pub vtable: *const StrategyVTable,
666}
667
668/// SAFETY: the pointer is `'static` and immutable for the process lifetime.
669unsafe impl Send for StrategyRegistration {}
670/// SAFETY: see above.
671unsafe impl Sync for StrategyRegistration {}
672
673/// Registration entry for one plug-in controller type.
674#[repr(C)]
675pub struct ControllerRegistration {
676    /// Canonical type name; must match the `type_name` returned by the vtable.
677    pub type_name: BorrowedStr<'static>,
678    /// Pointer to the static vtable for this controller type.
679    pub vtable: *const ControllerVTable,
680}
681
682/// SAFETY: the pointer is `'static` and immutable for the process lifetime.
683unsafe impl Send for ControllerRegistration {}
684/// SAFETY: see above.
685unsafe impl Sync for ControllerRegistration {}
686
687/// Host-side view of a manifest that passed structural validation.
688///
689/// This wrapper is not part of the ABI. Hosts use it after loader validation
690/// so registration code can carry the manifest invariants in the type system.
691#[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    /// Validates `manifest` and returns a typed host-side view.
709    ///
710    /// # Errors
711    ///
712    /// Returns every structural problem found in the manifest.
713    pub fn new(manifest: &'a PluginManifest) -> Result<Self, PluginManifestValidationErrors> {
714        manifest.validate()?;
715        Ok(Self { manifest })
716    }
717
718    /// Returns the raw ABI manifest behind this validated view.
719    #[must_use]
720    pub fn manifest(self) -> &'a PluginManifest {
721        self.manifest
722    }
723
724    /// Returns the validated plug-in name.
725    #[must_use]
726    pub fn plugin_name(self) -> &'static str {
727        // SAFETY: validation checked the descriptor and manifest strings live
728        // in static plug-in storage.
729        unsafe { self.manifest.plugin_name.as_str() }
730    }
731
732    /// Returns validated custom-data registrations in manifest order.
733    #[must_use]
734    pub fn custom_data(self) -> impl ExactSizeIterator<Item = ValidatedCustomDataRegistration> {
735        // SAFETY: validation checked the slice descriptor.
736        unsafe { self.manifest.custom_data.as_slice() }
737            .iter()
738            .map(ValidatedCustomDataRegistration::from_validated_registration)
739    }
740
741    /// Returns validated actor registrations in manifest order.
742    #[must_use]
743    pub fn actors(self) -> impl ExactSizeIterator<Item = ValidatedActorRegistration> {
744        // SAFETY: validation checked the slice descriptor.
745        unsafe { self.manifest.actors.as_slice() }
746            .iter()
747            .map(ValidatedActorRegistration::from_validated_registration)
748    }
749
750    /// Returns validated strategy registrations in manifest order.
751    #[must_use]
752    pub fn strategies(self) -> impl ExactSizeIterator<Item = ValidatedStrategyRegistration> {
753        // SAFETY: validation checked the slice descriptor.
754        unsafe { self.manifest.strategies.as_slice() }
755            .iter()
756            .map(ValidatedStrategyRegistration::from_validated_registration)
757    }
758
759    /// Returns validated controller registrations in manifest order.
760    #[must_use]
761    pub fn controllers(self) -> impl ExactSizeIterator<Item = ValidatedControllerRegistration> {
762        // SAFETY: validation checked the slice descriptor.
763        unsafe { self.manifest.controllers.as_slice() }
764            .iter()
765            .map(ValidatedControllerRegistration::from_validated_registration)
766    }
767}
768
769/// Host-side custom-data registration with a validated type name and vtable.
770#[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            // SAFETY: validation checked the descriptor and manifest strings
782            // live in static plug-in storage.
783            type_name: unsafe { registration.type_name.as_str() },
784            vtable: ValidatedCustomDataVTable::from_validated_ptr(registration.vtable),
785        }
786    }
787
788    /// Returns the canonical custom-data type name.
789    #[must_use]
790    pub fn type_name(self) -> &'static str {
791        self.type_name
792    }
793
794    /// Returns the validated vtable wrapper.
795    #[must_use]
796    pub fn vtable(self) -> ValidatedCustomDataVTable {
797        self.vtable
798    }
799}
800
801/// Host-side actor registration with a validated type name and vtable.
802#[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            // SAFETY: validation checked the descriptor and manifest strings
814            // live in static plug-in storage.
815            type_name: unsafe { registration.type_name.as_str() },
816            vtable: ValidatedActorVTable::from_validated_ptr(registration.vtable),
817        }
818    }
819
820    /// Returns the canonical actor type name.
821    #[must_use]
822    pub fn type_name(self) -> &'static str {
823        self.type_name
824    }
825
826    /// Returns the validated vtable wrapper.
827    #[must_use]
828    pub fn vtable(self) -> ValidatedActorVTable {
829        self.vtable
830    }
831}
832
833/// Host-side strategy registration with a validated type name and vtable.
834#[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            // SAFETY: validation checked the descriptor and manifest strings
846            // live in static plug-in storage.
847            type_name: unsafe { registration.type_name.as_str() },
848            vtable: ValidatedStrategyVTable::from_validated_ptr(registration.vtable),
849        }
850    }
851
852    /// Returns the canonical strategy type name.
853    #[must_use]
854    pub fn type_name(self) -> &'static str {
855        self.type_name
856    }
857
858    /// Returns the validated vtable wrapper.
859    #[must_use]
860    pub fn vtable(self) -> ValidatedStrategyVTable {
861        self.vtable
862    }
863}
864
865/// Host-side controller registration with a validated type name and vtable.
866#[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            // SAFETY: validation checked the descriptor and manifest strings
878            // live in static plug-in storage.
879            type_name: unsafe { registration.type_name.as_str() },
880            vtable: ValidatedControllerVTable::from_validated_ptr(registration.vtable),
881        }
882    }
883
884    /// Returns the canonical controller type name.
885    #[must_use]
886    pub fn type_name(self) -> &'static str {
887        self.type_name
888    }
889
890    /// Returns the validated vtable wrapper.
891    #[must_use]
892    pub fn vtable(self) -> ValidatedControllerVTable {
893        self.vtable
894    }
895}
896
897/// Host-side pointer to a validated [`CustomDataVTable`].
898#[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    /// Wraps a custom-data vtable pointer that the caller already validated.
914    ///
915    /// # Safety
916    ///
917    /// `ptr` must be non-null, point at immutable process-lifetime storage,
918    /// and contain every required [`CustomDataVTable`] function slot.
919    #[must_use]
920    pub unsafe fn from_raw_unchecked(ptr: *const CustomDataVTable) -> Self {
921        Self::from_validated_ptr(ptr)
922    }
923
924    /// Returns the raw vtable pointer for ABI calls.
925    #[must_use]
926    pub fn as_ptr(self) -> *const CustomDataVTable {
927        self.ptr.as_ptr()
928    }
929}
930
931/// SAFETY: validated vtables point at immutable process-lifetime storage.
932#[cfg(feature = "host")]
933unsafe impl Send for ValidatedCustomDataVTable {}
934/// SAFETY: see `Send`.
935#[cfg(feature = "host")]
936unsafe impl Sync for ValidatedCustomDataVTable {}
937
938/// Host-side pointer to a validated [`ActorVTable`].
939#[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    /// Wraps an actor vtable pointer that the caller already validated.
955    ///
956    /// # Safety
957    ///
958    /// `ptr` must be non-null, point at immutable process-lifetime storage,
959    /// and contain every required [`ActorVTable`] function slot.
960    #[must_use]
961    pub unsafe fn from_raw_unchecked(ptr: *const ActorVTable) -> Self {
962        Self::from_validated_ptr(ptr)
963    }
964
965    /// Returns the raw vtable pointer for ABI calls.
966    #[must_use]
967    pub fn as_ptr(self) -> *const ActorVTable {
968        self.ptr.as_ptr()
969    }
970}
971
972/// SAFETY: validated vtables point at immutable process-lifetime storage.
973#[cfg(feature = "host")]
974unsafe impl Send for ValidatedActorVTable {}
975/// SAFETY: see `Send`.
976#[cfg(feature = "host")]
977unsafe impl Sync for ValidatedActorVTable {}
978
979/// Host-side pointer to a validated [`StrategyVTable`].
980#[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    /// Wraps a strategy vtable pointer that the caller already validated.
996    ///
997    /// # Safety
998    ///
999    /// `ptr` must be non-null, point at immutable process-lifetime storage,
1000    /// and contain every required [`StrategyVTable`] function slot.
1001    #[must_use]
1002    pub unsafe fn from_raw_unchecked(ptr: *const StrategyVTable) -> Self {
1003        Self::from_validated_ptr(ptr)
1004    }
1005
1006    /// Returns the raw vtable pointer for ABI calls.
1007    #[must_use]
1008    pub fn as_ptr(self) -> *const StrategyVTable {
1009        self.ptr.as_ptr()
1010    }
1011}
1012
1013/// SAFETY: validated vtables point at immutable process-lifetime storage.
1014#[cfg(feature = "host")]
1015unsafe impl Send for ValidatedStrategyVTable {}
1016/// SAFETY: see `Send`.
1017#[cfg(feature = "host")]
1018unsafe impl Sync for ValidatedStrategyVTable {}
1019
1020/// Host-side pointer to a validated [`ControllerVTable`].
1021#[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    /// Wraps a controller vtable pointer that the caller already validated.
1037    ///
1038    /// # Safety
1039    ///
1040    /// `ptr` must be non-null, point at immutable process-lifetime storage,
1041    /// and contain every required [`ControllerVTable`] function slot.
1042    #[must_use]
1043    pub unsafe fn from_raw_unchecked(ptr: *const ControllerVTable) -> Self {
1044        Self::from_validated_ptr(ptr)
1045    }
1046
1047    /// Returns the raw vtable pointer for ABI calls.
1048    #[must_use]
1049    pub fn as_ptr(self) -> *const ControllerVTable {
1050        self.ptr.as_ptr()
1051    }
1052}
1053
1054/// SAFETY: validated vtables point at immutable process-lifetime storage.
1055#[cfg(feature = "host")]
1056unsafe impl Send for ValidatedControllerVTable {}
1057/// SAFETY: see `Send`.
1058#[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        // SAFETY: generated test vtable lives for the process lifetime.
1256        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        // SAFETY: generated test vtable lives for the process lifetime.
1276        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        // SAFETY: generated test vtable lives for the process lifetime.
1296        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        // SAFETY: generated test vtable lives for the process lifetime.
1341        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        // SAFETY: generated test vtable lives for the process lifetime.
1386        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        // SAFETY: generated test vtable lives for the process lifetime.
1449        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        // SAFETY: generated test vtable lives for the process lifetime.
1512        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        // SAFETY: build id strings are baked into static crate storage.
1550        assert_eq!(
1551            unsafe { id.nautilus_plugin_version.as_str() },
1552            env!("CARGO_PKG_VERSION")
1553        );
1554        // SAFETY: see above.
1555        assert!(!unsafe { id.target_triple.as_str() }.is_empty());
1556        // SAFETY: see above.
1557        assert!(!unsafe { id.build_profile.as_str() }.is_empty());
1558        // SAFETY: see above.
1559        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}