Skip to main content

lenso_native_adapter/
lib.rs

1//! Native Rust Execution Adapter for statically linked Plugin packages.
2
3mod managed_tasks;
4
5use std::{collections::BTreeMap, rc::Rc};
6
7#[doc(hidden)]
8pub use inventory as __inventory;
9use lenso_app_plan::{
10    ExecutionClassId, ResolvedAppPlan,
11    authoring::{HostCatalog, HostDefaultPlugin, HostPluginRelease, HostSlot, PluginDescriptor},
12};
13use lenso_kernel::{ActivateContext, DeactivateContext, PrepareContext};
14pub use lenso_kernel::{CancellationToken, RuntimeFailure};
15pub use lenso_native_adapter_macros::{PluginConfig, plugin, provides};
16pub use managed_tasks::{ManagedTasks, ManagedTasksError};
17
18/// Optional convention-based lifecycle hooks for a struct-level Plugin.
19///
20/// Add `#[plugin(lifecycle)]`, implement this trait, and override only the
21/// phases that own real work. The generated Adapter lifecycle still connects
22/// declared Capability ports before `activate`.
23#[allow(async_fn_in_trait)]
24pub trait Lifecycle: Clone + 'static {
25    async fn prepare(&self, _context: PrepareContext) -> Result<(), RuntimeFailure> {
26        Ok(())
27    }
28
29    async fn activate(&self, _context: ActivateContext) -> Result<(), RuntimeFailure> {
30        Ok(())
31    }
32
33    async fn deactivate(&self, _context: DeactivateContext) -> Result<(), RuntimeFailure> {
34        Ok(())
35    }
36}
37
38/// Implementation details referenced by generated Plugin glue.
39#[doc(hidden)]
40pub mod __private {
41    pub use crate::{
42        __inventory, Lifecycle, LinkedNativePluginFactory, NativePluginFactory,
43        NativePluginFactoryContext, NativePluginInstance, RuntimeFailure,
44    };
45    pub use futures;
46    pub use futures::future::LocalBoxFuture;
47    pub use lenso_kernel::{
48        ActivateContext, DeactivateContext, InvocationContext, NativeEventEndpoint,
49        NativeRequestEndpoint, NativeRequestFuture, NativeStreamEndpoint, NativeStreamSession,
50        PluginFuture, PluginLifecycle, PrepareContext,
51    };
52    pub use serde_json;
53}
54
55use lenso_kernel::{
56    NativeEndpointSet, NativeEventEndpoint, NativeExecutionAdapter, NativeRequestEndpoint,
57    NativeStreamEndpoint, NoopPluginLifecycle, PluginLifecycle, PreparedBinding,
58    PreparedEventBinding, PreparedNativeApp, PreparedNativePlugin, PreparedStreamBinding,
59};
60
61/// One native Plugin factory contributed to the Host's link-time catalog.
62#[derive(Clone, Copy, Debug)]
63#[doc(hidden)]
64pub struct LinkedNativePluginFactory {
65    constructor: fn() -> Rc<dyn NativePluginFactory>,
66    descriptor: &'static str,
67}
68
69impl LinkedNativePluginFactory {
70    /// Creates a link-time catalog record. Intended for generated authoring glue.
71    #[doc(hidden)]
72    pub const fn new(
73        constructor: fn() -> Rc<dyn NativePluginFactory>,
74        descriptor: &'static str,
75    ) -> Self {
76        Self {
77            constructor,
78            descriptor,
79        }
80    }
81}
82
83inventory::collect!(LinkedNativePluginFactory);
84
85/// Endpoints created for one statically linked Plugin Instance generation.
86#[derive(Debug)]
87pub struct NativePluginInstance {
88    endpoints: NativeEndpointSet,
89    lifecycle: Rc<dyn PluginLifecycle>,
90}
91
92impl NativePluginInstance {
93    /// Creates a generation from its exact declared endpoint set.
94    pub fn new(endpoints: Vec<Rc<dyn NativeRequestEndpoint>>) -> Self {
95        Self::with_lifecycle(endpoints, NoopPluginLifecycle)
96    }
97
98    /// Creates a generation with its exact endpoints and lifecycle Interface.
99    pub fn with_lifecycle(
100        endpoints: Vec<Rc<dyn NativeRequestEndpoint>>,
101        lifecycle: impl PluginLifecycle,
102    ) -> Self {
103        Self {
104            endpoints: NativeEndpointSet::new(endpoints, Vec::new(), Vec::new()),
105            lifecycle: Rc::new(lifecycle),
106        }
107    }
108
109    /// Creates a generation with request and bidirectional stream endpoints.
110    pub fn with_endpoints(
111        endpoints: Vec<Rc<dyn NativeRequestEndpoint>>,
112        stream_endpoints: Vec<Rc<dyn NativeStreamEndpoint>>,
113        lifecycle: impl PluginLifecycle,
114    ) -> Self {
115        Self {
116            endpoints: NativeEndpointSet::new(endpoints, stream_endpoints, Vec::new()),
117            lifecycle: Rc::new(lifecycle),
118        }
119    }
120
121    /// Creates a generation containing only bidirectional stream endpoints.
122    pub fn with_stream_endpoints(
123        stream_endpoints: Vec<Rc<dyn NativeStreamEndpoint>>,
124        lifecycle: impl PluginLifecycle,
125    ) -> Self {
126        Self::with_endpoints(Vec::new(), stream_endpoints, lifecycle)
127    }
128
129    /// Creates a generation containing only ephemeral Event endpoints.
130    pub fn with_event_endpoints(
131        event_endpoints: Vec<Rc<dyn NativeEventEndpoint>>,
132        lifecycle: impl PluginLifecycle,
133    ) -> Self {
134        Self {
135            endpoints: NativeEndpointSet::new(Vec::new(), Vec::new(), event_endpoints),
136            lifecycle: Rc::new(lifecycle),
137        }
138    }
139
140    /// Creates a generation with request, stream, and ephemeral Event endpoints.
141    pub fn with_all_endpoints(
142        endpoints: Vec<Rc<dyn NativeRequestEndpoint>>,
143        stream_endpoints: Vec<Rc<dyn NativeStreamEndpoint>>,
144        event_endpoints: Vec<Rc<dyn NativeEventEndpoint>>,
145        lifecycle: impl PluginLifecycle,
146    ) -> Self {
147        Self {
148            endpoints: NativeEndpointSet::new(endpoints, stream_endpoints, event_endpoints),
149            lifecycle: Rc::new(lifecycle),
150        }
151    }
152
153    /// Returns the lifecycle Interface for this generation.
154    pub fn lifecycle(&self) -> Rc<dyn PluginLifecycle> {
155        self.lifecycle.clone()
156    }
157
158    /// Returns the exact endpoint set created for this generation.
159    pub fn endpoints(&self) -> &[Rc<dyn NativeRequestEndpoint>] {
160        self.endpoints.request()
161    }
162
163    /// Returns the exact bidirectional stream endpoint set created for this generation.
164    pub fn stream_endpoints(&self) -> &[Rc<dyn NativeStreamEndpoint>] {
165        self.endpoints.stream()
166    }
167
168    /// Returns the exact ephemeral Event endpoint set created for this Instance.
169    pub fn event_endpoints(&self) -> &[Rc<dyn NativeEventEndpoint>] {
170        self.endpoints.event()
171    }
172}
173
174impl Default for NativePluginInstance {
175    fn default() -> Self {
176        Self::new(Vec::new())
177    }
178}
179
180/// Adapter-specific factory for a statically linked native Rust Plugin.
181pub trait NativePluginFactory: std::fmt::Debug + 'static {
182    /// Package identity selected by the Resolved App Plan.
183    fn package_id(&self) -> &'static str;
184    /// Exact statically linked Cargo package version.
185    fn package_version(&self) -> &'static str {
186        ""
187    }
188    /// Immutable factory identity advertised by the exact Host Build Manifest.
189    ///
190    /// Plugin-resolved Plans carry this value as their package revision. The
191    /// default keeps ordinary statically linked factories unique by package and
192    /// version while allowing a factory to override the identity when its build
193    /// authority is more specific than a Cargo package version.
194    fn factory_identity(&self) -> String {
195        let version = self.package_version();
196        if version.is_empty() {
197            self.package_id().to_owned()
198        } else {
199            format!("{}@{version}", self.package_id())
200        }
201    }
202    /// Creates a fresh Plugin Instance generation.
203    fn instantiate(
204        &self,
205        context: NativePluginFactoryContext<'_>,
206    ) -> Result<NativePluginInstance, RuntimeFailure>;
207}
208
209/// Immutable Plan input supplied when a native factory creates one generation.
210#[derive(Clone, Copy, Debug)]
211pub struct NativePluginFactoryContext<'a> {
212    instance_key: &'a str,
213    entrypoint: &'a str,
214    configuration: &'a str,
215}
216
217impl<'a> NativePluginFactoryContext<'a> {
218    fn from_plan(instance: &'a lenso_app_plan::PluginInstancePlan) -> Self {
219        Self {
220            instance_key: instance.instance_key(),
221            entrypoint: instance.entrypoint(),
222            configuration: instance.configuration(),
223        }
224    }
225
226    /// Returns the App-local Plugin Instance key.
227    pub const fn instance_key(self) -> &'a str {
228        self.instance_key
229    }
230
231    /// Returns the exact package entrypoint selected before boot.
232    pub const fn entrypoint(self) -> &'a str {
233        self.entrypoint
234    }
235
236    /// Returns opaque Plugin-owned configuration selected before boot.
237    pub const fn configuration(self) -> &'a str {
238        self.configuration
239    }
240}
241
242/// Statically linked native Plugin factories available to an App binary.
243#[derive(Debug, Default)]
244pub struct NativePluginRegistry {
245    factories: Vec<Rc<dyn NativePluginFactory>>,
246}
247
248type NativeInstances = BTreeMap<String, NativePluginInstance>;
249type PreparedGenerations = BTreeMap<String, PreparedNativePlugin>;
250type NativeBindings = (
251    Vec<PreparedBinding>,
252    Vec<PreparedStreamBinding>,
253    Vec<PreparedEventBinding>,
254);
255
256fn factory_matches(
257    factory: &dyn NativePluginFactory,
258    expected: &lenso_app_plan::PluginInstancePlan,
259) -> bool {
260    factory.package_id() == expected.package_id()
261        && (expected.package_revision().is_empty()
262            || factory.package_version() == expected.package_revision()
263            || factory.factory_identity() == expected.package_revision())
264}
265
266impl NativePluginRegistry {
267    /// Creates an empty linked-factory registry.
268    pub fn new() -> Self {
269        Self::default()
270    }
271
272    /// Adds every Plugin factory contributed to this Host at link time.
273    ///
274    /// This catalog describes code available in the binary. The Resolved App
275    /// Plan remains the sole authority that selects and binds Plugin Instances.
276    #[must_use]
277    pub fn with_linked_factories(mut self) -> Self {
278        self.factories.extend(
279            inventory::iter::<LinkedNativePluginFactory>
280                .into_iter()
281                .map(|linked| (linked.constructor)()),
282        );
283        self.factories
284            .sort_by_key(|factory| factory.factory_identity());
285        self
286    }
287
288    /// Returns the exact native factories available to this registry.
289    pub fn factories(&self) -> impl Iterator<Item = &dyn NativePluginFactory> {
290        self.factories.iter().map(std::convert::AsRef::as_ref)
291    }
292
293    /// Builds the immutable Host Catalog declared by this binary and Host policy.
294    pub fn host_catalog(
295        slots: impl IntoIterator<Item = HostSlot>,
296        defaults: impl IntoIterator<Item = HostDefaultPlugin>,
297    ) -> Result<HostCatalog, RuntimeFailure> {
298        let plugins = inventory::iter::<LinkedNativePluginFactory>
299            .into_iter()
300            .map(|linked| {
301                serde_json::from_str::<PluginDescriptor>(linked.descriptor)
302                    .map(HostPluginRelease::new)
303                    .map_err(|error| RuntimeFailure::InvalidResolvedPlan {
304                        detail: format!("invalid linked Plugin Descriptor: {error}"),
305                    })
306            })
307            .collect::<Result<Vec<_>, _>>()?;
308        Ok(HostCatalog::new(slots, plugins, defaults))
309    }
310    /// Adds one statically linked factory.
311    #[must_use]
312    pub fn with_factory(mut self, factory: impl NativePluginFactory) -> Self {
313        self.factories.push(Rc::new(factory));
314        self
315    }
316
317    fn prepare_instances(
318        &self,
319        plan: &ResolvedAppPlan,
320    ) -> Result<(NativeInstances, PreparedGenerations), RuntimeFailure> {
321        let mut instances = BTreeMap::new();
322        let mut generations = BTreeMap::new();
323        for expected in plan
324            .plugin_instances()
325            .iter()
326            .filter(|instance| instance.execution_class() == &ExecutionClassId::native_rust())
327        {
328            let matching_factories: Vec<_> = self
329                .factories
330                .iter()
331                .filter(|factory| factory_matches(factory.as_ref(), expected))
332                .collect();
333            let factory = match matching_factories.as_slice() {
334                [] => {
335                    return Err(RuntimeFailure::MissingPluginFactory {
336                        instance: expected.instance_key().to_owned(),
337                        package_id: expected.package_id().to_owned(),
338                    });
339                }
340                [factory] => *factory,
341                _ => {
342                    return invalid(format!(
343                        "multiple statically linked factories declare package `{}`",
344                        expected.package_id()
345                    ));
346                }
347            };
348            let generation =
349                factory.instantiate(NativePluginFactoryContext::from_plan(expected))?;
350            generations.insert(
351                expected.instance_key().to_owned(),
352                PreparedNativePlugin::with_endpoint_set_lifecycle(
353                    generation.endpoints.clone(),
354                    generation.lifecycle(),
355                ),
356            );
357            if instances
358                .insert(expected.instance_key().to_owned(), generation)
359                .is_some()
360            {
361                return invalid(format!(
362                    "duplicate Plugin Instance `{}`",
363                    expected.instance_key()
364                ));
365            }
366        }
367        Ok((instances, generations))
368    }
369}
370
371impl NativeExecutionAdapter for NativePluginRegistry {
372    fn prepare(&self, plan: &ResolvedAppPlan) -> Result<PreparedNativeApp, RuntimeFailure> {
373        plan.validate()
374            .map_err(|error| RuntimeFailure::InvalidResolvedPlan {
375                detail: error.to_string(),
376            })?;
377
378        let (instances, generations) = self.prepare_instances(plan)?;
379        let (bindings, stream_bindings, event_bindings) = prepare_bindings(plan, &instances)?;
380        Ok(PreparedNativeApp::new(bindings, generations)
381            .with_stream_bindings(stream_bindings)
382            .with_event_bindings(event_bindings))
383    }
384
385    fn recreate(
386        &self,
387        plan: &ResolvedAppPlan,
388        instance_key: &str,
389    ) -> Result<PreparedNativePlugin, RuntimeFailure> {
390        let expected = plan
391            .plugin_instances()
392            .iter()
393            .find(|instance| instance.instance_key() == instance_key)
394            .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
395                detail: format!("unknown Plugin Instance `{instance_key}`"),
396            })?;
397        let matching_factories: Vec<_> = self
398            .factories
399            .iter()
400            .filter(|factory| factory_matches(factory.as_ref(), expected))
401            .collect();
402        let factory = match matching_factories.as_slice() {
403            [] => {
404                return Err(RuntimeFailure::MissingPluginFactory {
405                    instance: expected.instance_key().to_owned(),
406                    package_id: expected.package_id().to_owned(),
407                });
408            }
409            [factory] => *factory,
410            _ => {
411                return invalid(format!(
412                    "multiple statically linked factories declare package `{}`",
413                    expected.package_id()
414                ));
415            }
416        };
417        let generation = factory.instantiate(NativePluginFactoryContext::from_plan(expected))?;
418        Ok(PreparedNativePlugin::with_endpoint_set_lifecycle(
419            generation.endpoints.clone(),
420            generation.lifecycle(),
421        ))
422    }
423}
424
425fn prepare_bindings(
426    plan: &ResolvedAppPlan,
427    instances: &NativeInstances,
428) -> Result<NativeBindings, RuntimeFailure> {
429    let mut bindings = Vec::new();
430    let mut stream_bindings = Vec::new();
431    let mut event_bindings = Vec::new();
432    for binding in plan.capability_bindings() {
433        if !instances.contains_key(binding.provider_instance()) {
434            continue;
435        }
436        let provider = plan
437            .plugin_instance(binding.provider_instance())
438            .expect("validated binding provider should exist");
439        let descriptor = provider
440            .provided_capabilities()
441            .iter()
442            .find(|descriptor| descriptor.capability_id() == binding.capability_id())
443            .expect("validated binding descriptor should exist");
444        if !descriptor.request_operations().is_empty() {
445            let endpoint = instances
446                .get(binding.provider_instance())
447                .and_then(|instance| {
448                    instance.endpoints.request().iter().find(|endpoint| {
449                        endpoint.capability_id() == binding.capability_id()
450                            && endpoint.descriptor_version() == binding.descriptor_version()
451                    })
452                })
453                .cloned()
454                .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
455                    detail: format!(
456                        "Capability `{}` version `{}` has no request endpoint on provider `{}`",
457                        binding.capability_id(),
458                        binding.descriptor_version(),
459                        binding.provider_instance()
460                    ),
461                })?;
462            bindings.push(PreparedBinding::new(
463                binding.consumer_instance(),
464                binding.provider_instance(),
465                endpoint,
466            ));
467        }
468        if !descriptor.stream_operations().is_empty() {
469            let endpoint = instances
470                .get(binding.provider_instance())
471                .and_then(|instance| {
472                    instance.endpoints.stream().iter().find(|endpoint| {
473                        endpoint.capability_id() == binding.capability_id()
474                            && endpoint.descriptor_version() == binding.descriptor_version()
475                    })
476                })
477                .cloned()
478                .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
479                    detail: format!(
480                        "Capability `{}` version `{}` has no stream endpoint on provider `{}`",
481                        binding.capability_id(),
482                        binding.descriptor_version(),
483                        binding.provider_instance()
484                    ),
485                })?;
486            stream_bindings.push(PreparedStreamBinding::new(
487                binding.consumer_instance(),
488                binding.provider_instance(),
489                endpoint,
490            ));
491        }
492        if !descriptor.event_operations().is_empty() {
493            let endpoint = instances
494                .get(binding.provider_instance())
495                .and_then(|instance| {
496                    instance.endpoints.event().iter().find(|endpoint| {
497                        endpoint.capability_id() == binding.capability_id()
498                            && endpoint.descriptor_version() == binding.descriptor_version()
499                    })
500                })
501                .cloned()
502                .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
503                    detail: format!(
504                        "Capability `{}` version `{}` has no Event endpoint on provider `{}`",
505                        binding.capability_id(),
506                        binding.descriptor_version(),
507                        binding.provider_instance()
508                    ),
509                })?;
510            event_bindings.push(PreparedEventBinding::new(
511                binding.consumer_instance(),
512                binding.provider_instance(),
513                endpoint,
514            ));
515        }
516    }
517    Ok((bindings, stream_bindings, event_bindings))
518}
519
520fn invalid<T>(detail: String) -> Result<T, RuntimeFailure> {
521    Err(RuntimeFailure::InvalidResolvedPlan { detail })
522}