Skip to main content

lenso_native_adapter/
lib.rs

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