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