wry_bindgen/
runtime.rs

1//! Runtime setup and event loop management.
2//!
3//! This module handles the connection between the Rust runtime and the
4//! JavaScript environment via winit's event loop.
5
6use core::pin::Pin;
7use std::sync::Arc;
8
9use alloc::boxed::Box;
10use async_channel::{Receiver, Sender};
11use futures_util::{FutureExt, StreamExt};
12use spin::RwLock;
13
14use crate::BinaryDecode;
15use crate::batch::with_runtime;
16use crate::function::{CALL_EXPORT_FN_ID, DROP_NATIVE_REF_FN_ID, RustCallback};
17use crate::ipc::MessageType;
18use crate::ipc::{DecodedData, DecodedVariant, IPCMessage};
19use crate::object_store::ObjectHandle;
20use crate::object_store::remove_object;
21
22/// Application-level events that can be sent through the event loop.
23///
24/// This enum wraps both IPC messages from JavaScript and control messages
25/// from the application (like shutdown requests).
26#[derive(Debug, Clone)]
27pub struct WryBindgenEvent {
28    id: u64,
29    event: AppEventVariant,
30}
31
32impl WryBindgenEvent {
33    /// Get the id of the event
34    pub(crate) fn id(&self) -> u64 {
35        self.id
36    }
37
38    /// Create a new IPC event.
39    pub(crate) fn ipc(id: u64, msg: IPCMessage) -> Self {
40        Self {
41            id,
42            event: AppEventVariant::Ipc(msg),
43        }
44    }
45
46    /// Create a new webview loaded event.
47    pub(crate) fn webview_loaded(id: u64) -> Self {
48        Self {
49            id,
50            event: AppEventVariant::WebviewLoaded,
51        }
52    }
53
54    /// Consume the event and return the inner variant.
55    pub(crate) fn into_variant(self) -> AppEventVariant {
56        self.event
57    }
58}
59
60#[derive(Debug, Clone)]
61pub(crate) enum AppEventVariant {
62    /// An IPC message from JavaScript
63    Ipc(IPCMessage),
64    /// The webview has finished loading
65    WebviewLoaded,
66}
67
68#[derive(Clone)]
69pub(crate) struct IPCSenders {
70    eval_sender: Sender<IPCMessage>,
71    respond_sender: futures_channel::mpsc::UnboundedSender<IPCMessage>,
72}
73
74impl IPCSenders {
75    pub(crate) fn start_send(&self, msg: IPCMessage) {
76        match msg.ty().unwrap() {
77            MessageType::Evaluate => {
78                self.eval_sender
79                    .try_send(msg)
80                    .expect("Failed to send evaluate message");
81            }
82            MessageType::Respond => {
83                self.respond_sender
84                    .unbounded_send(msg)
85                    .expect("Failed to send respond message");
86            }
87        }
88    }
89}
90
91struct IPCReceivers {
92    eval_receiver: Pin<Box<Receiver<IPCMessage>>>,
93    respond_receiver: futures_channel::mpsc::UnboundedReceiver<IPCMessage>,
94}
95
96impl IPCReceivers {
97    pub fn recv_blocking(&mut self) -> IPCMessage {
98        pollster::block_on(async {
99            let Self {
100                eval_receiver,
101                respond_receiver,
102            } = self;
103            futures_util::select_biased! {
104                // We need to always poll the respond receiver first. If the response is ready, quit immediately
105                // before running any more callbacks
106                respond_msg = respond_receiver.next().fuse() => {
107                    respond_msg.expect("Failed to receive respond message")
108                },
109                eval_msg = eval_receiver.next().fuse() => {
110                    eval_msg.expect("Failed to receive evaluate message")
111                },
112            }
113        })
114    }
115}
116
117/// The runtime environment for communicating with JavaScript.
118///
119/// This struct holds the event loop proxy for sending messages to the
120/// WebView and manages queued Rust calls.
121pub(crate) struct WryIPC {
122    pub(crate) proxy: Arc<dyn Fn(WryBindgenEvent) + Send + Sync>,
123    receivers: RwLock<IPCReceivers>,
124}
125
126impl WryIPC {
127    /// Create a new runtime with the given event loop proxy.
128    pub(crate) fn new(proxy: Arc<dyn Fn(WryBindgenEvent) + Send + Sync>) -> (Self, IPCSenders) {
129        let (eval_sender, eval_receiver) = async_channel::unbounded();
130        let (respond_sender, respond_receiver) = futures_channel::mpsc::unbounded();
131        let senders = IPCSenders {
132            eval_sender,
133            respond_sender,
134        };
135        let receivers = RwLock::new(IPCReceivers {
136            eval_receiver: Box::pin(eval_receiver),
137            respond_receiver,
138        });
139        let ipc = Self { proxy, receivers };
140        (ipc, senders)
141    }
142
143    /// Send a response back to JavaScript.
144    pub(crate) fn js_response(&self, id: u64, responder: IPCMessage) {
145        (self.proxy)(WryBindgenEvent::ipc(id, responder));
146    }
147}
148
149pub(crate) fn progress_js_with<O>(
150    with_respond: impl for<'a> Fn(DecodedData<'a>) -> O,
151) -> Option<O> {
152    let response = with_runtime(|runtime| runtime.ipc().receivers.write().recv_blocking());
153
154    let decoder = response.decoded().expect("Failed to decode response");
155    match decoder {
156        DecodedVariant::Respond { data } => Some(with_respond(data)),
157        DecodedVariant::Evaluate { mut data } => {
158            handle_rust_callback(&mut data);
159            None
160        }
161    }
162}
163
164pub async fn handle_callbacks() {
165    let receiver = with_runtime(|runtime| runtime.ipc().receivers.read().eval_receiver.clone());
166
167    while let Ok(response) = receiver.recv().await {
168        let decoder = response.decoded().expect("Failed to decode response");
169        match decoder {
170            DecodedVariant::Respond { .. } => unreachable!(),
171            DecodedVariant::Evaluate { mut data } => {
172                handle_rust_callback(&mut data);
173            }
174        }
175    }
176}
177
178/// Handle a Rust callback invocation from JavaScript.
179fn handle_rust_callback(data: &mut DecodedData) {
180    let fn_id = data.take_u32().expect("Failed to read fn_id");
181    let response = match fn_id {
182        // Call a registered Rust callback
183        0 => {
184            let key = data.take_u32().unwrap();
185
186            // Clone the Rc while briefly borrowing the batch state, then release the borrow.
187            // This allows nested callbacks to access the object store during our callback execution.
188            let callback = with_runtime(|state| {
189                let rust_callback = state.get_object::<RustCallback>(key);
190
191                rust_callback.clone_rc()
192            });
193
194            // Push a borrow frame before calling the callback - nested calls won't clear our borrowed refs
195            with_runtime(|state| state.push_borrow_frame());
196
197            // Call through the cloned Rc (uniform Fn interface)
198            let response = IPCMessage::new_respond(|encoder| {
199                (callback)(data, encoder);
200            });
201
202            // Pop the borrow frame after the callback completes
203            with_runtime(|state| state.pop_borrow_frame());
204
205            response
206        }
207        // Drop a native Rust object when JS GC'd the wrapper
208        DROP_NATIVE_REF_FN_ID => {
209            let key = ObjectHandle::decode(data).expect("Failed to decode object handle");
210
211            // Remove the object from the thread-local encoder
212            remove_object::<RustCallback>(key);
213
214            // Send empty response
215            IPCMessage::new_respond(|_| {})
216        }
217        // Call an exported Rust struct method
218        CALL_EXPORT_FN_ID => {
219            // Read the export name
220            let export_name: alloc::string::String =
221                crate::encode::BinaryDecode::decode(data).expect("Failed to decode export name");
222
223            // Find the export handler
224            let export = crate::inventory::iter::<crate::JsExportSpec>()
225                .find(|e| e.name == export_name)
226                .unwrap_or_else(|| panic!("Unknown export: {export_name}"));
227
228            // Call the handler
229            let result = (export.handler)(data);
230
231            assert!(data.is_empty(), "Extra data remaining after export call");
232
233            // Send response
234            match result {
235                Ok(encoded) => IPCMessage::new_respond(|encoder| {
236                    encoder.extend(&encoded);
237                }),
238                Err(err) => {
239                    panic!("Export call failed: {err}");
240                }
241            }
242        }
243        _ => todo!(),
244    };
245    with_runtime(|runtime| runtime.ipc().js_response(runtime.webview_id(), response));
246}