Repository: tauri-apps/tauri (language: rust)

Issue:
[feat] Emit json string
### Describe the problem

I am implementing `pytauri` Python bindings for the [Emitter](https://docs.rs/tauri/latest/tauri/trait.Emitter.html) trait, but currently, the `Emitter` API is blocking me. Specifically:

In pytauri, the way we handle IPC between Python and the frontend is:

1. On the Python side, we serialize the PyObject to JSON bytes using [pydantic](https://github.com/pydantic/pydantic).
2. Use the IPC API provided by Tauri to send the bytes to the frontend.
3. [On the frontend, use `TextEncoder` and `JSON.parse` to parse the bytes into a JSON object](https://github.com/WSH032/pytauri/blob/d19b13e6f5f71eef1a7692b7ebd27e2daefc6e38/crates/tauri-plugin-pytauri/guest-js/src/index.ts#L37-L54).

I have implemented `Channel` and `Command` IPC using this method.

However, for `Emitter`, its `emit*` methods require `payload: Serialize`, and I cannot implement `Serialize` for a [PyObject](https://docs.rs/pyo3/latest/pyo3/type.PyObject.html) without affecting performance.

> Implementing `Serialize` for `PyObject` for serialization is far slower than using `pydantic` for serialization.

---

I noticed that `Emitter` just serializes the `payload` to JSON behind the scenes: <https://github.com/tauri-apps/tauri/blob/b8eb28877fe822dbe17999fc8af98ed7d0983679/crates/tauri/src/event/mod.rs#L134-L144>.

So, would Tauri accept adding the following method to `Emitter`:

```rust
fn emit_json(&self, event: &str, payload: String) -> Result<()>;
```

This way, I can use pydantic for serialization on the Python side without incurring the [high checking cost](https://github.com/davidhewitt/pythonize/blob/4491cdb46f35ab3fc1b419beef92171fbef510da/src/de.rs#L111-L150).

### Describe the solution you'd like

Add `emit_json_*` series methods

### Alternatives considered

None

### Additional context

This issue is blocking the release of pytauri version 0.2. If Tauri can accept this feature request, I am willing to submit a PR.


Discussion / hints:

Fix patch:
diff --git a/.changes/emit-str.md b/.changes/emit-str.md
new file mode 100644
index 000000000000..b4b63c923219
--- /dev/null
+++ b/.changes/emit-str.md
@@ -0,0 +1,5 @@
+---
+tauri: 'minor:feat'
+---
+
+Add `emit_str*` methods to `Emitter` trait to allow emitting JSON serialized data directly.
diff --git a/crates/tauri/src/event/mod.rs b/crates/tauri/src/event/mod.rs
index b9b881e23c61..d3bad66d1fbb 100644
--- a/crates/tauri/src/event/mod.rs
+++ b/crates/tauri/src/event/mod.rs
@@ -128,6 +128,15 @@ impl EmitArgs {
       payload: serde_json::to_string(payload)?,
     })
   }
+
+  pub fn new_str(event: EventName<&str>, payload: String) -> crate::Result<Self> {
+    #[cfg(feature = "tracing")]
+    let _span = tracing::debug_span!("window::emit::json").entered();
+    Ok(EmitArgs {
+      event: event.into_owned(),
+      payload,
+    })
+  }
 }
 
 /// An event that was emitted.
diff --git a/crates/tauri/src/lib.rs b/crates/tauri/src/lib.rs
index 7d05f7f14c46..caf81748e45e 100644
--- a/crates/tauri/src/lib.rs
+++ b/crates/tauri/src/lib.rs
@@ -210,6 +210,7 @@ pub use self::utils::TitleBarStyle;
 
 use self::event::EventName;
 pub use self::event::{Event, EventId, EventTarget};
+use self::manager::EmitPayload;
 pub use {
   self::app::{
     App, AppHandle, AssetResolver, Builder, CloseRequestApi, RunEvent, UriSchemeContext,
@@ -940,7 +941,15 @@ pub trait Emitter<R: Runtime>: sealed::ManagerBase<R> {
   /// ```
   fn emit<S: Serialize + Clone>(&self, event: &str, payload: S) -> Result<()> {
     let event = EventName::new(event)?;
-    self.manager().emit(event, &payload)
+    let payload = EmitPayload::Serialize(&payload);
+    self.manager().emit(event, payload)
+  }
+
+  /// Similar to [`Emitter::emit`] but the payload is json serialized.
+  fn emit_str(&self, event: &str, payload: String) -> Result<()> {
+    let event = EventName::new(event)?;
+    let payload = EmitPayload::<()>::Str(payload);
+    self.manager().emit(event, payload)
   }
 
   /// Emits an event to all [targets](EventTarget) matching the given target.
@@ -971,7 +980,18 @@ pub trait Emitter<R: Runtime>: sealed::ManagerBase<R> {
     S: Serialize + Clone,
   {
     let event = EventName::new(event)?;
-    self.manager().emit_to(target, event, &payload)
+    let payload = EmitPayload::Serialize(&payload);
+    self.manager().emit_to(target, event, payload)
+  }
+
+  /// Similar to [`Emitter::emit_to`] but the payload is json serialized.
+  fn emit_str_to<I>(&self, target: I, event: &str, payload: String) -> Result<()>
+  where
+    I: Into<EventTarget>,
+  {
+    let event = EventName::new(event)?;
+    let payload = EmitPayload::<()>::Str(payload);
+    self.manager().emit_to(target, event, payload)
   }
 
   /// Emits an event to all [targets](EventTarget) based on the given filter.
@@ -998,7 +1018,18 @@ pub trait Emitter<R: Runtime>: sealed::ManagerBase<R> {
     F: Fn(&EventTarget) -> bool,
   {
     let event = EventName::new(event)?;
-    self.manager().emit_filter(event, &payload, filter)
+    let payload = EmitPayload::Serialize(&payload);
+    self.manager().emit_filter(event, payload, filter)
+  }
+
+  /// Similar to [`Emitter::emit_filter`] but the payload is json serialized.
+  fn emit_str_filter<F>(&self, event: &str, payload: String, filter: F) -> Result<()>
+  where
+    F: Fn(&EventTarget) -> bool,
+  {
+    let event = EventName::new(event)?;
+    let payload = EmitPayload::<()>::Str(payload);
+    self.manager().emit_filter(event, payload, filter)
   }
 }
 
diff --git a/crates/tauri/src/manager/mod.rs b/crates/tauri/src/manager/mod.rs
index 3919968aec82..9670348cab58 100644
--- a/crates/tauri/src/manager/mod.rs
+++ b/crates/tauri/src/manager/mod.rs
@@ -244,6 +244,11 @@ impl<R: Runtime> fmt::Debug for AppManager<R> {
   }
 }
 
+pub(crate) enum EmitPayload<'a, S: Serialize> {
+  Serialize(&'a S),
+  Str(String),
+}
+
 impl<R: Runtime> AppManager<R> {
   #[allow(clippy::too_many_arguments, clippy::type_complexity)]
   pub(crate) fn with_handlers(
@@ -540,11 +545,14 @@ impl<R: Runtime> AppManager<R> {
   pub(crate) fn emit<S: Serialize>(
     &self,
     event: EventName<&str>,
-    payload: &S,
+    payload: EmitPayload<'_, S>,
   ) -> crate::Result<()> {
     #[cfg(feature = "tracing")]
     let _span = tracing::debug_span!("emit::run").entered();
-    let emit_args = EmitArgs::new(event, &payload)?;
+    let emit_args = match payload {
+      EmitPayload::Serialize(payload) => EmitArgs::new(event, payload)?,
+      EmitPayload::Str(payload) => EmitArgs::new_str(event, payload)?,
+    };
 
     let listeners = self.listeners();
     let webviews = self
@@ -567,7 +575,7 @@ impl<R: Runtime> AppManager<R> {
   pub(crate) fn emit_filter<S, F>(
     &self,
     event: EventName<&str>,
-    payload: S,
+    payload: EmitPayload<'_, S>,
     filter: F,
   ) -> crate::Result<()>
   where
@@ -576,7 +584,10 @@ impl<R: Runtime> AppManager<R> {
   {
     #[cfg(feature = "tracing")]
     let _span = tracing::debug_span!("emit::run").entered();
-    let emit_args = EmitArgs::new(event, &payload)?;
+    let emit_args = match payload {
+      EmitPayload::Serialize(payload) => EmitArgs::new(event, payload)?,
+      EmitPayload::Str(payload) => EmitArgs::new_str(event, payload)?,
+    };
 
     let listeners = self.listeners();
 
@@ -599,7 +610,7 @@ impl<R: Runtime> AppManager<R> {
     &self,
     target: I,
     event: EventName<&str>,
-    payload: &S,
+    payload: EmitPayload<'_, S>,
   ) -> crate::Result<()>
   where
     I: Into<EventTarget>,
diff --git a/crates/tauri/src/manager/webview.rs b/crates/tauri/src/manager/webview.rs
index f6b2798d2bab..8bade86ebde2 100644
--- a/crates/tauri/src/manager/webview.rs
+++ b/crates/tauri/src/manager/webview.rs
@@ -30,7 +30,7 @@ use crate::{
 
 use super::{
   window::{DragDropPayload, DRAG_DROP_EVENT, DRAG_ENTER_EVENT, DRAG_LEAVE_EVENT, DRAG_OVER_EVENT},
-  AppManager,
+  {AppManager, EmitPayload},
 };
 
 // we need to proxy the dev server on mobile because we can't use `localhost`, so we use the local IP address
@@ -631,12 +631,14 @@ impl<R: Runtime> WebviewManager<R> {
         .expect("failed to run on_webview_created hook");
     }
 
-    let _ = webview.manager.emit(
-      crate::EventName::from_str("tauri://webview-created"),
-      &Some(crate::webview::CreatedEvent {
-        label: webview.label().into(),
-      }),
-    );
+    let event = crate::EventName::from_str("tauri://webview-created");
+    let payload = Some(crate::webview::CreatedEvent {
+      label: webview.label().into(),
+    });
+
+    let _ = webview
+      .manager
+      .emit(event, EmitPayload::Serialize(&payload));
 
     webview
   }
@@ -660,12 +662,13 @@ impl<R: Runtime> WebviewManager<R> {
 
 impl<R: Runtime> Webview<R> {
   /// Emits event to [`EventTarget::Window`] and [`EventTarget::WebviewWindow`]
-  fn emit_to_webview<S: Serialize + Clone>(
+  fn emit_to_webview<S: Serialize>(
     &self,
     event: crate::EventName<&str>,
-    payload: S,
+    payload: &S,
   ) -> crate::Result<()> {
     let window_label = self.label();
+    let payload = EmitPayload::Serialize(payload);
     self
       .manager()
       .emit_filter(event, payload, |target| match target {
@@ -685,14 +688,14 @@ fn on_webview_event<R: Runtime>(webview: &Webview<R>, event: &WebviewEvent) -> c
           paths: Some(paths),
           position,
         };
-        webview.emit_to_webview(DRAG_ENTER_EVENT, payload)?
+        webview.emit_to_webview(DRAG_ENTER_EVENT, &payload)?
       }
       DragDropEvent::Over { position } => {
         let payload = DragDropPayload {
           position,
           paths: None,
         };
-        webview.emit_to_webview(DRAG_OVER_EVENT, payload)?
+        webview.emit_to_webview(DRAG_OVER_EVENT, &payload)?
       }
       DragDropEvent::Drop { paths, position } => {
         let scopes = webview.state::<Scopes>();
@@ -707,9 +710,9 @@ fn on_webview_event<R: Runtime>(webview: &Webview<R>, event: &WebviewEvent) -> c
           paths: Some(paths),
           position,
         };
-        webview.emit_to_webview(DRAG_DROP_EVENT, payload)?
+        webview.emit_to_webview(DRAG_DROP_EVENT, &payload)?
       }
-      DragDropEvent::Leave => webview.emit_to_webview(DRAG_LEAVE_EVENT, ())?,
+      DragDropEvent::Leave => webview.emit_to_webview(DRAG_LEAVE_EVENT, &())?,
       _ => unimplemented!(),
     },
   }
diff --git a/crates/tauri/src/manager/window.rs b/crates/tauri/src/manager/window.rs
index 60709f1448ed..f56f40a26c23 100644
--- a/crates/tauri/src/manager/window.rs
+++ b/crates/tauri/src/manager/window.rs
@@ -21,6 +21,8 @@ use crate::{
   EventLoopMessage, EventTarget, Manager, Runtime, Scopes, Window, WindowEvent,
 };
 
+use super::EmitPayload;
+
 const WINDOW_RESIZED_EVENT: EventName<&str> = EventName::from_str("tauri://resize");
 const WINDOW_MOVED_EVENT: EventName<&str> = EventName::from_str("tauri://move");
 const WINDOW_CLOSE_REQUESTED_EVENT: EventName<&str> =
@@ -128,6 +130,7 @@ impl<R: Runtime> Window<R> {
   /// Emits event to [`EventTarget::Window`] and [`EventTarget::WebviewWindow`]
   fn emit_to_window<S: Serialize>(&self, event: EventName<&str>, payload: &S) -> crate::Result<()> {
     let window_label = self.label();
+    let payload = EmitPayload::Serialize(payload);
     self
       .manager()
       .emit_filter(event, payload, |target| match target {
@@ -200,7 +203,7 @@ fn on_window_event<R: Runtime>(window: &Window<R>, event: &WindowEvent) -> crate
           window.manager().emit_to(
             EventTarget::labeled(window.label()),
             DRAG_ENTER_EVENT,
-            &payload,
+            EmitPayload::Serialize(&payload),
           )?
         } else {
           window.emit_to_window(DRAG_ENTER_EVENT, &payload)?
@@ -216,7 +219,7 @@ fn on_window_event<R: Runtime>(window: &Window<R>, event: &WindowEvent) -> crate
           window.manager().emit_to(
             EventTarget::labeled(window.label()),
             DRAG_OVER_EVENT,
-            &payload,
+            EmitPayload::Serialize(&payload),
           )?
         } else {
           window.emit_to_window(DRAG_OVER_EVENT, &payload)?
@@ -241,7 +244,7 @@ fn on_window_event<R: Runtime>(window: &Window<R>, event: &WindowEvent) -> crate
           window.manager().emit_to(
             EventTarget::labeled(window.label()),
             DRAG_DROP_EVENT,
-            &payload,
+            EmitPayload::Serialize(&payload),
           )?
         } else {
           window.emit_to_window(DRAG_DROP_EVENT, &payload)?
@@ -250,9 +253,11 @@ fn on_window_event<R: Runtime>(window: &Window<R>, event: &WindowEvent) -> crate
       DragDropEvent::Leave => {
         if window.is_webview_window() {
           // use underlying manager, otherwise have to recheck EventName
-          window
-            .manager()
-            .emit_to(EventTarget::labeled(window.label()), DRAG_LEAVE_EVENT, &())?
+          window.manager().emit_to(
+            EventTarget::labeled(window.label()),
+            DRAG_LEAVE_EVENT,
+            EmitPayload::Serialize(&()),
+          )?
         } else {
           window.emit_to_window(DRAG_LEAVE_EVENT, &())?
         }
diff --git a/crates/tauri/src/window/mod.rs b/crates/tauri/src/window/mod.rs
index 5479d4e730e4..bde970efb2b0 100644
--- a/crates/tauri/src/window/mod.rs
+++ b/crates/tauri/src/window/mod.rs
@@ -19,7 +19,7 @@ use crate::{
   app::AppHandle,
   event::{Event, EventId, EventTarget},
   ipc::{CommandArg, CommandItem, InvokeError},
-  manager::AppManager,
+  manager::{AppManager, EmitPayload},
   runtime::{
     monitor::Monitor as RuntimeMonitor,
     window::{DetachedWindow, PendingWindow, WindowBuilder as _},
@@ -404,12 +404,11 @@ tauri::Builder::default()
     let window_label = window.label().to_string();
     // run on the main thread to fix a deadlock on webview.eval if the tracing feature is enabled
     let _ = window.run_on_main_thread(move || {
-      let _ = app_manager.emit(
-        crate::EventName::from_str("tauri://window-created"),
-        &Some(crate::webview::CreatedEvent {
-          label: window_label,
-        }),
-      );
+      let event = crate::EventName::from_str("tauri://window-created");
+      let payload = Some(crate::webview::CreatedEvent {
+        label: window_label,
+      });
+      let _ = app_manager.emit(event, EmitPayload::Serialize(&payload));
     });
 
     Ok(window)


Test patch: