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