Skip to main content

nautilus_plugin/bridge/
configured.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//! Safe wrappers for config-driven plug-in registration and adapter construction.
17
18#![allow(unsafe_code)]
19
20use nautilus_model::identifiers::ActorId;
21use nautilus_trading::strategy::StrategyConfig;
22
23use crate::{
24    bridge::{
25        PluginActorAdapter, PluginControllerAdapter, PluginStrategyAdapter, controller_host_vtable,
26        host_vtable, register_custom_data_from_manifest,
27    },
28    manifest::{
29        ValidatedActorRegistration, ValidatedActorVTable, ValidatedControllerRegistration,
30        ValidatedControllerVTable, ValidatedPluginManifest, ValidatedStrategyRegistration,
31        ValidatedStrategyVTable,
32    },
33};
34
35/// Config-resolved plug-in component entry.
36pub enum ConfiguredPluginEntry {
37    Actor(ConfiguredActorEntry),
38    Strategy(ConfiguredStrategyEntry),
39    Controller(ConfiguredControllerEntry),
40}
41
42/// Actor entry copied from a loaded manifest.
43pub struct ConfiguredActorEntry {
44    plugin_name: String,
45    type_name: String,
46    vtable: ValidatedActorVTable,
47}
48
49/// Strategy entry copied from a loaded manifest.
50pub struct ConfiguredStrategyEntry {
51    plugin_name: String,
52    type_name: String,
53    vtable: ValidatedStrategyVTable,
54}
55
56/// Controller entry copied from a loaded manifest.
57pub struct ConfiguredControllerEntry {
58    plugin_name: String,
59    type_name: String,
60    vtable: ValidatedControllerVTable,
61}
62
63impl ConfiguredActorEntry {
64    /// Creates a host-side adapter for this configured actor entry.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if the plug-in vtable rejects construction.
69    pub fn create_adapter(
70        &self,
71        actor_id: ActorId,
72        config_json: &str,
73    ) -> anyhow::Result<PluginActorAdapter> {
74        // SAFETY: entries come from a manifest owned by `PluginLoader`, and
75        // `host_vtable()` is process-lifetime static.
76        unsafe {
77            PluginActorAdapter::new(
78                actor_id,
79                self.plugin_name.clone(),
80                self.type_name.clone(),
81                self.vtable,
82                host_vtable(),
83                config_json,
84            )
85        }
86    }
87}
88
89impl ConfiguredStrategyEntry {
90    /// Creates a host-side adapter for this configured strategy entry.
91    ///
92    /// # Errors
93    ///
94    /// Returns an error if the plug-in vtable rejects construction.
95    pub fn create_adapter(
96        &self,
97        strategy_config: StrategyConfig,
98        config_json: &str,
99    ) -> anyhow::Result<PluginStrategyAdapter> {
100        // SAFETY: entries come from a manifest owned by `PluginLoader`, and
101        // `host_vtable()` is process-lifetime static.
102        unsafe {
103            PluginStrategyAdapter::new(
104                strategy_config,
105                self.plugin_name.clone(),
106                self.type_name.clone(),
107                self.vtable,
108                host_vtable(),
109                config_json,
110            )
111        }
112    }
113}
114
115impl ConfiguredControllerEntry {
116    /// Creates a host-side adapter for this configured controller entry.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if the plug-in vtable rejects construction.
121    pub fn create_adapter(&self, config_json: &str) -> anyhow::Result<PluginControllerAdapter> {
122        // SAFETY: entries come from a manifest owned by `PluginLoader`, and
123        // `controller_host_vtable()` is process-lifetime static.
124        unsafe {
125            PluginControllerAdapter::new(
126                self.plugin_name.clone(),
127                self.type_name.clone(),
128                self.vtable,
129                controller_host_vtable(),
130                config_json,
131            )
132        }
133    }
134}
135
136/// Registers every custom data type declared by a loaded manifest.
137///
138/// # Errors
139///
140/// Returns an error if the host-side custom-data registry rejects an entry.
141pub fn register_manifest_custom_data(
142    manifest: ValidatedPluginManifest<'_>,
143) -> anyhow::Result<usize> {
144    register_custom_data_from_manifest(manifest)
145}
146
147/// Resolves an actor, strategy, or controller entry from a loaded manifest by type name.
148///
149/// # Errors
150///
151/// Returns an error when the type is missing or ambiguous.
152pub fn configured_entry(
153    manifest: ValidatedPluginManifest<'_>,
154    path: &str,
155    type_name: &str,
156) -> anyhow::Result<ConfiguredPluginEntry> {
157    let plugin_name = manifest.plugin_name().to_string();
158    let actor_entry = find_actor_entry(manifest, type_name);
159    let strategy_entry = find_strategy_entry(manifest, type_name);
160    let controller_entry = find_controller_entry(manifest, type_name);
161
162    match (actor_entry, strategy_entry, controller_entry) {
163        (Some(entry), None, None) => Ok(ConfiguredPluginEntry::Actor(ConfiguredActorEntry {
164            plugin_name,
165            type_name: entry.type_name().to_string(),
166            vtable: entry.vtable(),
167        })),
168        (None, Some(entry), None) => Ok(ConfiguredPluginEntry::Strategy(ConfiguredStrategyEntry {
169            plugin_name,
170            type_name: entry.type_name().to_string(),
171            vtable: entry.vtable(),
172        })),
173        (None, None, Some(entry)) => Ok(ConfiguredPluginEntry::Controller(
174            ConfiguredControllerEntry {
175                plugin_name,
176                type_name: entry.type_name().to_string(),
177                vtable: entry.vtable(),
178            },
179        )),
180        (None, None, None) => {
181            anyhow::bail!(
182                "plug-in '{path}' does not expose actor, strategy, or controller type '{type_name}'"
183            )
184        }
185        (actor, strategy, controller) => {
186            let mut kinds = Vec::new();
187            if actor.is_some() {
188                kinds.push("actor");
189            }
190
191            if strategy.is_some() {
192                kinds.push("strategy");
193            }
194
195            if controller.is_some() {
196                kinds.push("controller");
197            }
198
199            anyhow::bail!(
200                "plug-in '{path}' exposes type '{type_name}' as multiple component kinds ({})",
201                kinds.join(", ")
202            )
203        }
204    }
205}
206
207fn find_actor_entry(
208    manifest: ValidatedPluginManifest<'_>,
209    type_name: &str,
210) -> Option<ValidatedActorRegistration> {
211    manifest
212        .actors()
213        .find(|entry| entry.type_name() == type_name)
214}
215
216fn find_strategy_entry(
217    manifest: ValidatedPluginManifest<'_>,
218    type_name: &str,
219) -> Option<ValidatedStrategyRegistration> {
220    manifest
221        .strategies()
222        .find(|entry| entry.type_name() == type_name)
223}
224
225fn find_controller_entry(
226    manifest: ValidatedPluginManifest<'_>,
227    type_name: &str,
228) -> Option<ValidatedControllerRegistration> {
229    manifest
230        .controllers()
231        .find(|entry| entry.type_name() == type_name)
232}
233
234#[cfg(test)]
235mod tests {
236    use std::sync::LazyLock;
237
238    use rstest::rstest;
239
240    use super::*;
241    use crate::{
242        NAUTILUS_PLUGIN_ABI_VERSION,
243        boundary::{BorrowedStr, Slice},
244        host::{HostContext, HostVTable},
245        manifest::{ActorRegistration, PluginBuildId, PluginManifest, StrategyRegistration},
246        surfaces::{
247            actor::{PluginActor, actor_vtable},
248            controller::{PluginController, controller_vtable},
249            strategy::{PluginStrategy, strategy_vtable},
250        },
251    };
252
253    struct ExampleActor;
254
255    impl PluginActor for ExampleActor {
256        const TYPE_NAME: &'static str = "ExampleActor";
257
258        fn new(_host: *const HostVTable, _ctx: *const HostContext, _config_json: &str) -> Self {
259            Self
260        }
261    }
262
263    struct ExampleStrategy;
264
265    impl PluginStrategy for ExampleStrategy {
266        const TYPE_NAME: &'static str = "ExampleStrategy";
267
268        fn new(_host: *const HostVTable, _ctx: *const HostContext, _config_json: &str) -> Self {
269            Self
270        }
271    }
272
273    struct ExampleController;
274
275    impl PluginController for ExampleController {
276        const TYPE_NAME: &'static str = "ExampleController";
277
278        fn new(
279            _host: *const crate::host::ControllerHostVTable,
280            _ctx: *const crate::host::ControllerHostContext,
281            _config_json: &str,
282        ) -> Self {
283            Self
284        }
285    }
286
287    static ACTOR_REGISTRATIONS: LazyLock<[ActorRegistration; 1]> = LazyLock::new(|| {
288        [ActorRegistration {
289            type_name: BorrowedStr::from_str("ExampleActor"),
290            vtable: actor_vtable::<ExampleActor>(),
291        }]
292    });
293    static STRATEGY_REGISTRATIONS: LazyLock<[StrategyRegistration; 1]> = LazyLock::new(|| {
294        [StrategyRegistration {
295            type_name: BorrowedStr::from_str("ExampleStrategy"),
296            vtable: strategy_vtable::<ExampleStrategy>(),
297        }]
298    });
299    static CONTROLLER_REGISTRATIONS: LazyLock<[crate::manifest::ControllerRegistration; 1]> =
300        LazyLock::new(|| {
301            [crate::manifest::ControllerRegistration {
302                type_name: BorrowedStr::from_str("ExampleController"),
303                vtable: controller_vtable::<ExampleController>(),
304            }]
305        });
306    static AMBIGUOUS_ACTOR_REGISTRATIONS: LazyLock<[ActorRegistration; 1]> = LazyLock::new(|| {
307        [ActorRegistration {
308            type_name: BorrowedStr::from_str("DuplicateType"),
309            vtable: actor_vtable::<ExampleActor>(),
310        }]
311    });
312    static AMBIGUOUS_STRATEGY_REGISTRATIONS: LazyLock<[StrategyRegistration; 1]> =
313        LazyLock::new(|| {
314            [StrategyRegistration {
315                type_name: BorrowedStr::from_str("DuplicateType"),
316                vtable: strategy_vtable::<ExampleStrategy>(),
317            }]
318        });
319    static AMBIGUOUS_CONTROLLER_REGISTRATIONS: LazyLock<
320        [crate::manifest::ControllerRegistration; 1],
321    > = LazyLock::new(|| {
322        [crate::manifest::ControllerRegistration {
323            type_name: BorrowedStr::from_str("DuplicateType"),
324            vtable: controller_vtable::<ExampleController>(),
325        }]
326    });
327
328    fn manifest(
329        actors: Slice<'static, ActorRegistration>,
330        strategies: Slice<'static, StrategyRegistration>,
331        controllers: Slice<'static, crate::manifest::ControllerRegistration>,
332    ) -> PluginManifest {
333        PluginManifest {
334            abi_version: NAUTILUS_PLUGIN_ABI_VERSION,
335            plugin_name: BorrowedStr::from_str("test-plugin"),
336            plugin_vendor: BorrowedStr::from_str("nautech"),
337            plugin_version: BorrowedStr::from_str("0.0.0"),
338            build_id: PluginBuildId::current(),
339            custom_data: Slice::empty(),
340            actors,
341            strategies,
342            controllers,
343        }
344    }
345
346    #[rstest]
347    fn configured_entry_resolves_actor_by_type_name() {
348        let manifest = manifest(
349            Slice::from_slice(&*ACTOR_REGISTRATIONS),
350            Slice::from_slice(&*STRATEGY_REGISTRATIONS),
351            Slice::from_slice(&*CONTROLLER_REGISTRATIONS),
352        );
353        let manifest = ValidatedPluginManifest::new(&manifest)
354            .expect("configured actor lookup uses a loader-valid manifest");
355
356        let entry = configured_entry(manifest, "./libexample.so", "ExampleActor").unwrap();
357
358        let ConfiguredPluginEntry::Actor(entry) = entry else {
359            panic!("expected actor entry");
360        };
361        assert_eq!(entry.plugin_name, "test-plugin");
362        assert_eq!(entry.type_name, "ExampleActor");
363        assert_eq!(entry.vtable.as_ptr(), ACTOR_REGISTRATIONS[0].vtable);
364    }
365
366    #[rstest]
367    fn configured_entry_resolves_strategy_by_type_name() {
368        let manifest = manifest(
369            Slice::from_slice(&*ACTOR_REGISTRATIONS),
370            Slice::from_slice(&*STRATEGY_REGISTRATIONS),
371            Slice::from_slice(&*CONTROLLER_REGISTRATIONS),
372        );
373        let manifest = ValidatedPluginManifest::new(&manifest)
374            .expect("configured strategy lookup uses a loader-valid manifest");
375
376        let entry = configured_entry(manifest, "./libexample.so", "ExampleStrategy").unwrap();
377
378        let ConfiguredPluginEntry::Strategy(entry) = entry else {
379            panic!("expected strategy entry");
380        };
381        assert_eq!(entry.plugin_name, "test-plugin");
382        assert_eq!(entry.type_name, "ExampleStrategy");
383        assert_eq!(entry.vtable.as_ptr(), STRATEGY_REGISTRATIONS[0].vtable);
384    }
385
386    #[rstest]
387    fn configured_entry_resolves_controller_by_type_name() {
388        let manifest = manifest(
389            Slice::from_slice(&*ACTOR_REGISTRATIONS),
390            Slice::from_slice(&*STRATEGY_REGISTRATIONS),
391            Slice::from_slice(&*CONTROLLER_REGISTRATIONS),
392        );
393        let manifest = ValidatedPluginManifest::new(&manifest)
394            .expect("configured controller lookup uses a loader-valid manifest");
395
396        let entry = configured_entry(manifest, "./libexample.so", "ExampleController").unwrap();
397
398        let ConfiguredPluginEntry::Controller(entry) = entry else {
399            panic!("expected controller entry");
400        };
401        assert_eq!(entry.plugin_name, "test-plugin");
402        assert_eq!(entry.type_name, "ExampleController");
403        assert_eq!(entry.vtable.as_ptr(), CONTROLLER_REGISTRATIONS[0].vtable);
404    }
405
406    #[rstest]
407    fn configured_entry_rejects_missing_type_name() {
408        let manifest = manifest(
409            Slice::from_slice(&*ACTOR_REGISTRATIONS),
410            Slice::from_slice(&*STRATEGY_REGISTRATIONS),
411            Slice::from_slice(&*CONTROLLER_REGISTRATIONS),
412        );
413        let manifest = ValidatedPluginManifest::new(&manifest)
414            .expect("missing configured type test uses a loader-valid manifest");
415
416        let error = match configured_entry(manifest, "./libexample.so", "MissingType") {
417            Ok(_) => panic!("configured entry should reject missing type"),
418            Err(e) => e.to_string(),
419        };
420
421        assert!(error.contains("does not expose actor, strategy, or controller type"));
422        assert!(error.contains("MissingType"));
423    }
424
425    #[rstest]
426    fn validated_manifest_rejects_ambiguous_type_name() {
427        let manifest = manifest(
428            Slice::from_slice(&*AMBIGUOUS_ACTOR_REGISTRATIONS),
429            Slice::from_slice(&*AMBIGUOUS_STRATEGY_REGISTRATIONS),
430            Slice::empty(),
431        );
432        let validation_error = ValidatedPluginManifest::new(&manifest)
433            .expect_err("loader rejects ambiguous manifest type names");
434        assert!(
435            validation_error
436                .to_string()
437                .contains("type name 'DuplicateType' appears in both actors[0] and strategies[0]")
438        );
439    }
440
441    #[rstest]
442    fn validated_manifest_rejects_controller_ambiguous_type_name() {
443        let manifest = manifest(
444            Slice::from_slice(&*AMBIGUOUS_ACTOR_REGISTRATIONS),
445            Slice::empty(),
446            Slice::from_slice(&*AMBIGUOUS_CONTROLLER_REGISTRATIONS),
447        );
448        let validation_error = ValidatedPluginManifest::new(&manifest)
449            .expect_err("loader rejects ambiguous controller manifest type names");
450        assert!(
451            validation_error
452                .to_string()
453                .contains("type name 'DuplicateType' appears in both actors[0] and controllers[0]")
454        );
455    }
456
457    #[rstest]
458    fn validated_manifest_rejects_strategy_controller_ambiguous_type_name() {
459        let manifest = manifest(
460            Slice::empty(),
461            Slice::from_slice(&*AMBIGUOUS_STRATEGY_REGISTRATIONS),
462            Slice::from_slice(&*AMBIGUOUS_CONTROLLER_REGISTRATIONS),
463        );
464        let validation_error = ValidatedPluginManifest::new(&manifest)
465            .expect_err("loader rejects ambiguous strategy/controller manifest type names");
466        assert!(validation_error.to_string().contains(
467            "type name 'DuplicateType' appears in both strategies[0] and controllers[0]"
468        ));
469    }
470}