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