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