Skip to main content

lenso_native_adapter/
lib.rs

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