Skip to main content

cu29_runtime/
resource.rs

1//! Resource descriptors and utilities to hand resources to tasks and bridges.
2//! User view: in `copperconfig.ron`, map the binding names your tasks/bridges
3//! expect to the resources exported by your board bundle. Exclusive things
4//! (like a serial port) should be bound once; shared things (like a telemetry
5//! bus `Arc`) can be bound to multiple consumers.
6//!
7//! ```ron
8//! (
9//!     resources: [ ( id: "board", provider: "board_crate::BoardBundle" ) ],
10//!     bridges: [
11//!         ( id: "crsf", type: "cu_crsf::CrsfBridge<SerialPort, SerialError>",
12//!           resources: { serial: "board.uart0" }
13//!         ),
14//!     ],
15//!     tasks: [
16//!         ( id: "telemetry", type: "app::TelemetryTask",
17//!           resources: { bus: "board.telemetry_bus" }
18//!         ),
19//!     ],
20//! )
21//! ```
22//!
23//! Writing your own task/bridge? Add a small `Resources` struct and implement
24//! `ResourceBindings` to pull the names you declared:
25//! ```rust,ignore
26//! pub struct TelemetryResources<'r> { pub bus: Borrowed<'r, TelemetryBus> }
27//! impl<'r> ResourceBindings<'r> for TelemetryResources<'r> {
28//!     type Binding = Binding;
29//!     fn from_bindings(mgr: &'r mut ResourceManager, map: Option<&ResourceBindingMap<Self::Binding>>) -> CuResult<Self> {
30//!         let key = map.expect("bus binding").get(Binding::Bus).expect("bus").typed();
31//!         Ok(Self { bus: mgr.borrow(key)? })
32//!     }
33//! }
34//! pub fn new(_cfg: Option<&ComponentConfig>, res: TelemetryResources<'_>) -> CuResult<Self> {
35//!     Ok(Self { bus: res.bus })
36//! }
37//! ```
38//! Or use the `resources!` macro. `Shared<T>` bindings clone the registered
39//! `Arc<T>` so tasks can keep a shared handle without borrowing from the
40//! manager for their full lifetime. `Borrowed<T>` bindings borrow from the
41//! manager directly.
42//! Bundles can also consume resources through their own RON `resources` map.
43//! Declare inputs with `resources!`, set [`ResourceBundle::INPUT_NAMES`] to the
44//! generated [`ResourceBindings::NAMES`], and call [`BundleContext::inputs`] during
45//! `build`. The runtime macro orders bundle construction per mission and compiles
46//! bindings into static keys. Existing bundles have no inputs by default.
47
48use crate::config::ComponentConfig;
49use core::any::Any;
50use core::fmt;
51use core::marker::PhantomData;
52use cu29_traits::{CuError, CuResult};
53
54use alloc::boxed::Box;
55use alloc::format;
56use alloc::sync::Arc;
57use alloc::vec::Vec;
58
59/// Lightweight wrapper used when a task needs to take ownership of a resource.
60pub struct Owned<T>(pub T);
61
62/// Wrapper used when a task needs to borrow a resource that remains managed by
63/// the `ResourceManager`.
64pub struct Borrowed<'r, T>(pub &'r T);
65
66/// A resource can be exclusive (most common case) or shared.
67enum ResourceEntry {
68    Owned(Box<dyn Any + Send + Sync>),
69    Shared(Arc<dyn Any + Send + Sync>),
70}
71
72impl ResourceEntry {
73    fn as_shared<T: 'static + Send + Sync>(&self) -> Option<&T> {
74        match self {
75            ResourceEntry::Shared(arc) => arc.downcast_ref::<T>(),
76            ResourceEntry::Owned(boxed) => boxed.downcast_ref::<T>(),
77        }
78    }
79
80    fn as_shared_arc<T: 'static + Send + Sync>(&self) -> Option<Arc<T>> {
81        match self {
82            ResourceEntry::Shared(arc) => Arc::downcast::<T>(arc.clone()).ok(),
83            ResourceEntry::Owned(_) => None,
84        }
85    }
86
87    fn into_owned<T: 'static + Send + Sync>(self) -> Option<T> {
88        match self {
89            ResourceEntry::Owned(boxed) => boxed.downcast::<T>().map(|b| *b).ok(),
90            ResourceEntry::Shared(_) => None,
91        }
92    }
93}
94
95/// Typed identifier for a resource entry.
96#[derive(Copy, Clone, Eq, PartialEq)]
97pub struct ResourceKey<T = ()> {
98    bundle: BundleIndex,
99    index: usize,
100    _boo: PhantomData<fn() -> T>,
101}
102
103impl<T> ResourceKey<T> {
104    pub const fn new(bundle: BundleIndex, index: usize) -> Self {
105        Self {
106            bundle,
107            index,
108            _boo: PhantomData,
109        }
110    }
111
112    pub const fn bundle(&self) -> BundleIndex {
113        self.bundle
114    }
115
116    pub const fn index(&self) -> usize {
117        self.index
118    }
119
120    /// Reinterpret this key as pointing to a concrete resource type.
121    pub fn typed<U>(self) -> ResourceKey<U> {
122        ResourceKey {
123            bundle: self.bundle,
124            index: self.index,
125            _boo: PhantomData,
126        }
127    }
128}
129
130impl<T> fmt::Debug for ResourceKey<T> {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        f.debug_struct("ResourceKey")
133            .field("bundle", &self.bundle.index())
134            .field("index", &self.index)
135            .finish()
136    }
137}
138
139/// Index identifying a resource bundle in the active mission.
140#[derive(Copy, Clone, Debug, Eq, PartialEq)]
141pub struct BundleIndex(usize);
142
143impl BundleIndex {
144    pub const fn new(index: usize) -> Self {
145        Self(index)
146    }
147
148    pub const fn index(self) -> usize {
149        self.0
150    }
151
152    pub fn key<T, I: ResourceId>(self, id: I) -> ResourceKey<T> {
153        ResourceKey::new(self, id.index())
154    }
155}
156
157/// Trait implemented by resource id enums generated by `bundle_resources!`.
158pub trait ResourceId: Copy + Eq {
159    const COUNT: usize;
160    fn index(self) -> usize;
161}
162
163/// Trait implemented by bundle providers to declare their resource id enum.
164pub trait ResourceBundleDecl {
165    type Id: ResourceId;
166}
167
168/// Optional name metadata for resource bundles.
169///
170/// Bundles created via `bundle_resources!` implement this automatically. The
171/// derive macro uses these canonical slot names to resolve `bundle.resource`
172/// bindings without guessing enum variant casing from config strings.
173pub trait NamedResourceBundleDecl: ResourceBundleDecl {
174    const NAMES: &'static [&'static str];
175}
176
177const fn str_eq(left: &str, right: &str) -> bool {
178    let left = left.as_bytes();
179    let right = right.as_bytes();
180    if left.len() != right.len() {
181        return false;
182    }
183
184    let mut idx = 0;
185    while idx < left.len() {
186        if left[idx] != right[idx] {
187            return false;
188        }
189        idx += 1;
190    }
191
192    true
193}
194
195/// Resolve a bundle slot name to its resource index.
196///
197/// This is a `const fn` so generated resource binding tables can stay static.
198#[doc(hidden)]
199pub const fn resource_index_by_name<B: NamedResourceBundleDecl>(name: &str) -> usize {
200    let mut idx = 0;
201    while idx < B::NAMES.len() {
202        if str_eq(B::NAMES[idx], name) {
203            return idx;
204        }
205        idx += 1;
206    }
207
208    panic!("resource slot name not declared by bundle");
209}
210
211/// Static mapping between user-defined binding ids and resource keys.
212#[derive(Clone, Copy)]
213pub struct ResourceBindingMap<B: Copy + Eq + 'static> {
214    entries: &'static [(B, ResourceKey)],
215}
216
217impl<B: Copy + Eq + 'static> ResourceBindingMap<B> {
218    pub const fn new(entries: &'static [(B, ResourceKey)]) -> Self {
219        Self { entries }
220    }
221
222    pub fn get(&self, binding: B) -> Option<ResourceKey> {
223        self.entries
224            .iter()
225            .find(|(entry_id, _)| *entry_id == binding)
226            .map(|(_, key)| *key)
227    }
228}
229
230/// Manages the concrete resources available to tasks and bridges.
231pub struct ResourceManager {
232    bundles: Box<[BundleEntries]>,
233}
234
235struct BundleEntries {
236    entries: Box<[Option<ResourceEntry>]>,
237}
238
239impl ResourceManager {
240    /// Creates a new manager sized for the number of resources generated for
241    /// each bundle in the current mission.
242    pub fn new(bundle_sizes: &[usize]) -> Self {
243        let bundles = bundle_sizes
244            .iter()
245            .map(|size| {
246                let mut entries = Vec::with_capacity(*size);
247                entries.resize_with(*size, || None);
248                BundleEntries {
249                    entries: entries.into_boxed_slice(),
250                }
251            })
252            .collect::<Vec<_>>();
253        Self {
254            bundles: bundles.into_boxed_slice(),
255        }
256    }
257
258    fn entry_mut<T>(&mut self, key: ResourceKey<T>) -> CuResult<&mut Option<ResourceEntry>> {
259        let bundle = self
260            .bundles
261            .get_mut(key.bundle.index())
262            .ok_or_else(|| CuError::from("Resource bundle index out of range"))?;
263        bundle
264            .entries
265            .get_mut(key.index)
266            .ok_or_else(|| CuError::from("Resource index out of range"))
267    }
268
269    fn entry<T>(&self, key: ResourceKey<T>) -> CuResult<&ResourceEntry> {
270        let bundle = self
271            .bundles
272            .get(key.bundle.index())
273            .ok_or_else(|| CuError::from("Resource bundle index out of range"))?;
274        bundle
275            .entries
276            .get(key.index)
277            .and_then(|opt| opt.as_ref())
278            .ok_or_else(|| CuError::from("Resource not found"))
279    }
280
281    fn take_entry<T>(&mut self, key: ResourceKey<T>) -> CuResult<ResourceEntry> {
282        let bundle = self
283            .bundles
284            .get_mut(key.bundle.index())
285            .ok_or_else(|| CuError::from("Resource bundle index out of range"))?;
286        let entry = bundle
287            .entries
288            .get_mut(key.index)
289            .and_then(|opt| opt.take())
290            .ok_or_else(|| CuError::from("Resource not found"))?;
291        Ok(entry)
292    }
293
294    /// Register an owned resource in the slot identified by `key`.
295    pub fn add_owned<T: 'static + Send + Sync>(
296        &mut self,
297        key: ResourceKey<T>,
298        value: T,
299    ) -> CuResult<()> {
300        let entry = self.entry_mut(key)?;
301        if entry.is_some() {
302            return Err(CuError::from("Resource already registered"));
303        }
304        *entry = Some(ResourceEntry::Owned(Box::new(value)));
305        Ok(())
306    }
307
308    /// Register a shared (borrowed) resource. Callers keep an `Arc` while tasks
309    /// receive references.
310    pub fn add_shared<T: 'static + Send + Sync>(
311        &mut self,
312        key: ResourceKey<T>,
313        value: Arc<T>,
314    ) -> CuResult<()> {
315        let entry = self.entry_mut(key)?;
316        if entry.is_some() {
317            return Err(CuError::from("Resource already registered"));
318        }
319        *entry = Some(ResourceEntry::Shared(value as Arc<dyn Any + Send + Sync>));
320        Ok(())
321    }
322
323    /// Borrow a shared resource by key.
324    pub fn borrow<'r, T: 'static + Send + Sync>(
325        &'r self,
326        key: ResourceKey<T>,
327    ) -> CuResult<Borrowed<'r, T>> {
328        let entry = self.entry(key)?;
329        entry.as_shared::<T>().map(Borrowed).ok_or_else(|| {
330            CuError::from(format!(
331                "Borrowing Resource has unexpected type, expected '{}'",
332                core::any::type_name::<T>()
333            ))
334        })
335    }
336
337    /// Borrow a shared `Arc`-backed resource by key, cloning the `Arc` for the caller.
338    pub fn borrow_shared_arc<T: 'static + Send + Sync>(
339        &self,
340        key: ResourceKey<T>,
341    ) -> CuResult<Arc<T>> {
342        let entry = self.entry(key)?;
343        entry.as_shared_arc::<T>().ok_or_else(|| {
344            CuError::from(format!(
345                "Borrow Shared Resource '{}' has unexpected type",
346                core::any::type_name::<T>()
347            ))
348        })
349    }
350
351    /// Take ownership of a resource by key.
352    pub fn take<T: 'static + Send + Sync>(&mut self, key: ResourceKey<T>) -> CuResult<Owned<T>> {
353        let entry = self.take_entry(key)?;
354        entry.into_owned::<T>().map(Owned).ok_or_else(|| {
355            CuError::from(format!(
356                "Resource {} is not owned or has unexpected type",
357                core::any::type_name::<T>()
358            ))
359        })
360    }
361
362    /// Insert a prebuilt bundle by running a caller-supplied function. This is
363    /// the escape hatch for resources that must be constructed in application
364    /// code (for example, owning handles to embedded peripherals).
365    pub fn add_bundle_prebuilt(
366        &mut self,
367        builder: impl FnOnce(&mut ResourceManager) -> CuResult<()>,
368    ) -> CuResult<()> {
369        builder(self)
370    }
371}
372
373/// Trait implemented by resource binding structs passed to task/bridge
374/// constructors. Implementors pull the concrete resources they need from the
375/// `ResourceManager`, using the symbolic mapping provided in the Copper config
376/// (`resources: { name: "bundle.resource" }`).
377pub trait ResourceBindings<'r>: Sized {
378    type Binding: Copy + Eq + 'static;
379
380    /// Input names in declaration order, generated by `resources!`.
381    const NAMES: &'static [&'static str] = &[];
382
383    /// Construct bundle inputs from compile-time resolved keys.
384    fn from_keys(_manager: &'r mut ResourceManager, _keys: &[ResourceKey]) -> CuResult<Self> {
385        Err(CuError::from(
386            "Resource bindings do not support bundle inputs",
387        ))
388    }
389
390    fn from_bindings(
391        manager: &'r mut ResourceManager,
392        mapping: Option<&ResourceBindingMap<Self::Binding>>,
393    ) -> CuResult<Self>;
394}
395
396impl<'r> ResourceBindings<'r> for () {
397    type Binding = ();
398
399    fn from_keys(_manager: &'r mut ResourceManager, keys: &[ResourceKey]) -> CuResult<Self> {
400        if !keys.is_empty() {
401            return Err(CuError::from("Unexpected resource inputs"));
402        }
403        Ok(())
404    }
405
406    fn from_bindings(
407        _manager: &'r mut ResourceManager,
408        _mapping: Option<&ResourceBindingMap<Self::Binding>>,
409    ) -> CuResult<Self> {
410        Ok(())
411    }
412}
413
414/// Bundle providers implement this trait to populate the `ResourceManager` with
415/// concrete resources for a given bundle id.
416pub trait ResourceBundle: ResourceBundleDecl + Sized {
417    /// Dependencies consumed during construction. Existing bundles have none.
418    const INPUT_NAMES: &'static [&'static str] = &[];
419
420    fn build(
421        bundle: BundleContext<Self>,
422        config: Option<&ComponentConfig>,
423        manager: &mut ResourceManager,
424    ) -> CuResult<()>;
425}
426
427/// Context passed to bundle providers when building resources.
428pub struct BundleContext<B: ResourceBundleDecl> {
429    bundle_index: BundleIndex,
430    bundle_id: &'static str,
431    input_keys: &'static [ResourceKey],
432    _boo: PhantomData<B>,
433}
434
435impl<B: ResourceBundleDecl> BundleContext<B> {
436    pub const fn new(bundle_index: BundleIndex, bundle_id: &'static str) -> Self {
437        Self {
438            bundle_index,
439            bundle_id,
440            input_keys: &[],
441            _boo: PhantomData,
442        }
443    }
444
445    pub const fn bundle_id(&self) -> &'static str {
446        self.bundle_id
447    }
448
449    pub const fn bundle_index(&self) -> BundleIndex {
450        self.bundle_index
451    }
452
453    pub fn key<T>(&self, id: B::Id) -> ResourceKey<T> {
454        ResourceKey::new(self.bundle_index, id.index())
455    }
456
457    /// Attach statically generated input keys, in `INPUT_NAMES` order.
458    pub const fn with_input_keys(mut self, keys: &'static [ResourceKey]) -> Self {
459        self.input_keys = keys;
460        self
461    }
462
463    /// Resolve typed construction inputs. Owned inputs move into the new resource;
464    /// shared inputs clone a handle. Borrowed inputs cannot outlive the manager borrow.
465    pub fn inputs<'r, R: ResourceBindings<'r>>(
466        &self,
467        manager: &'r mut ResourceManager,
468    ) -> CuResult<R>
469    where
470        B: ResourceBundle,
471    {
472        if B::INPUT_NAMES != R::NAMES || self.input_keys.len() != R::NAMES.len() {
473            return Err(CuError::from("Resource bundle input declaration mismatch"));
474        }
475        R::from_keys(manager, self.input_keys)
476    }
477}
478
479/// Order and validate bundle inputs during constant evaluation, without runtime
480/// name lookup or allocations. Missing, duplicate and unknown bindings are errors.
481#[doc(hidden)]
482pub const fn resource_input_keys<const N: usize>(
483    names: &[&str],
484    entries: &[(&str, ResourceKey)],
485) -> [ResourceKey; N] {
486    assert!(
487        names.len() == N && entries.len() == N,
488        "missing resource bundle inputs"
489    );
490    let mut keys = [ResourceKey::new(BundleIndex::new(0), 0); N];
491    let mut seen = [false; N];
492    let mut entry = 0;
493    while entry < entries.len() {
494        let mut index = 0;
495        while index < N && !str_eq(names[index], entries[entry].0) {
496            index += 1;
497        }
498        assert!(index < N, "unknown resource bundle input");
499        assert!(!seen[index], "duplicate resource bundle input");
500        keys[index] = entries[entry].1;
501        seen[index] = true;
502        entry += 1;
503    }
504    keys
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510
511    #[derive(Copy, Clone, Eq, PartialEq)]
512    #[repr(usize)]
513    enum DummyBundleId {
514        Uart0,
515        I2c1,
516    }
517
518    impl ResourceId for DummyBundleId {
519        const COUNT: usize = 2;
520
521        fn index(self) -> usize {
522            self as usize
523        }
524    }
525
526    struct DummyBundle;
527
528    impl ResourceBundleDecl for DummyBundle {
529        type Id = DummyBundleId;
530    }
531
532    impl NamedResourceBundleDecl for DummyBundle {
533        const NAMES: &'static [&'static str] = &["uart0", "i2c1"];
534    }
535
536    #[test]
537    fn resource_index_by_name_matches_declared_slot_name() {
538        assert_eq!(DummyBundleId::Uart0.index(), 0);
539        assert_eq!(DummyBundleId::I2c1.index(), 1);
540        assert_eq!(resource_index_by_name::<DummyBundle>("uart0"), 0);
541        assert_eq!(resource_index_by_name::<DummyBundle>("i2c1"), 1);
542    }
543}