compo_platform_loop/
loop.rs

1//! Platform-specific event loop implementations for the Compo framework.
2//!
3//! This module provides cross-platform event loop integration that allows Compo
4//! applications to run natively on different operating systems. Each platform
5//! uses its native event loop mechanism:
6//!
7//! - **Windows**: Win32 message loop with PeekMessage for non-blocking processing
8//! - **macOS**: NSApplication with NSRunLoop and NSTimer for periodic polling
9//! - **iOS**: NSRunLoop with NSTimer for periodic polling (without NSApplication)
10//! - **Android**: JNI integration with Java MainLoop for Android event system
11//!
12//! The module exports platform-appropriate `run` functions that initialize the
13//! Compo runtime and integrate it with the platform's native event loop.
14
15use compo::prelude::*;
16#[cfg(windows)]
17use windows::Win32::UI::WindowsAndMessaging::{
18    DispatchMessageW, MSG, PM_REMOVE, PeekMessageW, TranslateMessage, WM_QUIT,
19};
20#[cfg(target_os = "ios")]
21use {
22    block2::RcBlock,
23    objc2::{ClassType, msg_send},
24    objc2_foundation::{NSRunLoop, NSString, NSTimer},
25};
26#[cfg(target_os = "android")]
27use {
28    jni::{AttachGuard, JNIEnv, JavaVM, NativeMethod, errors::Result as JniResult},
29    std::{
30        any::Any,
31        cell::{Cell, RefCell},
32        ptr::null_mut,
33    },
34    tracing::error,
35};
36
37#[cfg(target_os = "macos")]
38use {
39    block2::RcBlock,
40    objc2::{ClassType, msg_send},
41    objc2_foundation::{NSRunLoop, NSString, NSTimer},
42};
43
44// Thread-local storage for Android runtime and component management.
45//
46// On Android, we use thread-local storage to maintain the Compo runtime and
47// component instances. This is necessary because Android's JNI callbacks
48// need access to the runtime from the same thread where it was created.
49#[cfg(target_os = "android")]
50thread_local! {
51    /// The Compo runtime instance for processing async tasks
52    static RT: Rc<Runtime<'static, ()>> = Rc::new(Runtime::new());
53    /// Storage for the root component instance
54    static COMPONENT: Cell<Rc<dyn Any>> = Cell::new(Rc::new(()));
55    /// Storage for the JavaVM instance to allow JNI calls from any thread
56    ///
57    /// This is used to store the JavaVM instance obtained during JNI_OnLoad
58    /// so that we can attach threads to the JVM when needed for callbacks.
59    /// The RefCell allows mutable access to the JavaVM instance when attaching
60    /// new threads or performing JNI operations.
61    static JAVA_VM: RefCell<JniResult<JavaVM>> = RefCell::new(unsafe { JavaVM::from_raw(null_mut()) });
62}
63
64/// Handles Windows message processing for the event loop.
65///
66/// This function processes Windows messages using PeekMessage instead of GetMessage
67/// to avoid blocking the event loop. It handles WM_QUIT messages for graceful shutdown
68/// and dispatches other messages to their appropriate window procedures.
69///
70/// # Arguments
71/// * `r#loop` - Reference to the Loop instance for controlling the event loop
72#[cfg(windows)]
73fn handle_windows_message(r#loop: &Loop) {
74    // Use PeekMessage instead of GetMessage because GetMessage blocks until a message is available
75    unsafe {
76        let mut msg = MSG::default();
77        // Check if there are messages in the queue without blocking
78        while PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE).as_bool() {
79            // If it's a WM_QUIT message, exit the loop
80            if msg.message == WM_QUIT {
81                r#loop.quit();
82                break;
83            }
84
85            // Translate virtual key messages
86            let _ = TranslateMessage(&msg);
87            // Dispatch message to window procedure
88            DispatchMessageW(&msg);
89        }
90    }
91}
92
93/// Runs the platform-specific event loop with the given entry component.
94///
95/// This function initializes and starts the appropriate event loop for the current platform:
96/// - **Windows**: Uses Win32 message loop with PeekMessage for non-blocking message processing
97/// - **macOS**: Uses NSApplication with NSRunLoop and NSTimer for periodic runtime polling
98/// - **iOS**: Uses NSRunLoop with NSTimer for periodic runtime polling (without NSApplication)
99///
100/// The function creates a Compo runtime, spawns the entry component as an async task,
101/// and integrates with the platform's native event loop to ensure proper execution
102/// of async components.
103///
104/// # Type Parameters
105/// * `C` - The component type that implements `Component<'a>`
106/// * `F` - The async function type that takes a `Weak<C>` and returns a future
107///
108/// # Arguments
109/// * `entry` - The entry point async function that will be executed as the root component
110///
111/// # Platform-specific behavior
112/// - **Windows**: Registers a message handler and runs the Loop with Windows message processing
113/// - **macOS**: Creates NSApplication, sets up a timer for runtime polling, and runs the app loop
114/// - **iOS**: Sets up NSRunLoop with a timer for runtime polling (suitable for iOS apps)
115///
116/// # Examples
117/// ```rust
118/// //! Platform-specific event loop implementations for the Compo framework.
119/// //!
120/// //! This module provides cross-platform event loop integration that allows Compo
121/// //! applications to run natively on different operating systems. Each platform
122/// //! uses its native event loop mechanism:
123/// //!
124/// //! - **Windows**: Win32 message loop with PeekMessage for non-blocking processing
125/// //! - **macOS**: NSApplication with NSRunLoop and NSTimer for periodic polling
126/// //! - **iOS**: NSRunLoop with NSTimer for periodic polling (without NSApplication)
127/// //! - **Android**: JNI integration with Java MainLoop for Android event system
128/// //!
129/// //! The module exports platform-appropriate `run` functions that initialize the
130/// //! Compo runtime and integrate it with the platform's native event loop.
131///
132/// use compo::prelude::*;
133/// use compo_platform_loop::prelude::run;
134///
135/// #[component]
136/// async fn app() {
137///     println!("Hello from Compo!");
138/// }
139///
140/// fn main() {
141///     run(app);
142/// }
143/// ```
144#[cfg(not(target_os = "android"))]
145pub fn run<'a, C, F>(entry: F)
146where
147    C: Component<'a> + 'a,
148    F: AsyncFn(Weak<C>) + 'a,
149{
150    #[cfg(windows)]
151    Loop::new()
152        .register_poll_handler(handle_windows_message)
153        .run(entry);
154
155    #[cfg(target_os = "ios")]
156    {
157        // 创建运行时和组件
158        let rt = Rc::new(Runtime::new());
159        let rt_weak = Rc::downgrade(&rt);
160        let c = Rc::new(C::new(rt_weak.clone()));
161        let c_weak = Rc::downgrade(&c);
162
163        // 启动异步任务
164        rt.spawn(async move { entry(c_weak).await });
165
166        // 获取主线程的运行循环
167        let run_loop = NSRunLoop::mainRunLoop();
168
169        // 创建一个定时器,用于定期轮询 Runtime
170        let poll_block = RcBlock::new(move || {
171            // 轮询 Runtime 以推进异步任务
172            if let Some(rt) = rt_weak.upgrade() {
173                rt.poll_all();
174            }
175        });
176
177        // 创建一个重复的定时器,每 0.01 秒轮询一次
178        let timer: *mut NSTimer = unsafe {
179            msg_send![NSTimer::class(),
180                scheduledTimerWithTimeInterval: 0.01,
181                repeats: true,
182                block: &*poll_block
183            ]
184        };
185
186        // 将定时器添加到运行循环中
187        let mode = NSString::from_str("NSDefaultRunLoopMode");
188        let _: () = unsafe { msg_send![&run_loop, addTimer: timer, forMode: &*mode] };
189
190        // 运行主循环,这会阻塞当前线程
191        #[cfg(not(feature = "application"))]
192        run_loop.run();
193        #[cfg(feature = "application")]
194        if let Some(mtm) = objc2::MainThreadMarker::new() {
195            objc2_ui_kit::UIApplication::main(None, None, mtm)
196        } else {
197            tracing::error!("Can't run on non-main-thread.")
198        }
199    }
200
201    #[cfg(target_os = "macos")]
202    {
203        // 创建运行时和组件
204        let rt = Rc::new(Runtime::new());
205        let rt_weak = Rc::downgrade(&rt);
206        let c = Rc::new(C::new(rt_weak.clone()));
207        let c_weak = Rc::downgrade(&c);
208
209        // 启动异步任务
210        rt.spawn(async move { entry(c_weak).await });
211
212        // 获取主线程的运行循环
213        let run_loop = NSRunLoop::mainRunLoop();
214
215        // 创建一个定时器,用于定期轮询 Runtime
216        let poll_block = RcBlock::new(move || {
217            // 轮询 Runtime 以推进异步任务
218            if let Some(rt) = rt_weak.upgrade() {
219                rt.poll_all();
220            }
221        });
222
223        // 创建一个重复的定时器,每 0.01 秒轮询一次
224        let timer: *mut NSTimer = unsafe {
225            msg_send![NSTimer::class(),
226                scheduledTimerWithTimeInterval: 0.01,
227                repeats: true,
228                block: &*poll_block
229            ]
230        };
231
232        // 将定时器添加到运行循环中
233        // 创建 NSDefaultRunLoopMode 字符串
234        let mode = NSString::from_str("NSDefaultRunLoopMode");
235        let _: () = unsafe { msg_send![&run_loop, addTimer: timer, forMode: &*mode] };
236
237        #[cfg(not(feature = "application"))]
238        run_loop.run();
239        #[cfg(feature = "application")]
240        if let Some(mtm) = objc2::MainThreadMarker::new() {
241            let app = objc2_app_kit::NSApplication::sharedApplication(mtm);
242            app.activate();
243
244            // 运行应用程序主循环,这会阻塞当前线程
245            app.run();
246        } else {
247            tracing::error!("Can't run on non-main-thread.")
248        }
249    }
250}
251
252/// Runs the Android-specific event loop with JNI integration.
253///
254/// This function sets up the event loop for Android applications using JNI (Java Native Interface).
255/// It creates a Compo runtime in thread-local storage, spawns the entry component, and integrates
256/// with the Android Java MainLoop class for proper event loop execution.
257///
258/// The function registers a native method `poll_all` that can be called from Java to advance
259/// the async runtime, enabling proper integration with Android's event system.
260///
261/// # Type Parameters
262/// * `C` - The component type that implements `Component<'static>` (must be 'static for Android)
263/// * `F` - The async function type that takes a `Weak<C>` and returns a future (must be 'static)
264///
265/// # Arguments
266/// * `vm` - The JavaVM instance provided by the Android runtime
267/// * `entry` - The entry point async function that will be executed as the root component
268///
269/// # Android Integration
270/// This function:
271/// 1. Creates a thread-local Compo runtime and component
272/// 2. Spawns the entry component as an async task
273/// 3. Calls the Java `MainLoop.run()` method to start the Android event loop
274/// 4. Registers the native `poll_all` method for runtime advancement
275///
276/// # Examples
277/// ```rust
278/// //! Platform-specific event loop implementations for the Compo framework.
279/// //!
280/// //! This module provides cross-platform event loop integration that allows Compo
281/// //! applications to run natively on different operating systems. Each platform
282/// //! uses its native event loop mechanism:
283/// //!
284/// //! - **Windows**: Win32 message loop with PeekMessage for non-blocking processing
285/// //! - **macOS**: NSApplication with NSRunLoop and NSTimer for periodic polling
286/// //! - **iOS**: NSRunLoop with NSTimer for periodic polling (without NSApplication)
287/// //! - **Android**: JNI integration with Java MainLoop for Android event system
288/// //!
289/// //! The module exports platform-appropriate `run` functions that initialize the
290/// //! Compo runtime and integrate it with the platform's native event loop.
291///
292/// use compo::prelude::*;
293/// use compo_platform_loop::prelude::run;
294/// use jni::JavaVM;
295///
296/// #[component]
297/// async fn app() {
298///     println!("Hello from Android!");
299/// }
300///
301/// fn main(vm: JavaVM) {
302///     run(vm, app);
303/// }
304/// ```
305#[cfg(target_os = "android")]
306pub fn run<C, F>(vm: JavaVM, entry: F)
307where
308    C: Component<'static> + 'static,
309    F: AsyncFn(Weak<C>) + 'static,
310{
311    JAVA_VM.set(Ok(vm));
312    RT.with(|rt| {
313        let rt_weak = Rc::downgrade(rt);
314        let c = Rc::new(C::new(rt_weak.clone()));
315        let c_weak = Rc::downgrade(&c);
316
317        // 启动异步任务
318        rt.spawn(async move { entry(c_weak).await });
319        COMPONENT.set(c);
320    });
321    vm_exec(|mut env| {
322        const CLASS: &str = "rust/compo/MainLoop";
323        if let Err(e) = env.call_static_method(CLASS, "run", "()V", &[]) {
324            error!(?e, "Run failed.");
325        }
326        let method = NativeMethod {
327            name: "poll_all".into(),
328            sig: "()V".into(),
329            fn_ptr: poll_all as *mut _,
330        };
331        if let Err(e) = env.register_native_methods(CLASS, &[method]) {
332            error!(?e, "Register native method failed.");
333        }
334    });
335}
336
337/// Native method called from Java to advance the Compo runtime.
338///
339/// This function is registered as a JNI native method and called by the Android
340/// Java MainLoop to poll and advance all pending async tasks in the Compo runtime.
341/// It accesses the thread-local runtime and calls `poll_all()` to process any
342/// ready futures.
343///
344/// # Safety
345/// This function is marked as `unsafe` because it's a C-style callback function
346/// that will be called from Java via JNI. The JNI environment parameter is
347/// currently unused but required by the JNI interface.
348///
349/// # Arguments
350/// * `_env` - The JNI environment (unused in current implementation)
351#[cfg(target_os = "android")]
352unsafe extern "C" fn poll_all(_env: JNIEnv) {
353    RT.with(|rt| rt.poll_all());
354}
355
356/// Executes a closure with an attached JNI environment.
357///
358/// This function provides thread-safe access to the Java VM environment by:
359/// 1. Borrowing the stored JavaVM instance
360/// 2. Attaching the current thread to the JVM
361/// 3. Executing the provided closure with the attached environment
362///
363/// # Type Parameters
364/// * `F` - Closure type that takes an `AttachGuard` (must be thread-safe)
365///
366/// # Safety
367/// The closure must not perform any operations that could cause thread-local
368/// data corruption or violate JNI safety rules.
369///
370/// # Error Handling
371/// Logs errors if:
372/// - JavaVM is not initialized (must call `run` first)
373/// - Thread attachment fails
374#[cfg(target_os = "android")]
375pub fn vm_exec<F>(f: F)
376where
377    F: for<'a> FnOnce(AttachGuard<'a>),
378{
379    JAVA_VM.with_borrow_mut(move |vm| match vm {
380        Ok(vm) => match vm.attach_current_thread() {
381            Ok(env) => f(env),
382            Err(e) => error!(?e, "Can't attach current thread."),
383        },
384        Err(e) => error!(?e, "Java VM is not initialized, please call the `run` function and set the correct JavaVM first."),
385    })
386}