1#![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
35pub enum ConfiguredPluginEntry {
37 Actor(ConfiguredActorEntry),
38 Strategy(ConfiguredStrategyEntry),
39 Controller(ConfiguredControllerEntry),
40}
41
42pub struct ConfiguredActorEntry {
44 plugin_name: String,
45 type_name: String,
46 vtable: ValidatedActorVTable,
47}
48
49pub struct ConfiguredStrategyEntry {
51 plugin_name: String,
52 type_name: String,
53 vtable: ValidatedStrategyVTable,
54}
55
56pub struct ConfiguredControllerEntry {
58 plugin_name: String,
59 type_name: String,
60 vtable: ValidatedControllerVTable,
61}
62
63impl ConfiguredActorEntry {
64 pub fn create_adapter(
70 &self,
71 actor_id: ActorId,
72 config_json: &str,
73 ) -> anyhow::Result<PluginActorAdapter> {
74 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 pub fn create_adapter(
96 &self,
97 strategy_config: StrategyConfig,
98 config_json: &str,
99 ) -> anyhow::Result<PluginStrategyAdapter> {
100 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 pub fn create_adapter(&self, config_json: &str) -> anyhow::Result<PluginControllerAdapter> {
122 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
136pub fn register_manifest_custom_data(
142 manifest: ValidatedPluginManifest<'_>,
143) -> anyhow::Result<usize> {
144 register_custom_data_from_manifest(manifest)
145}
146
147pub 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}