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.serial0" } // pick whichever serial port you want
13//!         ),
14//!     ],
15//!     tasks: [
16//!         ( id: "telemetry", type: "app::TelemetryTask",
17//!           resources: { bus: "board.telemetry_bus" } // shared: borrowed
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//!     fn from_bindings(mgr: &'r mut ResourceManager, map: Option<&ResourceMapping>) -> CuResult<Self> {
29//!         let key = map.expect("bus binding").get("bus").expect("bus").typed();
30//!         Ok(Self { bus: mgr.borrow(key)? })
31//!     }
32//! }
33//! pub fn new_with(_cfg: Option<&ComponentConfig>, res: TelemetryResources<'_>) -> CuResult<Self> {
34//!     Ok(Self { bus: res.bus })
35//! }
36//! ```
37//! Otherwise, use config to point to the right board resource and you're done.
38
39use crate::config::ComponentConfig;
40use core::any::Any;
41use core::fmt;
42use core::marker::PhantomData;
43use cu29_traits::{CuError, CuResult};
44
45use alloc::boxed::Box;
46use alloc::sync::Arc;
47use alloc::vec::Vec;
48
49/// Lightweight wrapper used when a task needs to take ownership of a resource.
50pub struct Owned<T>(pub T);
51
52/// Wrapper used when a task needs to borrow a resource that remains managed by
53/// the `ResourceManager`.
54pub struct Borrowed<'r, T>(pub &'r T);
55
56/// A resource can be exclusive (most common case) or shared.
57enum ResourceEntry {
58    Owned(Box<dyn Any + Send + Sync>),
59    Shared(Arc<dyn Any + Send + Sync>),
60}
61
62impl ResourceEntry {
63    fn as_shared<T: 'static + Send + Sync>(&self) -> Option<&T> {
64        match self {
65            ResourceEntry::Shared(arc) => arc.downcast_ref::<T>(),
66            ResourceEntry::Owned(boxed) => boxed.downcast_ref::<T>(),
67        }
68    }
69
70    fn into_owned<T: 'static + Send + Sync>(self) -> Option<T> {
71        match self {
72            ResourceEntry::Owned(boxed) => boxed.downcast::<T>().map(|b| *b).ok(),
73            ResourceEntry::Shared(_) => None,
74        }
75    }
76}
77
78/// Typed identifier for a resource entry.
79#[derive(Copy, Clone, Eq, PartialEq)]
80pub struct ResourceKey<T = ()> {
81    // This index is unique per mission
82    index: usize,
83    _boo: PhantomData<fn() -> T>,
84}
85
86impl<T> ResourceKey<T> {
87    pub const fn new(index: usize) -> Self {
88        Self {
89            index,
90            _boo: PhantomData,
91        }
92    }
93
94    pub const fn index(&self) -> usize {
95        self.index
96    }
97
98    /// Reinterpret this key as pointing to a concrete resource type.
99    pub fn typed<U>(self) -> ResourceKey<U> {
100        ResourceKey {
101            index: self.index,
102            _boo: PhantomData,
103        }
104    }
105}
106
107impl<T> fmt::Debug for ResourceKey<T> {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        f.debug_struct("ResourceKey")
110            .field("index", &self.index)
111            .finish()
112    }
113}
114
115/// Static declaration of a single resource path bound to a key.
116#[derive(Copy, Clone, Debug)]
117pub struct ResourceDecl {
118    pub key: ResourceKey,
119    pub path: &'static str,
120}
121
122impl ResourceDecl {
123    pub const fn new(key: ResourceKey, path: &'static str) -> Self {
124        Self { key, path }
125    }
126}
127
128/// Static mapping between user-defined binding names (e.g. "bus", "irq") and
129/// resource keys. Backed by a slice to avoid runtime allocation.
130#[derive(Clone, Copy)]
131pub struct ResourceMapping {
132    entries: &'static [(&'static str, ResourceKey)],
133}
134
135impl ResourceMapping {
136    pub const fn new(entries: &'static [(&'static str, ResourceKey)]) -> Self {
137        Self { entries }
138    }
139
140    pub fn get(&self, name: &str) -> Option<ResourceKey> {
141        self.entries
142            .iter()
143            .find(|(entry_name, _)| *entry_name == name)
144            .map(|(_, key)| *key)
145    }
146}
147
148/// Manages the concrete resources available to tasks and bridges.
149pub struct ResourceManager {
150    entries: Box<[Option<ResourceEntry>]>,
151}
152
153impl ResourceManager {
154    /// Creates a new manager sized for the number of resources generated for
155    /// the current mission.
156    pub fn new(num_resources: usize) -> Self {
157        let mut entries = Vec::with_capacity(num_resources);
158        entries.resize_with(num_resources, || None);
159        Self {
160            entries: entries.into_boxed_slice(),
161        }
162    }
163
164    /// Register an owned resource in the slot identified by `key`.
165    pub fn add_owned<T: 'static + Send + Sync>(
166        &mut self,
167        key: ResourceKey<T>,
168        value: T,
169    ) -> CuResult<()> {
170        let idx = key.index();
171        if self.entries[idx].is_some() {
172            return Err(CuError::from("Resource already registered"));
173        }
174        self.entries[idx] = Some(ResourceEntry::Owned(Box::new(value)));
175        Ok(())
176    }
177
178    /// Register a shared (borrowed) resource. Callers keep an `Arc` while tasks
179    /// receive references.
180    pub fn add_shared<T: 'static + Send + Sync>(
181        &mut self,
182        key: ResourceKey<T>,
183        value: Arc<T>,
184    ) -> CuResult<()> {
185        let idx = key.index();
186        if self.entries[idx].is_some() {
187            return Err(CuError::from("Resource already registered"));
188        }
189        self.entries[idx] = Some(ResourceEntry::Shared(value as Arc<dyn Any + Send + Sync>));
190        Ok(())
191    }
192
193    /// Borrow a shared resource by key.
194    pub fn borrow<'r, T: 'static + Send + Sync>(
195        &'r self,
196        key: ResourceKey<T>,
197    ) -> CuResult<Borrowed<'r, T>> {
198        let idx = key.index();
199        let entry = self
200            .entries
201            .get(idx)
202            .and_then(|opt| opt.as_ref())
203            .ok_or_else(|| CuError::from("Resource not found"))?;
204        entry
205            .as_shared::<T>()
206            .map(Borrowed)
207            .ok_or_else(|| CuError::from("Resource has unexpected type"))
208    }
209
210    /// Borrow a shared `Arc`-backed resource by key, cloning the `Arc` for the caller.
211    #[cfg(feature = "std")]
212    pub fn borrow_shared_arc<T: 'static + Send + Sync>(
213        &self,
214        key: ResourceKey<T>,
215    ) -> CuResult<Arc<T>> {
216        let idx = key.index();
217        let entry = self
218            .entries
219            .get(idx)
220            .and_then(|opt| opt.as_ref())
221            .ok_or_else(|| CuError::from("Resource not found"))?;
222        match entry {
223            ResourceEntry::Shared(arc) => arc
224                .clone()
225                .downcast::<T>()
226                .map_err(|_| CuError::from("Resource has unexpected type")),
227            ResourceEntry::Owned(_) => Err(CuError::from("Resource is owned, not shared")),
228        }
229    }
230
231    /// Move out an owned resource by key.
232    pub fn take<T: 'static + Send + Sync>(&mut self, key: ResourceKey<T>) -> CuResult<Owned<T>> {
233        let idx = key.index();
234        let entry = self.entries.get_mut(idx).and_then(|opt| opt.take());
235        let entry = entry.ok_or_else(|| CuError::from("Resource not found"))?;
236        entry
237            .into_owned::<T>()
238            .map(Owned)
239            .ok_or_else(|| CuError::from("Resource is not owned or has unexpected type"))
240    }
241
242    /// Insert a prebuilt bundle by running a caller-supplied function. This is
243    /// the escape hatch for resources that must be constructed in application
244    /// code (for example, owning handles to embedded peripherals).
245    pub fn add_bundle_prebuilt(
246        &mut self,
247        builder: impl FnOnce(&mut ResourceManager) -> CuResult<()>,
248    ) -> CuResult<()> {
249        builder(self)
250    }
251}
252
253/// Trait implemented by resource binding structs passed to task/bridge
254/// constructors. Implementors pull the concrete resources they need from the
255/// `ResourceManager`, using the symbolic mapping provided in the Copper config
256/// (`resources: { name: "bundle.resource" }`).
257pub trait ResourceBindings<'r>: Sized {
258    fn from_bindings(
259        manager: &'r mut ResourceManager,
260        mapping: Option<&ResourceMapping>,
261    ) -> CuResult<Self>;
262}
263
264impl<'r> ResourceBindings<'r> for () {
265    fn from_bindings(
266        _manager: &'r mut ResourceManager,
267        _mapping: Option<&ResourceMapping>,
268    ) -> CuResult<Self> {
269        Ok(())
270    }
271}
272
273/// Bundle providers implement this trait to populate the `ResourceManager` with
274/// concrete resources for a given bundle id.
275pub trait ResourceBundle {
276    fn build(
277        bundle_id: &str,
278        config: Option<&ComponentConfig>,
279        resources: &[ResourceDecl],
280        manager: &mut ResourceManager,
281    ) -> CuResult<()>;
282}
283
284/// Static metadata describing how to create a bundle of resources.
285///
286/// The runtime will call the provider identified by `provider_path` to populate
287/// the resources listed in `resources`.
288#[derive(Copy, Clone, Debug)]
289pub struct ResourceProvider {
290    pub id: &'static str,
291    pub provider_path: &'static str,
292    pub resources: &'static [ResourceDecl],
293}
294
295impl ResourceProvider {
296    pub const fn new(
297        id: &'static str,
298        provider_path: &'static str,
299        resources: &'static [ResourceDecl],
300    ) -> Self {
301        Self {
302            id,
303            provider_path,
304            resources,
305        }
306    }
307}
308
309// The legacy `resources!` macro was replaced by the proc-macro in `cu29_derive::resources`.
310// Import it from `cu29_derive` or `cu29::prelude`.
311
312#[cfg(feature = "std")]
313pub struct ThreadPoolBundle;
314
315#[cfg(feature = "std")]
316impl ResourceBundle for ThreadPoolBundle {
317    fn build(
318        bundle_id: &str,
319        config: Option<&ComponentConfig>,
320        resources: &[ResourceDecl],
321        manager: &mut ResourceManager,
322    ) -> CuResult<()> {
323        use rayon::ThreadPoolBuilder;
324
325        const DEFAULT_THREADS: usize = 2;
326        let threads: usize = config
327            .and_then(|cfg| cfg.get::<u64>("threads"))
328            .map(|v| v as usize)
329            .unwrap_or(DEFAULT_THREADS);
330
331        let pool = ThreadPoolBuilder::new()
332            .num_threads(threads)
333            .build()
334            .map_err(|e| CuError::from(format!("Failed to build threadpool: {e}")))?;
335
336        let key = resources
337            .iter()
338            .find(|decl| decl.path == format!("{bundle_id}.bg_threads"))
339            .ok_or_else(|| {
340                CuError::from("ThreadPoolBundle missing required resource 'bg_threads'")
341            })?
342            .key
343            .typed();
344
345        manager.add_shared(key, Arc::new(pool))
346    }
347}