pytauri_core/ext_mod_impl/
ipc.rs

1use std::{borrow::Cow, str::FromStr as _};
2
3use pyo3::{
4    exceptions::PyValueError,
5    intern,
6    prelude::*,
7    pybacked::{PyBackedBytes, PyBackedStr},
8    types::{PyBytes, PyDict, PyList, PyString, PyType},
9};
10use pyo3_utils::py_wrapper::{PyWrapper, PyWrapperT0, PyWrapperT2};
11use tauri::ipc::{self, CommandArg as _, CommandItem, InvokeBody, InvokeMessage};
12
13use crate::{
14    ext_mod::{
15        webview::{Webview, WebviewWindow},
16        PyAppHandleExt as _, StateManager,
17    },
18    tauri_runtime::Runtime,
19    utils::TauriError,
20};
21
22type IpcInvoke = tauri::ipc::Invoke<Runtime>;
23type IpcInvokeResolver = tauri::ipc::InvokeResolver<Runtime>;
24type TauriWebviewWindow = tauri::webview::WebviewWindow<Runtime>;
25type TauriInvokeResponseBody = tauri::ipc::InvokeResponseBody;
26
27// PERF, TODO: maybe we should use `downcast` to manually implement `FromPyObject`,
28// because `derive(FromPyObject)` will be based on `extract`,
29// which has higher overhead on errors
30#[derive(FromPyObject)]
31enum InvokeResponseBody {
32    // NOTE: Json appears more frequently, so we put it first.
33    Json(PyBackedStr),
34    // NOTE: use `Cow<[u8]>` instead of `Vec<u8>`,
35    // see: <https://github.com/PyO3/pyo3/issues/2888>
36    Raw(PyBackedBytes),
37}
38
39impl From<InvokeResponseBody> for TauriInvokeResponseBody {
40    fn from(value: InvokeResponseBody) -> Self {
41        match value {
42            InvokeResponseBody::Json(json) => TauriInvokeResponseBody::Json(json.to_owned()),
43            InvokeResponseBody::Raw(raw) => TauriInvokeResponseBody::Raw(raw.to_owned()),
44        }
45    }
46}
47
48/// Please refer to the Python-side documentation
49#[pyclass(frozen, generic)]
50#[non_exhaustive]
51pub struct InvokeResolver {
52    inner: PyWrapper<PyWrapperT2<IpcInvokeResolver>>,
53    #[pyo3(get)]
54    arguments: Py<PyDict>,
55}
56
57impl InvokeResolver {
58    #[inline]
59    fn new(resolver: IpcInvokeResolver, arguments: Py<PyDict>) -> Self {
60        Self {
61            inner: PyWrapper::new2(resolver),
62            arguments,
63        }
64    }
65}
66
67#[pymethods]
68// NOTE: These pymethods implementation must not block
69impl InvokeResolver {
70    fn resolve(&self, py: Python<'_>, value: InvokeResponseBody) -> PyResult<()> {
71        // NOTE: This function implementation must not block
72        py.allow_threads(|| {
73            let resolver = self.inner.try_take_inner()??;
74            resolver.resolve(TauriInvokeResponseBody::from(value));
75            Ok(())
76        })
77    }
78
79    // TODO: Support more Python types. Tauri seems to only support `serde` types,
80    // and not support `Raw: Vec<[u8]>`. We should open an issue to ask them about this.
81    //
82    // TODO, PERF: once we drop py39, we can use `value: &str` instead of `Cow<'_, str>`.
83    fn reject(&self, py: Python<'_>, value: Cow<'_, str>) -> PyResult<()> {
84        // NOTE: This function implementation must not block
85        py.allow_threads(|| {
86            let resolver = self.inner.try_take_inner()??;
87            resolver.reject(value);
88            Ok(())
89        })
90    }
91}
92
93/// Please refer to the Python-side documentation
94#[pyclass(frozen)]
95#[non_exhaustive]
96pub struct Invoke {
97    inner: PyWrapper<PyWrapperT2<IpcInvoke>>,
98    #[pyo3(get)]
99    command: Py<PyString>,
100}
101
102impl Invoke {
103    /// If the frontend makes an illegal IPC call, it will automatically reject and return [None]
104    #[cfg(feature = "__private")]
105    pub fn new(py: Python<'_>, invoke: IpcInvoke) -> Option<Self> {
106        let func_name = match Self::get_func_name_from_message(&invoke.message) {
107            Ok(name) => name,
108            Err(e) => {
109                invoke.resolver.reject(e);
110                return None;
111            }
112        };
113        // TODO, PERF: may be we should use [PyString::intern] ?
114        //     > However, for security reasons, since the input can be any string,
115        //     > unconditionally using [PyString::intern] will cause continuous memory growth issues.
116        //     > TODO, XXX: 👆 is this right?
117        let command = PyString::new(py, func_name).unbind();
118
119        let slf = Self {
120            inner: PyWrapper::new2(invoke),
121            command,
122        };
123        Some(slf)
124    }
125
126    const PYFUNC_HEADER_KEY: &str = "pyfunc";
127
128    #[inline]
129    fn get_func_name_from_message(message: &InvokeMessage<Runtime>) -> Result<&str, String> {
130        let func_name = message
131            .headers()
132            .get(Self::PYFUNC_HEADER_KEY)
133            .ok_or_else(|| format!("There is no {} header", Self::PYFUNC_HEADER_KEY))?
134            .to_str()
135            .map_err(|e| format!("{e}"))?;
136        Ok(func_name)
137    }
138}
139
140#[pymethods]
141// NOTE: These pymethods implementation must not block
142impl Invoke {
143    // NOTE: remember to use `pyo3::intern!` for performance,
144    // see: <https://github.com/PyO3/pyo3/discussions/2266#discussioncomment-2491646>.
145    const BODY_KEY: &str = "body";
146    const APP_HANDLE_KEY: &str = "app_handle";
147    const WEBVIEW_WINDOW_KEY: &str = "webview_window";
148    const HEADERS_KEY: &str = "headers";
149    const STATES_KEY: &str = "states";
150
151    /// Pass in a Python dictionary, which can contain the following
152    /// optional keys:
153    ///
154    /// - [Self::BODY_KEY] : [PyBytes]
155    /// - [Self::APP_HANDLE_KEY] : [crate::ext_mod::AppHandle]
156    /// - [Self::WEBVIEW_WINDOW_KEY] : [crate::ext_mod::webview::WebviewWindow]
157    /// - [Self::HEADERS_KEY] : `list[tuple[bytes, bytes]]`
158    /// - [Self::STATES_KEY] : `dict[str, type[Any]]`
159    ///
160    /// # Returns
161    ///
162    /// - On successful parsing of [Invoke], this function will set
163    ///     the corresponding types for the existing keys and return [InvokeResolver].
164    ///
165    ///     The return value [InvokeResolver::arguments] is not the same object as
166    ///     the input `parameters`.
167    /// - On failure, it returns [None], consumes and rejects [Invoke];
168    fn bind_to(&self, parameters: &Bound<'_, PyDict>) -> PyResult<Option<InvokeResolver>> {
169        // NOTE: This function implementation must not block
170
171        // see <https://docs.rs/tauri/2.1.1/tauri/ipc/trait.CommandArg.html#implementors>
172        // for how to parse the arguments
173
174        let py = parameters.py();
175        let invoke = self.inner.try_take_inner()??;
176        let IpcInvoke {
177            message,
178            resolver,
179            acl,
180        } = invoke;
181
182        let arguments = PyDict::new(py);
183
184        let body_key = intern!(py, Invoke::BODY_KEY);
185        if parameters.contains(body_key)? {
186            match message.payload() {
187                InvokeBody::Json(_) => {
188                    resolver.reject(
189                        "Please use `ArrayBuffer` or `Uint8Array` for `InvokeBody::Raw`. \
190                        If you are using `pyInvoke`, please report this as bug to pytauri developers.",
191                    );
192                    return Ok(None);
193                }
194                InvokeBody::Raw(body) => arguments.set_item(body_key, PyBytes::new(py, body))?,
195            }
196        }
197
198        let app_handle_key = intern!(py, Invoke::APP_HANDLE_KEY);
199        if parameters.contains(app_handle_key)? {
200            let py_app_handle = message.webview_ref().try_py_app_handle()?;
201            arguments.set_item(app_handle_key, py_app_handle)?;
202        }
203
204        let webview_window_key = intern!(py, Invoke::WEBVIEW_WINDOW_KEY);
205        if parameters.contains(webview_window_key)? {
206            let command_webview_window_item = CommandItem {
207                plugin: None,
208                name: "__whatever__pyfunc",
209                key: "__whatever__webviewWindow",
210                message: &message,
211                acl: &acl,
212            };
213            // TODO, PERF: maybe we should release the GIL here?
214            let webview_window = match TauriWebviewWindow::from_command(command_webview_window_item)
215            {
216                Ok(webview_window) => webview_window,
217                Err(e) => {
218                    resolver.invoke_error(e);
219                    return Ok(None);
220                }
221            };
222            arguments.set_item(webview_window_key, WebviewWindow::new(webview_window))?;
223        }
224
225        let headers_key = intern!(py, Invoke::HEADERS_KEY);
226        if parameters.contains(headers_key)? {
227            let headers: Vec<(&[u8], &[u8])> = message
228                .headers()
229                // PERF:
230                // > Each key will be yielded once per associated value.
231                // > So, if a key has 3 associated values, it will be yielded 3 times.
232                //
233                // This means the same key may generate multiple PyBytes objects
234                // (although this is consistent with the Python `h11` implementation).
235                // We need to use [HeaderMap::into_iter] to improve this (but this requires ownership, need a Tauri feature request):
236                // when get [None], we only need to clone the previous PyBytes.
237                .iter()
238                // PERF: Perhaps we don't need to filter out [PYFUNC_HEADER_KEY], just pass it to Python as is.
239                //
240                // TODO: Ideally, we should use [HeaderMap::remove] in [Self::get_func_name_from_message]
241                // to pop [PYFUNC_HEADER_KEY], but currently, we cannot obtain ownership/mutable reference
242                // of `headers` from `invoke`. We should submit a feature request to Tauri.
243                .filter(|(key, _)| **key != Self::PYFUNC_HEADER_KEY)
244                .map(|(key, value)| (key.as_ref(), value.as_bytes()))
245                .collect();
246            // TODO: Unify and export this type in [crate::ext_mod::ipc], see Python [pytauri.ipc.Headers] type.
247            let py_headers = PyList::new(py, headers)?;
248            arguments.set_item(headers_key, py_headers)?;
249        }
250
251        let states_key = intern!(py, Invoke::STATES_KEY);
252        if let Some(states_params) = parameters.get_item(states_key)? {
253            let states_params = states_params.downcast_into::<PyDict>()?;
254            // TODO, PERF: benchmark `PyDict::copy` vs `PyDict::new` vs `PyDict::from_sequence`.
255            let states_args = PyDict::new(py);
256            let state_manager = StateManager::get_or_init(py, message.webview_ref());
257
258            // TODO, PERF: use `BorrowedDictIterator` in the future (not implemented in pyo3 yet).
259            for (key, value) in states_params.into_iter() {
260                let state_type = value.downcast::<PyType>()?;
261                if let Some(state) = state_manager.try_state(py, state_type)? {
262                    states_args.set_item(key, state)?;
263                } else {
264                    resolver.reject(format!(
265                        "state `{state_type}` not managed for field `{key}`. \
266                        You must call `.manage()` before using this command"
267                    ));
268                    return Ok(None);
269                }
270            }
271            arguments.set_item(states_key, states_args)?;
272        }
273
274        Ok(Some(InvokeResolver::new(resolver, arguments.unbind())))
275    }
276
277    fn resolve(&self, py: Python<'_>, value: InvokeResponseBody) -> PyResult<()> {
278        // NOTE: This function implementation must not block
279
280        py.allow_threads(|| {
281            let resolver = self.inner.try_take_inner()??.resolver;
282            resolver.resolve(TauriInvokeResponseBody::from(value));
283            Ok(())
284        })
285    }
286
287    // TODO: Support more Python types. Tauri seems to only support `serde` types,
288    // and not support `Raw: Vec<[u8]>`. We should open an issue to ask them about this.
289    //
290    // TODO, PERF: once we drop py39, we can use `value: &str` instead of `Cow<'_, str>`.
291    fn reject(&self, py: Python<'_>, value: Cow<'_, str>) -> PyResult<()> {
292        // NOTE: This function implementation must not block
293
294        py.allow_threads(|| {
295            let resolver = self.inner.try_take_inner()??.resolver;
296            resolver.reject(value);
297            Ok(())
298        })
299    }
300}
301
302/// See also: [tauri::ipc::JavaScriptChannelId]
303#[pyclass(frozen)]
304#[non_exhaustive]
305pub struct JavaScriptChannelId(PyWrapper<PyWrapperT0<ipc::JavaScriptChannelId>>);
306
307impl JavaScriptChannelId {
308    fn new(js_channel_id: ipc::JavaScriptChannelId) -> Self {
309        Self(PyWrapper::new0(js_channel_id))
310    }
311}
312
313#[pymethods]
314impl JavaScriptChannelId {
315    #[staticmethod]
316    fn from_str(py: Python<'_>, value: &str) -> PyResult<Self> {
317        // PERF: it's short enough, so we don't release the GIL
318        let result = ipc::JavaScriptChannelId::from_str(value);
319        match result {
320            Ok(js_channel_id) => Ok(Self::new(js_channel_id)),
321            Err(err) => {
322                let msg: &'static str = err;
323                // because the `err` is `static`, so we use `PyString::intern`.
324                // TODO, PERF: maybe we can just use `pyo3::intern!("failed to parse JavaScriptChannelId")`.
325                let msg = PyString::intern(py, msg).unbind();
326                Err(PyValueError::new_err(msg))
327            }
328        }
329    }
330
331    /// PERF, TODO: maybe we should accept `Union[Webview, WebviewWindow]`,
332    /// so that user dont need create new `Webview` pyobject for `WebviewWindow`.
333    fn channel_on(&self, py: Python<'_>, webview: Py<Webview>) -> Channel {
334        py.allow_threads(|| {
335            let js_channel_id = self.0.inner_ref();
336            let webview = webview.get().0.inner_ref().clone();
337            // TODO, FIXME, PERF:
338            // Why [JavaScriptChannelId::channel_on] need take the ownership of [Webview]?
339            // We should ask tauri developers.
340            let channel = js_channel_id.channel_on(webview); // maybe block, so we release the GIL
341            Channel::new(channel)
342        })
343    }
344}
345
346/// See also: [tauri::ipc::Channel]
347#[pyclass(frozen)]
348#[non_exhaustive]
349pub struct Channel(PyWrapper<PyWrapperT0<ipc::Channel>>);
350
351impl Channel {
352    fn new(channel: ipc::Channel) -> Self {
353        Self(PyWrapper::new0(channel))
354    }
355}
356
357#[pymethods]
358impl Channel {
359    fn id(&self) -> u32 {
360        self.0.inner_ref().id()
361    }
362
363    fn send(&self, py: Python<'_>, data: InvokeResponseBody) -> PyResult<()> {
364        // [tauri::ipc::Channel::send] is not a very fast operation,
365        // so we need to release the GIL
366        py.allow_threads(|| {
367            self.0
368                .inner_ref()
369                .send(TauriInvokeResponseBody::from(data))
370                .map_err(TauriError::from)?;
371            Ok(())
372        })
373    }
374}
375
376// You can enable this comment and expand the macro
377// with rust-analyzer to understand how tauri implements IPC
378/*
379#[expect(unused_variables)]
380#[expect(dead_code)]
381#[expect(unused_imports)]
382mod foo {
383    use super::*;
384
385    use tauri::ipc::{Channel, CommandScope, GlobalScope, InvokeResponseBody, Request, Response};
386
387    #[tauri::command]
388    #[expect(clippy::too_many_arguments)]
389    async fn foo(
390        request: Request<'_>,
391        command_scope: CommandScope<String>,
392        global_scope: GlobalScope<String>,
393        app_handle: tauri::AppHandle,
394        webview: tauri::Webview,
395        webview_window: tauri::WebviewWindow,
396        window: tauri::Window,
397        channel: Channel<InvokeResponseBody>,
398        state: tauri::State<'_, String>,
399    ) -> Result<Response, String> {
400        Ok(Response::new(InvokeResponseBody::Raw(Vec::new())))
401    }
402
403    fn bar() {
404        let _ = tauri::Builder::new().invoke_handler(tauri::generate_handler![foo]);
405    }
406}
407 */