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