Skip to main content

bb_runtime/
concrete.rs

1//! `ConcreteComponent` polymorphism contract + `ComponentHandle`
2//! fn-pointer-capture wrapper. See `docs/AUTHORING_COMPONENTS.md`
3//! §4 + §9.
4
5use std::any::Any;
6
7use bb_ir::component::ErasedComponent;
8
9// Foundation plumbing lives in `bb_ir::component` to keep `bb-dsl`
10// and `bb-runtime` cycle-free; re-exported here for ergonomics.
11pub use bb_ir::component::{
12    ComponentPackage, ConstructError, ConstructFn, DependencyDecl, RestoreError, RestoreFn,
13    SerializeFn,
14};
15
16/// Polymorphism contract. Implementing this trait IS the
17/// registration mechanism — no global registry, no macro
18/// required. Serialized state must be self-contained so `restore`
19/// reconstructs without the original `Config`.
20pub trait ConcreteComponent: ErasedComponent + Sized {
21    /// Stable identifier. Convention: `<crate>::<TypeName>`.
22    const TYPE_NAME: &'static str;
23
24    /// Origin tag; defaults to `Application`.
25    const PACKAGE: ComponentPackage = ComponentPackage::Application;
26
27    /// Sibling components this depends on. Populated by the
28    /// `bb::Concrete` derive from `#[bb::depends(...)]`.
29    const DEPENDENCIES: &'static [DependencyDecl] = &[];
30
31    /// Per-deployment config. Use `()` for stateless concretes.
32    type Config: Any + 'static;
33
34    /// Error from [`Self::new`]; use `Infallible` if construction
35    /// can't fail.
36    type Error: std::error::Error + Send + Sync + 'static;
37
38    /// Construct from `&Self::Config`. Install calls this once per
39    /// slot.
40    fn new(config: &Self::Config) -> Result<Self, Self::Error>;
41
42    /// Serialize state to bytes, including config-derived fields.
43    fn serialize(&self) -> Vec<u8>;
44
45    /// Reconstruct from `serialize` output.
46    fn restore(bytes: &[u8]) -> Result<Self, RestoreError>;
47}
48
49/// Owned wrapper that travels through the ModelProto → Node
50/// pipeline. Carries captured fn pointers + state bytes.
51pub struct ComponentHandle {
52    /// Stable type identifier.
53    pub type_name: &'static str,
54    /// Origin tag.
55    pub package: ComponentPackage,
56    /// Per-Node instance disambiguator assigned by install.
57    pub instance_id: u32,
58    /// Monomorphized `T::serialize` via downcast.
59    pub serialize_fn: SerializeFn,
60    /// Monomorphized `T::restore`.
61    pub restore_fn: RestoreFn,
62    /// Serialized state captured at build or snapshot time.
63    pub state_bytes: Vec<u8>,
64}
65
66impl std::fmt::Debug for ComponentHandle {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        f.debug_struct("ComponentHandle")
69            .field("type_name", &self.type_name)
70            .field("package", &self.package)
71            .field("instance_id", &self.instance_id)
72            .field("state_bytes_len", &self.state_bytes.len())
73            .finish_non_exhaustive()
74    }
75}
76
77impl ComponentHandle {
78    /// Materialize a fresh instance via `restore_fn(state_bytes)`.
79    pub fn materialize(&self) -> Result<Box<dyn ErasedComponent>, RestoreError> {
80        (self.restore_fn)(&self.state_bytes)
81    }
82
83    /// Capture state from a live `&dyn ErasedComponent`.
84    pub fn capture_state(&self, instance: &dyn ErasedComponent) -> Vec<u8> {
85        (self.serialize_fn)(instance)
86    }
87
88    /// Build from a live `&T: ConcreteComponent`. Captures
89    /// monomorphized `serialize`/`restore` + freezes current bytes.
90    pub fn from_concrete<T: ConcreteComponent>(instance: &T, instance_id: u32) -> Self {
91        Self {
92            type_name: T::TYPE_NAME,
93            package: T::PACKAGE,
94            instance_id,
95            serialize_fn: |erased: &dyn ErasedComponent| -> Vec<u8> {
96                let any: &dyn Any = erased;
97                let concrete: &T = any
98                    .downcast_ref::<T>()
99                    .expect("ComponentHandle::serialize_fn called with mismatched type");
100                concrete.serialize()
101            },
102            restore_fn: |bytes: &[u8]| -> Result<Box<dyn ErasedComponent>, RestoreError> {
103                T::restore(bytes).map(|v| Box::new(v) as Box<dyn ErasedComponent>)
104            },
105            state_bytes: instance.serialize(),
106        }
107    }
108}