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 */