godot_testability_runtime/
runtime.rs1use crate::error::{TestError, TestResult};
8use once_cell::sync::Lazy;
9use parking_lot::Mutex;
10use std::sync::Arc;
11use tracing::{error, info};
12
13type GDExtensionInterfaceGetProcAddress = *const std::ffi::c_void;
15type GDExtensionClassLibraryPtr = *mut std::ffi::c_void;
16type SceneTreePtr = *mut std::ffi::c_void;
17
18static USER_CALLBACKS: Mutex<Option<UserCallbacks>> = Mutex::new(None);
20
21#[allow(clippy::type_complexity)]
23static SCENE_CALLBACK: Mutex<Option<Box<dyn FnOnce(SceneTreePtr) -> TestResult<()> + Send>>> =
24 Mutex::new(None);
25
26pub struct UserCallbacks {
28 pub initialize_ffi:
30 fn(GDExtensionInterfaceGetProcAddress, GDExtensionClassLibraryPtr) -> Result<(), String>,
31 pub load_class_method_table: fn(u32),
33 pub register_classes: Option<fn(u32)>,
35}
36
37static RUNTIME_STATE: Lazy<Arc<Mutex<RuntimeState>>> =
39 Lazy::new(|| Arc::new(Mutex::new(RuntimeState::new())));
40
41#[derive(Debug)]
43struct RuntimeState {
44 initialized: bool,
45 running: bool,
46}
47
48impl RuntimeState {
49 fn new() -> Self {
50 Self {
51 initialized: false,
52 running: false,
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct RuntimeConfig {
60 pub headless: bool,
62 pub verbose: bool,
64 pub custom_args: Vec<String>,
66}
67
68impl Default for RuntimeConfig {
69 fn default() -> Self {
70 Self {
71 headless: true,
72 verbose: false,
73 custom_args: Vec::new(),
74 }
75 }
76}
77
78pub struct GodotRuntime;
84
85#[cfg(feature = "embedded_runtime")]
86impl GodotRuntime {
87 pub fn is_initialized() -> bool {
89 RUNTIME_STATE.lock().initialized
90 }
91
92 pub fn is_running() -> bool {
94 RUNTIME_STATE.lock().running
95 }
96
97 pub fn shutdown() -> TestResult<()> {
101 let mut state = RUNTIME_STATE.lock();
102 if !state.initialized {
103 return Ok(());
104 }
105
106 info!("Shutting down Godot runtime");
107 state.running = false;
108 state.initialized = false;
109 Ok(())
110 }
111
112 pub fn run_godot<F>(
117 _config: RuntimeConfig,
118 callbacks: UserCallbacks,
119 load_scene: F,
120 ) -> TestResult<i32>
121 where
122 F: FnOnce(SceneTreePtr) -> TestResult<()> + Send + 'static,
123 {
124 use crate::ffi::{
125 libgodot_gdextension_bind, GDExtensionClassLibraryPtr, GDExtensionInitialization,
126 GDExtensionInitializationLevel, GDExtensionInterfaceGetProcAddress,
127 };
128 use std::ffi::c_void;
129
130 info!("Starting Godot runtime with SwiftGodot-style initialization");
131
132 {
134 USER_CALLBACKS.lock().replace(callbacks);
135 SCENE_CALLBACK.lock().replace(Box::new(load_scene));
136 }
137
138 extern "C" fn initialization_callback(
139 get_proc_addr: Option<GDExtensionInterfaceGetProcAddress>,
140 library: GDExtensionClassLibraryPtr,
141 r_initialization: *mut GDExtensionInitialization,
142 ) -> i32 {
143 if get_proc_addr.is_none() || library.is_null() {
144 return 0;
145 }
146
147 unsafe {
148 if !r_initialization.is_null() {
149 (*r_initialization).minimum_initialization_level =
150 GDExtensionInitializationLevel::Core;
151 (*r_initialization).userdata = library;
152 (*r_initialization).initialize = Some(godot_rust_bridge_initialize);
153 (*r_initialization).deinitialize = Some(godot_rust_bridge_deinitialize);
154 }
155
156 if let Some(callbacks) = USER_CALLBACKS.lock().as_ref() {
158 if let Some(get_proc_address_fn) = get_proc_addr {
159 if let Err(e) = (callbacks.initialize_ffi)(
160 get_proc_address_fn as *const c_void,
161 library,
162 ) {
163 error!("Failed to initialize godot-rust FFI: {}", e);
164 return 0;
165 }
166 }
167 }
168 }
169 1
170 }
171
172 extern "C" fn scene_callback(scene_tree_ptr: *mut c_void) {
173 info!("Scene tree ready - Godot engine available");
174 if !scene_tree_ptr.is_null() {
175 if let Some(callback) = SCENE_CALLBACK.lock().take() {
176 info!("Executing test callback with SceneTree pointer");
177 match callback(scene_tree_ptr) {
178 Ok(()) => {
179 info!("Test callback executed successfully");
180 }
181 Err(e) => {
182 error!("Test callback failed: {:?}", e);
183 }
184 }
185 } else {
186 info!("No test callback to execute");
187 }
188 } else {
189 error!("Scene tree pointer is null!");
190 }
191 }
192
193 unsafe {
194 libgodot_gdextension_bind(initialization_callback, Some(scene_callback));
195 }
196
197 std::env::set_var("__CFBundleIdentifier", "GodotBevyKit");
198
199 let args = vec![
200 "GodotBevyKit".to_string(),
201 "--headless".to_string(),
202 "--verbose".to_string(),
203 ];
204
205 let mut runtime = crate::ffi::LibgodotRuntime::new();
206 runtime
207 .initialize()
208 .map_err(TestError::RuntimeInitialization)?;
209
210 info!("Starting godot_main");
211 let result = runtime
212 .run_main(&args)
213 .map_err(TestError::RuntimeInitialization)?;
214
215 info!("Godot main loop finished with exit code: {}", result);
216 Ok(result)
217 }
218}
219
220extern "C" fn godot_rust_bridge_initialize(
221 _userdata: *mut std::ffi::c_void,
222 level: crate::ffi::GDExtensionInitializationLevel,
223) {
224 info!("Godot-Rust bridge initialize (level: {:?})", level);
225
226 let init_level = match level {
228 crate::ffi::GDExtensionInitializationLevel::Core => 0,
229 crate::ffi::GDExtensionInitializationLevel::Servers => 1,
230 crate::ffi::GDExtensionInitializationLevel::Scene => 2,
231 crate::ffi::GDExtensionInitializationLevel::Editor => 3,
232 _ => return,
233 };
234
235 if let Some(callbacks) = USER_CALLBACKS.lock().as_ref() {
237 (callbacks.load_class_method_table)(init_level);
238
239 if let Some(register_classes) = callbacks.register_classes {
241 register_classes(init_level);
242 }
243 }
244}
245
246extern "C" fn godot_rust_bridge_deinitialize(
247 _userdata: *mut std::ffi::c_void,
248 level: crate::ffi::GDExtensionInitializationLevel,
249) {
250 info!("Godot-Rust bridge deinitialize (level: {:?})", level);
251}