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 mut factories = inventory::iter::<LinkedNativePluginFactory>
120        .into_iter()
121        .copied()
122        .collect::<Vec<_>>();
123    factories.extend(
124        explicitly_linked_factories()
125            .lock()
126            .unwrap_or_else(std::sync::PoisonError::into_inner)
127            .iter()
128            .copied(),
129    );
130    factories
131}
132
133/// Endpoints created for one statically linked Plugin Instance generation.
134#[derive(Debug)]
135pub struct NativePluginInstance {
136    endpoints: NativeEndpointSet,
137    lifecycle: Rc<dyn PluginLifecycle>,
138}
139
140impl NativePluginInstance {
141    /// Creates a generation from its exact declared endpoint set.
142    pub fn new(endpoints: Vec<Rc<dyn NativeRequestEndpoint>>) -> Self {
143        Self::with_lifecycle(endpoints, NoopPluginLifecycle)
144    }
145
146    /// Creates a generation with its exact endpoints and lifecycle Interface.
147    pub fn with_lifecycle(
148        endpoints: Vec<Rc<dyn NativeRequestEndpoint>>,
149        lifecycle: impl PluginLifecycle,
150    ) -> Self {
151        Self {
152            endpoints: NativeEndpointSet::new(endpoints, Vec::new(), Vec::new()),
153            lifecycle: Rc::new(lifecycle),
154        }
155    }
156
157    /// Creates a generation with request and bidirectional stream endpoints.
158    pub fn with_endpoints(
159        endpoints: Vec<Rc<dyn NativeRequestEndpoint>>,
160        stream_endpoints: Vec<Rc<dyn NativeStreamEndpoint>>,
161        lifecycle: impl PluginLifecycle,
162    ) -> Self {
163        Self {
164            endpoints: NativeEndpointSet::new(endpoints, stream_endpoints, Vec::new()),
165            lifecycle: Rc::new(lifecycle),
166        }
167    }
168
169    /// Creates a generation containing only bidirectional stream endpoints.
170    pub fn with_stream_endpoints(
171        stream_endpoints: Vec<Rc<dyn NativeStreamEndpoint>>,
172        lifecycle: impl PluginLifecycle,
173    ) -> Self {
174        Self::with_endpoints(Vec::new(), stream_endpoints, lifecycle)
175    }
176
177    /// Creates a generation containing only ephemeral Event endpoints.
178    pub fn with_event_endpoints(
179        event_endpoints: Vec<Rc<dyn NativeEventEndpoint>>,
180        lifecycle: impl PluginLifecycle,
181    ) -> Self {
182        Self {
183            endpoints: NativeEndpointSet::new(Vec::new(), Vec::new(), event_endpoints),
184            lifecycle: Rc::new(lifecycle),
185        }
186    }
187
188    /// Creates a generation with request, stream, and ephemeral Event endpoints.
189    pub fn with_all_endpoints(
190        endpoints: Vec<Rc<dyn NativeRequestEndpoint>>,
191        stream_endpoints: Vec<Rc<dyn NativeStreamEndpoint>>,
192        event_endpoints: Vec<Rc<dyn NativeEventEndpoint>>,
193        lifecycle: impl PluginLifecycle,
194    ) -> Self {
195        Self {
196            endpoints: NativeEndpointSet::new(endpoints, stream_endpoints, event_endpoints),
197            lifecycle: Rc::new(lifecycle),
198        }
199    }
200
201    /// Returns the lifecycle Interface for this generation.
202    pub fn lifecycle(&self) -> Rc<dyn PluginLifecycle> {
203        self.lifecycle.clone()
204    }
205
206    /// Returns the exact endpoint set created for this generation.
207    pub fn endpoints(&self) -> &[Rc<dyn NativeRequestEndpoint>] {
208        self.endpoints.request()
209    }
210
211    /// Returns the exact bidirectional stream endpoint set created for this generation.
212    pub fn stream_endpoints(&self) -> &[Rc<dyn NativeStreamEndpoint>] {
213        self.endpoints.stream()
214    }
215
216    /// Returns the exact ephemeral Event endpoint set created for this Instance.
217    pub fn event_endpoints(&self) -> &[Rc<dyn NativeEventEndpoint>] {
218        self.endpoints.event()
219    }
220}
221
222impl Default for NativePluginInstance {
223    fn default() -> Self {
224        Self::new(Vec::new())
225    }
226}
227
228/// Adapter-specific factory for a statically linked native Rust Plugin.
229pub trait NativePluginFactory: std::fmt::Debug + 'static {
230    /// Package identity selected by the Resolved App Plan.
231    fn package_id(&self) -> &'static str;
232    /// Exact statically linked Cargo package version.
233    fn package_version(&self) -> &'static str {
234        ""
235    }
236    /// Exact authoring/runtime protocol implemented by this factory.
237    fn runtime_profile(&self) -> &'static str {
238        "lenso.native-authoring@1"
239    }
240    /// Immutable factory identity advertised by the exact Host Build Manifest.
241    ///
242    /// Plugin-resolved Plans carry this value as their package revision. The
243    /// default keeps ordinary statically linked factories unique by package and
244    /// version while allowing a factory to override the identity when its build
245    /// authority is more specific than a Cargo package version.
246    fn factory_identity(&self) -> String {
247        let version = self.package_version();
248        if version.is_empty() {
249            self.package_id().to_owned()
250        } else {
251            format!("{}@{version}", self.package_id())
252        }
253    }
254    /// Creates a fresh Plugin Instance generation.
255    fn instantiate(
256        &self,
257        context: NativePluginFactoryContext<'_>,
258    ) -> Result<NativePluginInstance, RuntimeFailure>;
259}
260
261/// Immutable Plan input supplied when a native factory creates one generation.
262#[derive(Clone, Copy, Debug)]
263pub struct NativePluginFactoryContext<'a> {
264    instance_key: &'a str,
265    entrypoint: &'a str,
266    configuration: &'a str,
267    resources: &'a InstanceResources,
268}
269
270impl<'a> NativePluginFactoryContext<'a> {
271    fn from_plan(
272        instance: &'a lenso_app_plan::PluginInstancePlan,
273        resources: &'a InstanceResources,
274    ) -> Self {
275        Self {
276            instance_key: instance.instance_key(),
277            entrypoint: instance.entrypoint(),
278            configuration: instance.configuration(),
279            resources,
280        }
281    }
282
283    /// Returns the App-local Plugin Instance key.
284    pub const fn instance_key(self) -> &'a str {
285        self.instance_key
286    }
287
288    /// Returns the exact package entrypoint selected before boot.
289    pub const fn entrypoint(self) -> &'a str {
290        self.entrypoint
291    }
292
293    /// Returns opaque Plugin-owned configuration selected before boot.
294    pub const fn configuration(self) -> &'a str {
295        self.configuration
296    }
297
298    /// Returns immutable supporting files snapshotted for this Generation.
299    pub const fn resources(self) -> &'a InstanceResources {
300        self.resources
301    }
302}
303
304/// Statically linked native Plugin factories available to an App binary.
305#[derive(Debug, Default)]
306pub struct NativePluginRegistry {
307    factories: Vec<Rc<dyn NativePluginFactory>>,
308    resources: lenso_runtime_codec::InstanceResourceCatalog,
309}
310
311type NativeInstances = BTreeMap<String, NativePluginInstance>;
312type PreparedGenerations = BTreeMap<String, PreparedNativePlugin>;
313type NativeBindings = (
314    Vec<PreparedBinding>,
315    Vec<PreparedStreamBinding>,
316    Vec<PreparedEventBinding>,
317);
318
319fn factory_matches(
320    factory: &dyn NativePluginFactory,
321    expected: &lenso_app_plan::PluginInstancePlan,
322) -> bool {
323    factory.package_id() == expected.package_id()
324        && factory.runtime_profile() == expected.runtime_profile()
325        && (expected.package_revision().is_empty()
326            || factory.package_version() == expected.package_revision()
327            || factory.factory_identity() == expected.package_revision())
328}
329
330impl NativePluginRegistry {
331    /// Creates an empty linked-factory registry.
332    pub fn new() -> Self {
333        Self::default()
334    }
335
336    /// Adds every Plugin factory contributed to this Host at link time.
337    ///
338    /// This catalog describes code available in the binary. The Resolved App
339    /// Plan remains the sole authority that selects and binds Plugin Instances.
340    #[must_use]
341    pub fn with_linked_factories(mut self) -> Self {
342        self.factories.extend(
343            linked_factories()
344                .into_iter()
345                .map(|linked| (linked.constructor)()),
346        );
347        self.factories
348            .sort_by_key(|factory| factory.factory_identity());
349        self.factories
350            .dedup_by(|left, right| left.factory_identity() == right.factory_identity());
351        self
352    }
353
354    /// Injects exact Generation-bound supporting files for selected Instances.
355    #[must_use]
356    pub fn with_resources(
357        mut self,
358        resources: lenso_runtime_codec::InstanceResourceCatalog,
359    ) -> Self {
360        self.resources = resources;
361        self
362    }
363
364    /// Returns the exact native factories available to this registry.
365    pub fn factories(&self) -> impl Iterator<Item = &dyn NativePluginFactory> {
366        self.factories.iter().map(std::convert::AsRef::as_ref)
367    }
368
369    /// Builds the immutable Host Catalog declared by this binary and Host policy.
370    pub fn host_catalog(
371        slots: impl IntoIterator<Item = HostSlot>,
372        defaults: impl IntoIterator<Item = HostDefaultPlugin>,
373    ) -> Result<HostCatalog, RuntimeFailure> {
374        let plugins = linked_factories()
375            .into_iter()
376            .map(|linked| {
377                serde_json::from_str::<PluginDescriptor>(linked.descriptor)
378                    .map(HostPluginRelease::new)
379                    .map_err(|error| RuntimeFailure::InvalidResolvedPlan {
380                        detail: format!("invalid linked Plugin Descriptor: {error}"),
381                    })
382            })
383            .collect::<Result<Vec<_>, _>>()?;
384        Ok(HostCatalog::new(slots, plugins, defaults))
385    }
386    /// Adds one statically linked factory.
387    #[must_use]
388    pub fn with_factory(mut self, factory: impl NativePluginFactory) -> Self {
389        self.factories.push(Rc::new(factory));
390        self
391    }
392
393    fn prepare_instances(
394        &self,
395        plan: &ResolvedAppPlan,
396    ) -> Result<(NativeInstances, PreparedGenerations), RuntimeFailure> {
397        let mut instances = BTreeMap::new();
398        let mut generations = BTreeMap::new();
399        for expected in plan
400            .plugin_instances()
401            .iter()
402            .filter(|instance| instance.execution_class() == &ExecutionClassId::native_rust())
403        {
404            let matching_factories: Vec<_> = self
405                .factories
406                .iter()
407                .filter(|factory| factory_matches(factory.as_ref(), expected))
408                .collect();
409            let factory = match matching_factories.as_slice() {
410                [] => {
411                    return Err(RuntimeFailure::MissingPluginFactory {
412                        instance: expected.instance_key().to_owned(),
413                        package_id: expected.package_id().to_owned(),
414                    });
415                }
416                [factory] => *factory,
417                _ => {
418                    return invalid(format!(
419                        "multiple statically linked factories declare package `{}`",
420                        expected.package_id()
421                    ));
422                }
423            };
424            let generation = factory.instantiate(NativePluginFactoryContext::from_plan(
425                expected,
426                self.resources.for_instance(expected.instance_key()),
427            ))?;
428            generations.insert(
429                expected.instance_key().to_owned(),
430                PreparedNativePlugin::with_endpoint_set_lifecycle(
431                    generation.endpoints.clone(),
432                    generation.lifecycle(),
433                ),
434            );
435            if instances
436                .insert(expected.instance_key().to_owned(), generation)
437                .is_some()
438            {
439                return invalid(format!(
440                    "duplicate Plugin Instance `{}`",
441                    expected.instance_key()
442                ));
443            }
444        }
445        Ok((instances, generations))
446    }
447}
448
449impl NativeExecutionAdapter for NativePluginRegistry {
450    fn supports_runtime_profile(&self, authoring_version: u32, profile: &str) -> bool {
451        matches!(
452            (authoring_version, profile),
453            (1, "lenso.native-authoring@1" | "lenso.native-rust@1")
454                | (2, "lenso.native-authoring@2")
455        )
456    }
457
458    fn prepare(&self, plan: &ResolvedAppPlan) -> Result<PreparedNativeApp, RuntimeFailure> {
459        plan.validate()
460            .map_err(|error| RuntimeFailure::InvalidResolvedPlan {
461                detail: error.to_string(),
462            })?;
463
464        let (instances, generations) = self.prepare_instances(plan)?;
465        let (bindings, stream_bindings, event_bindings) = prepare_bindings(plan, &instances)?;
466        Ok(PreparedNativeApp::new(bindings, generations)
467            .with_stream_bindings(stream_bindings)
468            .with_event_bindings(event_bindings))
469    }
470
471    fn recreate(
472        &self,
473        plan: &ResolvedAppPlan,
474        instance_key: &str,
475    ) -> Result<PreparedNativePlugin, RuntimeFailure> {
476        let expected = plan
477            .plugin_instances()
478            .iter()
479            .find(|instance| instance.instance_key() == instance_key)
480            .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
481                detail: format!("unknown Plugin Instance `{instance_key}`"),
482            })?;
483        let matching_factories: Vec<_> = self
484            .factories
485            .iter()
486            .filter(|factory| factory_matches(factory.as_ref(), expected))
487            .collect();
488        let factory = match matching_factories.as_slice() {
489            [] => {
490                return Err(RuntimeFailure::MissingPluginFactory {
491                    instance: expected.instance_key().to_owned(),
492                    package_id: expected.package_id().to_owned(),
493                });
494            }
495            [factory] => *factory,
496            _ => {
497                return invalid(format!(
498                    "multiple statically linked factories declare package `{}`",
499                    expected.package_id()
500                ));
501            }
502        };
503        let generation = factory.instantiate(NativePluginFactoryContext::from_plan(
504            expected,
505            self.resources.for_instance(expected.instance_key()),
506        ))?;
507        Ok(PreparedNativePlugin::with_endpoint_set_lifecycle(
508            generation.endpoints.clone(),
509            generation.lifecycle(),
510        ))
511    }
512}
513
514fn prepare_bindings(
515    plan: &ResolvedAppPlan,
516    instances: &NativeInstances,
517) -> Result<NativeBindings, RuntimeFailure> {
518    let mut bindings = Vec::new();
519    let mut stream_bindings = Vec::new();
520    let mut event_bindings = Vec::new();
521    for binding in plan.capability_bindings() {
522        if !instances.contains_key(binding.provider_instance()) {
523            continue;
524        }
525        let provider = plan
526            .plugin_instance(binding.provider_instance())
527            .expect("validated binding provider should exist");
528        let descriptor = provider
529            .provided_capabilities()
530            .iter()
531            .find(|descriptor| descriptor.capability_id() == binding.capability_id())
532            .expect("validated binding descriptor should exist");
533        if !descriptor.request_operations().is_empty() {
534            let endpoint = instances
535                .get(binding.provider_instance())
536                .and_then(|instance| {
537                    instance.endpoints.request().iter().find(|endpoint| {
538                        endpoint.capability_id() == binding.capability_id()
539                            && endpoint.descriptor_version() == binding.descriptor_version()
540                    })
541                })
542                .cloned()
543                .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
544                    detail: format!(
545                        "Capability `{}` version `{}` has no request endpoint on provider `{}`",
546                        binding.capability_id(),
547                        binding.descriptor_version(),
548                        binding.provider_instance()
549                    ),
550                })?;
551            bindings.push(
552                PreparedBinding::new(
553                    binding.consumer_instance(),
554                    binding.provider_instance(),
555                    endpoint,
556                )
557                .with_requirement_id(binding.requirement_id()),
558            );
559        }
560        if !descriptor.stream_operations().is_empty() {
561            let endpoint = instances
562                .get(binding.provider_instance())
563                .and_then(|instance| {
564                    instance.endpoints.stream().iter().find(|endpoint| {
565                        endpoint.capability_id() == binding.capability_id()
566                            && endpoint.descriptor_version() == binding.descriptor_version()
567                    })
568                })
569                .cloned()
570                .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
571                    detail: format!(
572                        "Capability `{}` version `{}` has no stream endpoint on provider `{}`",
573                        binding.capability_id(),
574                        binding.descriptor_version(),
575                        binding.provider_instance()
576                    ),
577                })?;
578            stream_bindings.push(
579                PreparedStreamBinding::new(
580                    binding.consumer_instance(),
581                    binding.provider_instance(),
582                    endpoint,
583                )
584                .with_requirement_id(binding.requirement_id()),
585            );
586        }
587        if !descriptor.event_operations().is_empty() {
588            let endpoint = instances
589                .get(binding.provider_instance())
590                .and_then(|instance| {
591                    instance.endpoints.event().iter().find(|endpoint| {
592                        endpoint.capability_id() == binding.capability_id()
593                            && endpoint.descriptor_version() == binding.descriptor_version()
594                    })
595                })
596                .cloned()
597                .ok_or_else(|| RuntimeFailure::InvalidResolvedPlan {
598                    detail: format!(
599                        "Capability `{}` version `{}` has no Event endpoint on provider `{}`",
600                        binding.capability_id(),
601                        binding.descriptor_version(),
602                        binding.provider_instance()
603                    ),
604                })?;
605            event_bindings.push(
606                PreparedEventBinding::new(
607                    binding.consumer_instance(),
608                    binding.provider_instance(),
609                    endpoint,
610                )
611                .with_requirement_id(binding.requirement_id()),
612            );
613        }
614    }
615    Ok((bindings, stream_bindings, event_bindings))
616}
617
618fn invalid<T>(detail: String) -> Result<T, RuntimeFailure> {
619    Err(RuntimeFailure::InvalidResolvedPlan { detail })
620}