Skip to main content

lenso_native_adapter/
lib.rs

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