pytauri_core/ext_mod_impl/lib/
app.rs

1use pyo3::prelude::*;
2use pyo3_utils::{
3    py_wrapper::{PyWrapper, PyWrapperT2},
4    ungil::UnsafeUngilExt,
5};
6
7use crate::{
8    ext_mod::{
9        debug_assert_app_handle_py_is_rs, AppHandle, PyAppHandleExt as _, RunEvent, TauriAppHandle,
10    },
11    tauri_runtime::Runtime,
12    utils::{PyResultExt as _, TauriError},
13};
14
15pub(crate) type TauriApp = tauri::App<Runtime>;
16
17#[pyclass(frozen, unsendable)]
18#[non_exhaustive]
19pub struct App(pub PyWrapper<PyWrapperT2<TauriApp>>);
20
21impl App {
22    #[cfg(feature = "__private")]
23    pub fn try_build(py: Python<'_>, app: TauriApp) -> PyResult<Self> {
24        // remember to initialize the global singleton [PyAppHandle], see it's doc
25        app.get_or_init_py_app_handle(py)?;
26        Ok(Self(PyWrapper::new2(app)))
27    }
28
29    fn py_cb_to_rs_cb(
30        callback: PyObject,
31        app_handle: Py<AppHandle>,
32    ) -> impl FnMut(&TauriAppHandle, tauri::RunEvent) {
33        move |_app_handle, run_event| {
34            let py_app_handle: &Py<AppHandle> = &app_handle;
35            debug_assert_app_handle_py_is_rs(&app_handle, _app_handle);
36
37            Python::with_gil(|py| {
38                let py_run_event: RunEvent = RunEvent::from_tauri(py, run_event)
39                    // TODO: maybe we should only `write_unraisable` and log it instead of `panic` here?
40                    .expect("Failed to convert rust `RunEvent` to pyobject");
41
42                let callback = callback.bind(py);
43                let result = callback.call1((py_app_handle, py_run_event));
44                // `panic` allows Python to exit `app.run()`,
45                // otherwise the Python main thread will be blocked by `app.run()`
46                // and unable to raise an error.
47                result.unwrap_unraisable_py_result(py, Some(callback), || {
48                    "Python exception occurred in `App` run callback"
49                });
50            })
51        }
52    }
53
54    fn noop_callback(_: &TauriAppHandle, _: tauri::RunEvent) {}
55}
56
57#[pymethods]
58impl App {
59    fn run_on_main_thread(&self, py: Python<'_>, handler: PyObject) -> PyResult<()> {
60        unsafe {
61            // TODO, PREF: do we really need to release the GIL here?
62            // It seems that `App::run_on_main_thread` will be called immediately.
63            //
64            // Safety: `tauri::App` does not hold the GIL, so this is safe
65            py.allow_threads_unsend(self, |slf| {
66                let app = slf.0.try_lock_inner_ref()??;
67                app.run_on_main_thread(move || {
68                    Python::with_gil(|py| {
69                        let handler = handler.bind(py);
70                        let result = handler.call0();
71                        result.unwrap_unraisable_py_result(py, Some(handler), || {
72                            "Python exception occurred in `App::run_on_main_thread`"
73                        });
74                    })
75                })
76                .map_err(TauriError::from)
77                .map_err(PyErr::from)
78            })
79        }
80    }
81
82    fn handle(&self, py: Python<'_>) -> PyResult<Py<AppHandle>> {
83        let app = self.0.try_lock_inner_ref()??;
84        // TODO, PERF: release the GIL?
85        let app_handle = app.py_app_handle().clone_ref(py);
86        Ok(app_handle)
87    }
88
89    #[pyo3(signature = (callback = None, /))]
90    fn run(&self, py: Python<'_>, callback: Option<PyObject>) -> PyResult<()> {
91        let app = self.0.try_take_inner()??;
92        let py_app_handle = app.py_app_handle().clone_ref(py);
93        unsafe {
94            // Safety: `tauri::App` does not hold the GIL, so this is safe
95            py.allow_threads_unsend(app, move |app| {
96                match callback {
97                    Some(callback) => app.run(Self::py_cb_to_rs_cb(callback, py_app_handle)),
98                    None => app.run(Self::noop_callback),
99                }
100                Ok(())
101            })
102        }
103    }
104
105    #[pyo3(signature = (callback = None, /))]
106    fn run_return(&self, py: Python<'_>, callback: Option<PyObject>) -> PyResult<i32> {
107        let app = self.0.try_take_inner()??;
108        let py_app_handle = app.py_app_handle().clone_ref(py);
109        unsafe {
110            // Safety: `tauri::App` does not hold the GIL, so this is safe
111            py.allow_threads_unsend(app, move |app| {
112                let exit_code = match callback {
113                    Some(callback) => app.run_return(Self::py_cb_to_rs_cb(callback, py_app_handle)),
114                    None => app.run_return(Self::noop_callback),
115                };
116                Ok(exit_code)
117            })
118        }
119    }
120
121    #[expect(deprecated)]
122    #[pyo3(signature = (callback = None, /))]
123    fn run_iteration(&self, py: Python<'_>, callback: Option<PyObject>) -> PyResult<()> {
124        let app = self.0.try_lock_inner_mut()??;
125        let py_app_handle = app.py_app_handle().clone_ref(py);
126        unsafe {
127            // Safety: `&mut tauri::App` does not hold the GIL, so this is safe
128            py.allow_threads_unsend(app, |mut app| {
129                match callback {
130                    Some(callback) => {
131                        app.run_iteration(Self::py_cb_to_rs_cb(callback, py_app_handle))
132                    }
133                    None => app.run_iteration(Self::noop_callback),
134                }
135                Ok(())
136            })
137        }
138    }
139
140    fn cleanup_before_exit(&self, py: Python<'_>) -> PyResult<()> {
141        unsafe {
142            // Safety: `self: &App` does not hold the GIL, so this is safe
143            py.allow_threads_unsend(self, |slf| {
144                let app = slf.0.try_lock_inner_ref()??;
145                app.cleanup_before_exit();
146                Ok(())
147            })
148        }
149    }
150}