Skip to main content

khora_core/runtime/
backends.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! `Backends` — typed container for **engine backends** (concrete
16//! implementations of abstract traits like [`RenderSystem`],
17//! [`PhysicsProvider`], [`AudioDevice`], [`LayoutSystem`]).
18//!
19//! See [`crate::runtime`] for the broader Services / Backends / Resources
20//! taxonomy.
21//!
22//! Backends are swappable by design — that is precisely why they live
23//! behind a trait. Game devs and plugins may register their own backends
24//! (e.g. a custom `NetworkProvider`) using the same API.
25//!
26//! # Convention
27//!
28//! Always register under the trait-object type, not the concrete impl:
29//!
30//! ```ignore
31//! backends.insert::<Arc<Mutex<Box<dyn PhysicsProvider>>>>(physics_arc);
32//! let physics = backends.get::<Arc<Mutex<Box<dyn PhysicsProvider>>>>();
33//! ```
34//!
35//! [`RenderSystem`]: crate::renderer::RenderSystem
36//! [`PhysicsProvider`]: crate::physics::PhysicsProvider
37//! [`AudioDevice`]: crate::audio::device::AudioDevice
38//! [`LayoutSystem`]: crate::ui::LayoutSystem
39
40use std::any::{type_name, Any, TypeId};
41use std::collections::HashMap;
42use std::sync::Arc;
43
44/// Container of engine backends — concrete impls of abstract traits.
45///
46/// API mirrors the legacy `ServiceRegistry` (drop-in replacement).
47#[derive(Default)]
48pub struct Backends {
49    inner: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
50    parent: Option<Arc<Backends>>,
51}
52
53impl Backends {
54    /// Creates an empty container.
55    #[must_use]
56    pub fn new() -> Self {
57        Self {
58            inner: HashMap::new(),
59            parent: None,
60        }
61    }
62
63    /// Creates a container that delegates lookups to `parent` when a key
64    /// is absent locally.
65    #[must_use]
66    pub fn with_parent(parent: Arc<Backends>) -> Self {
67        Self {
68            inner: HashMap::new(),
69            parent: Some(parent),
70        }
71    }
72
73    /// Inserts a backend, keyed by `T`'s `TypeId`. Convention: `T` is
74    /// the trait-object type (e.g. `Arc<Mutex<Box<dyn PhysicsProvider>>>`).
75    pub fn insert<T: Send + Sync + 'static>(&mut self, backend: T) {
76        self.inner.insert(TypeId::of::<T>(), Box::new(backend));
77    }
78
79    /// Returns a borrow of the registered backend, walking the parent chain.
80    #[must_use]
81    pub fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
82        self.inner
83            .get(&TypeId::of::<T>())
84            .and_then(|b| b.downcast_ref::<T>())
85            .or_else(|| self.parent.as_deref()?.get::<T>())
86    }
87
88    /// Returns a borrow of the registered backend, panicking if absent.
89    /// Use for backends mandatory at engine startup.
90    pub fn require<T: Send + Sync + 'static>(&self) -> &T {
91        self.get::<T>().unwrap_or_else(|| {
92            panic!(
93                "Backends: required backend `{}` is not registered",
94                type_name::<T>()
95            )
96        })
97    }
98
99    /// Reports whether a backend of the given type is registered.
100    #[must_use]
101    pub fn contains<T: Send + Sync + 'static>(&self) -> bool {
102        self.inner.contains_key(&TypeId::of::<T>())
103            || self
104                .parent
105                .as_deref()
106                .map(|p| p.contains::<T>())
107                .unwrap_or(false)
108    }
109
110    /// Number of backends registered locally.
111    #[must_use]
112    pub fn len(&self) -> usize {
113        self.inner.len()
114    }
115
116    /// Whether no backends are registered locally.
117    #[must_use]
118    pub fn is_empty(&self) -> bool {
119        self.inner.is_empty()
120    }
121}
122
123impl std::fmt::Debug for Backends {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        f.debug_struct("Backends")
126            .field("registered", &self.inner.len())
127            .field("has_parent", &self.parent.is_some())
128            .finish()
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use std::sync::Mutex;
136
137    trait FakePhysics: Send + Sync {
138        fn name(&self) -> &str;
139    }
140    struct Rapier;
141    impl FakePhysics for Rapier {
142        fn name(&self) -> &str {
143            "rapier"
144        }
145    }
146
147    #[test]
148    fn register_and_lookup_trait_object() {
149        let mut b = Backends::new();
150        let provider: Arc<Mutex<Box<dyn FakePhysics>>> = Arc::new(Mutex::new(Box::new(Rapier)));
151        b.insert(provider);
152
153        let got = b.require::<Arc<Mutex<Box<dyn FakePhysics>>>>();
154        assert_eq!(got.lock().unwrap().name(), "rapier");
155    }
156
157    #[test]
158    fn require_panics_when_absent() {
159        let b = Backends::new();
160        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
161            let _ = b.require::<Arc<Mutex<Box<dyn FakePhysics>>>>();
162        }));
163        assert!(result.is_err());
164    }
165
166    #[test]
167    fn parent_chain_delegates() {
168        let mut parent = Backends::new();
169        let provider: Arc<Mutex<Box<dyn FakePhysics>>> = Arc::new(Mutex::new(Box::new(Rapier)));
170        parent.insert(provider);
171        let child = Backends::with_parent(Arc::new(parent));
172        assert!(child.contains::<Arc<Mutex<Box<dyn FakePhysics>>>>());
173    }
174}