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}