wry_bindgen/
wry.rs

1//! Reusable wry-bindgen state for integrating with existing wry applications.
2//!
3//! This module provides [`WryBindgen`], a struct that manages the IPC protocol
4//! between Rust and JavaScript. It can be injected into any wry application
5//! to enable wry-bindgen functionality.
6
7use alloc::boxed::Box;
8use alloc::rc::Rc;
9use alloc::string::{String, ToString};
10use alloc::vec::Vec;
11use base64::Engine;
12use core::cell::RefCell;
13use core::future::poll_fn;
14use core::pin::{Pin, pin};
15use futures_util::FutureExt;
16use std::collections::HashMap;
17use std::sync::Arc;
18
19use http::Response;
20
21use crate::batch::{Runtime, in_runtime};
22use crate::function_registry::FUNCTION_REGISTRY;
23use crate::ipc::{DecodedVariant, IPCMessage, MessageType, decode_data};
24use crate::runtime::{AppEventVariant, IPCSenders, WryBindgenEvent, WryIPC, handle_callbacks};
25
26pub trait ImplWryBindgenResponder {
27    fn respond(self: Box<Self>, response: Response<Vec<u8>>);
28}
29
30/// Responder for wry-bindgen protocol requests.
31pub struct WryBindgenResponder {
32    respond: Box<dyn ImplWryBindgenResponder>,
33}
34
35impl<F> From<F> for WryBindgenResponder
36where
37    F: FnOnce(Response<Vec<u8>>) + 'static,
38{
39    fn from(respond: F) -> Self {
40        struct FnOnceWrapper<F> {
41            f: F,
42        }
43
44        impl<F> ImplWryBindgenResponder for FnOnceWrapper<F>
45        where
46            F: FnOnce(Response<Vec<u8>>) + 'static,
47        {
48            fn respond(self: Box<Self>, response: Response<Vec<u8>>) {
49                (self.f)(response)
50            }
51        }
52
53        Self {
54            respond: Box::new(FnOnceWrapper { f: respond }),
55        }
56    }
57}
58
59impl WryBindgenResponder {
60    pub fn new(f: impl ImplWryBindgenResponder + 'static) -> Self {
61        Self {
62            respond: Box::new(f),
63        }
64    }
65
66    fn respond(self, response: Response<Vec<u8>>) {
67        self.respond.respond(response);
68    }
69}
70
71/// Decode request data from the dioxus-data header.
72fn decode_request_data(request: &http::Request<Vec<u8>>) -> Option<IPCMessage> {
73    if let Some(header_value) = request.headers().get("dioxus-data") {
74        return decode_data(header_value.as_bytes());
75    }
76    None
77}
78
79/// Tracks the loading state of the webview.
80enum WebviewLoadingState {
81    /// Webview is still loading, messages are queued.
82    Pending { queued: Vec<IPCMessage> },
83    /// Webview is loaded and ready.
84    Loaded,
85}
86
87impl Default for WebviewLoadingState {
88    fn default() -> Self {
89        WebviewLoadingState::Pending { queued: Vec::new() }
90    }
91}
92
93/// Shared state for managing async protocol responses.
94struct WebviewState {
95    ongoing_request: Option<WryBindgenResponder>,
96    /// How many responses we are waiting for from JS
97    pending_js_evaluates: usize,
98    /// How many responses JS is waiting for from us
99    pending_rust_evaluates: usize,
100    /// The sender used to send IPC messages to the webview
101    sender: IPCSenders,
102    // The state of the webview. Either loading (with queued messages) or loaded.
103    loading_state: WebviewLoadingState,
104    // A function that evaluates scripts in the webview
105    evaluate_script: Box<dyn FnMut(&str)>,
106}
107
108impl WebviewState {
109    /// Create a new webview state.
110    fn new(sender: IPCSenders, evaluate_script: impl FnMut(&str) + 'static) -> Self {
111        Self {
112            ongoing_request: None,
113            pending_js_evaluates: 0,
114            pending_rust_evaluates: 0,
115            sender,
116            loading_state: WebviewLoadingState::default(),
117            evaluate_script: Box::new(evaluate_script),
118        }
119    }
120
121    fn set_ongoing_request(&mut self, responder: WryBindgenResponder) {
122        if self.ongoing_request.is_some() {
123            panic!(
124                "WARNING: Overwriting existing ongoing_request! Previous request will never be responded to."
125            );
126        }
127        self.ongoing_request = Some(responder);
128    }
129
130    fn take_ongoing_request(&mut self) -> Option<WryBindgenResponder> {
131        self.ongoing_request.take()
132    }
133
134    fn has_pending_request(&self) -> bool {
135        self.ongoing_request.is_some()
136    }
137
138    fn respond_to_request(&mut self, response: IPCMessage) {
139        if let Some(responder) = self.take_ongoing_request() {
140            let body = response.into_data();
141            // Encode as base64 - sync XMLHttpRequest cannot use responseType="arraybuffer"
142            let engine = base64::engine::general_purpose::STANDARD;
143            let body_base64 = engine.encode(&body);
144            responder.respond(
145                http::Response::builder()
146                    .status(200)
147                    .header("Content-Type", "text/plain")
148                    .body(body_base64.into_bytes())
149                    .expect("Failed to build response"),
150            );
151        } else {
152            panic!("WARNING: respond_to_request called but no pending request! Response dropped.");
153        }
154    }
155
156    fn evaluate_script(&mut self, script: &str) {
157        (self.evaluate_script)(script);
158    }
159}
160
161fn unique_id() -> u64 {
162    use core::sync::atomic::{AtomicU64, Ordering};
163    static COUNTER: AtomicU64 = AtomicU64::new(0);
164
165    COUNTER.fetch_add(1, Ordering::Relaxed)
166}
167
168/// A webview future that has a reserved id for use with wry-bindgen.
169///
170/// This struct is `Send` and can be moved to a spawned thread.
171/// Use `into_future()` to get the actual future to poll.
172pub struct PreparedApp {
173    id: u64,
174    future: Box<dyn FnOnce() -> Pin<Box<dyn core::future::Future<Output = ()> + 'static>> + Send>,
175}
176
177impl PreparedApp {
178    /// Get the unique id of this PreparedApp.
179    pub fn id(&self) -> u64 {
180        self.id
181    }
182
183    /// Get the inner future of this PreparedApp.
184    pub fn into_future(self) -> Pin<Box<dyn core::future::Future<Output = ()> + 'static>> {
185        (self.future)()
186    }
187}
188
189/// Factory for creating a protocol handler for a specific webview.
190///
191/// This struct is NOT `Send` because it holds a reference to shared webview state.
192/// Create the protocol handler on the main thread before spawning the app thread.
193pub struct ProtocolHandler {
194    id: u64,
195    webview: Rc<RefCell<HashMap<u64, WebviewState>>>,
196}
197
198impl ProtocolHandler {
199    /// Create a protocol handler closure suitable for `WebViewBuilder::with_asynchronous_custom_protocol`.
200    ///
201    /// The returned closure handles this subset of "{protocol}://" requests:
202    /// - "/__wbg__/initialized" - signals webview loaded
203    /// - "/__wbg__/snippets/{path}" - serves inline JS modules
204    /// - "/__wbg__/init.js" - serves the initialization script
205    /// - "/__wbg__/handler" - main IPC endpoint
206    ///
207    /// # Arguments
208    /// * `protocol` - The protocol scheme (e.g., "wry")
209    /// * `proxy` - Function to send events to the event loop
210    pub fn handle_request<F, R: Into<WryBindgenResponder>>(
211        &self,
212        protocol: &str,
213        proxy: F,
214        request: &http::Request<Vec<u8>>,
215        responder: R,
216    ) -> Option<R>
217    where
218        F: Fn(WryBindgenEvent),
219    {
220        let webviews = &self.webview;
221        let webview_id = self.id;
222
223        let protocol_prefix = format!("{protocol}://index.html");
224        let android_prefix = format!("https://{protocol}.index.html");
225        let windows_prefix = format!("http://{protocol}.index.html");
226
227        let uri = request.uri().to_string();
228        let real_path = uri
229            .strip_prefix(&protocol_prefix)
230            .or_else(|| uri.strip_prefix(&windows_prefix))
231            .or_else(|| uri.strip_prefix(&android_prefix))
232            .unwrap_or(&uri);
233        let real_path = real_path.trim_matches('/');
234
235        let Some(path_without_wbg) = real_path.strip_prefix("__wbg__/") else {
236            // Not a wry-bindgen request - let the caller handle it
237            return Some(responder);
238        };
239
240        // Serve inline_js modules from __wbg__/snippets/
241        if let Some(path_without_snippets) = path_without_wbg.strip_prefix("snippets/") {
242            let responder = responder.into();
243            if let Some(content) = FUNCTION_REGISTRY.get_module(path_without_snippets) {
244                responder.respond(module_response(content));
245                return None;
246            }
247            responder.respond(not_found_response());
248            return None;
249        }
250
251        if path_without_wbg == "init.js" {
252            let responder = responder.into();
253            responder.respond(module_response(&init_script()));
254            return None;
255        }
256
257        if path_without_wbg == "initialized" {
258            proxy(WryBindgenEvent::webview_loaded(webview_id));
259            let responder = responder.into();
260            responder.respond(blank_response());
261            return None;
262        }
263
264        // Js sent us either an Evaluate or Respond message
265        if path_without_wbg == "handler" {
266            let responder = responder.into();
267            let mut webviews = webviews.borrow_mut();
268            let Some(webview_state) = webviews.get_mut(&webview_id) else {
269                responder.respond(error_response());
270                return None;
271            };
272            let Some(msg) = decode_request_data(request) else {
273                responder.respond(error_response());
274                return None;
275            };
276            let msg_type = msg.ty().unwrap();
277            match msg_type {
278                // New call from JS - save responder and wait for the js application thread to respond
279                MessageType::Evaluate => {
280                    webview_state.pending_rust_evaluates += 1;
281                    webview_state.set_ongoing_request(responder);
282                }
283                // Response from JS to a previous Evaluate - decrement pending count and respond accordingly
284                MessageType::Respond => {
285                    webview_state.pending_js_evaluates =
286                        webview_state.pending_js_evaluates.saturating_sub(1);
287                    if webview_state.pending_rust_evaluates > 0
288                        || webview_state.pending_js_evaluates > 0
289                    {
290                        // Still more round-trips expected
291                        webview_state.set_ongoing_request(responder);
292                    } else {
293                        // Conversation is over
294                        responder.respond(blank_response());
295                    }
296                }
297            }
298            webview_state.sender.start_send(msg);
299            return None;
300        }
301
302        Some(responder)
303    }
304}
305
306/// Get the initialization script that must be evaluated in the webview.
307///
308/// This script sets up the JavaScript function registry and IPC infrastructure.
309fn init_script() -> String {
310    /// The script you need to include in the initialization of your webview.
311    const INITIALIZATION_SCRIPT: &str = include_str!("./js/main.js");
312    let collect_functions = FUNCTION_REGISTRY.script();
313    format!("{INITIALIZATION_SCRIPT}\n{collect_functions}")
314}
315
316/// Reusable wry-bindgen state for integrating with existing wry applications.
317///
318/// This struct manages the IPC protocol between Rust and JavaScript,
319/// handling message queuing, async responses, and JS function registration.
320///
321/// # Example
322///
323/// ```ignore
324/// let wry_bindgen = WryBindgen::new(move |event| { proxy.send_event(event).ok(); });
325///
326/// let (prepared_app, protocol_factory) = wry_bindgen.in_runtime(|| async { my_app().await });
327/// let protocol_handler = protocol_factory.create("wry", move |event| {
328///     proxy.send_event(event).ok();
329/// });
330///
331/// std::thread::spawn(move || {
332///     // Run prepared_app.into_future() in a tokio runtime
333/// });
334///
335/// let webview = WebViewBuilder::new()
336///     .with_asynchronous_custom_protocol("wry".into(), move |_, req, resp| {
337///         protocol_handler(&req, resp);
338///     })
339///     .with_url("wry://index")
340///     .build(&window)?;
341/// ```
342pub struct WryBindgen {
343    event_loop_proxy: Arc<dyn Fn(WryBindgenEvent) + Send + Sync>,
344    // State that is unique to each webview
345    webview: Rc<RefCell<HashMap<u64, WebviewState>>>,
346}
347
348impl WryBindgen {
349    /// Create a new WryBindgen instance.
350    pub fn new(event_loop_proxy: impl Fn(WryBindgenEvent) + Send + Sync + 'static) -> Self {
351        Self {
352            event_loop_proxy: Arc::new(event_loop_proxy),
353            webview: Rc::new(RefCell::new(HashMap::new())),
354        }
355    }
356
357    /// Start the application thread with the given event loop proxy.
358    ///
359    /// Returns a tuple of:
360    /// - `PreparedApp`: The app future, which is `Send` and can be moved to a spawned thread
361    /// - `ProtocolHandlerFactory`: Factory for creating the protocol handler (not `Send`, use on main thread)
362    pub fn app_builder<'a>(&'a self) -> AppBuilder<'a> {
363        let event_loop_proxy = self.event_loop_proxy.clone();
364        let webview_id = unique_id();
365        let (ipc, senders) = WryIPC::new(event_loop_proxy);
366        self.webview.borrow_mut().insert(
367            webview_id,
368            WebviewState::new(senders, |_| {
369                unreachable!("evaluate_script will only be used after spawning the app")
370            }),
371        );
372
373        AppBuilder {
374            webview_id,
375            bindgen: self,
376            ipc,
377        }
378    }
379
380    /// Handle a user event from the event loop.
381    ///
382    /// This should be called from your ApplicationHandler::user_event implementation.
383    /// Returns `Some(exit_code)` if the application should shut down with that exit code.
384    ///
385    /// # Arguments
386    /// * `event` - The AppEvent to handle
387    /// * `webview` - Reference to the webview for script evaluation
388    pub fn handle_user_event(&self, event: WryBindgenEvent) {
389        let id = event.id();
390        match event.into_variant() {
391            // The rust thread sent us an IPCMessage to send to JS
392            AppEventVariant::Ipc(ipc_msg) => self.handle_ipc_message(id, ipc_msg),
393            AppEventVariant::WebviewLoaded => {
394                let mut state = self.webview.borrow_mut();
395                let Some(webview_state) = state.get_mut(&id) else {
396                    return;
397                };
398                if let WebviewLoadingState::Pending { queued } = std::mem::replace(
399                    &mut webview_state.loading_state,
400                    WebviewLoadingState::Loaded,
401                ) {
402                    for msg in queued {
403                        self.immediately_handle_ipc_message(webview_state, msg);
404                    }
405                }
406            }
407        }
408    }
409
410    fn handle_ipc_message(&self, id: u64, ipc_msg: IPCMessage) {
411        let mut state = self.webview.borrow_mut();
412        let Some(webview_state) = state.get_mut(&id) else {
413            return;
414        };
415        if let WebviewLoadingState::Pending { queued } = &mut webview_state.loading_state {
416            queued.push(ipc_msg);
417            return;
418        }
419
420        self.immediately_handle_ipc_message(webview_state, ipc_msg)
421    }
422
423    fn immediately_handle_ipc_message(
424        &self,
425        webview_state: &mut WebviewState,
426        ipc_msg: IPCMessage,
427    ) {
428        let ty = ipc_msg.ty().unwrap();
429        match ty {
430            // Rust wants to evaluate something in js
431            MessageType::Evaluate => {
432                webview_state.pending_js_evaluates += 1;
433            }
434            // Rust is responding to a previous js evaluate
435            MessageType::Respond => {
436                webview_state.pending_rust_evaluates =
437                    webview_state.pending_rust_evaluates.saturating_sub(1);
438            }
439        }
440
441        // If there is an ongoing request, respond to immediately
442        if webview_state.has_pending_request() {
443            webview_state.respond_to_request(ipc_msg);
444            return;
445        }
446
447        // Otherwise call into js through evaluate_script
448        let decoded = ipc_msg.decoded().unwrap();
449
450        if let DecodedVariant::Evaluate { .. } = decoded {
451            // Encode the binary data as base64 and pass to JS
452            // JS will iterate over operations in the buffer
453            let engine = base64::engine::general_purpose::STANDARD;
454            let data_base64 = engine.encode(ipc_msg.data());
455            let code = format!("window.evaluate_from_rust_binary(\"{data_base64}\")");
456            webview_state.evaluate_script(&code);
457        }
458    }
459}
460
461/// A builder for the application future and protocol handler.
462pub struct AppBuilder<'a> {
463    webview_id: u64,
464    bindgen: &'a WryBindgen,
465    ipc: WryIPC,
466}
467
468impl<'a> AppBuilder<'a> {
469    /// Get the protocol handler for this webview.
470    pub fn protocol_handler(&self) -> ProtocolHandler {
471        ProtocolHandler {
472            id: self.webview_id,
473            webview: self.bindgen.webview.clone(),
474        }
475    }
476
477    /// Consume the builder and get the prepared app future.
478    pub fn build<F>(
479        self,
480        app: impl FnOnce() -> F + Send + 'static,
481        evaluate_script: impl FnMut(&str) + 'static,
482    ) -> PreparedApp
483    where
484        F: core::future::Future<Output = ()> + 'static,
485    {
486        // First set up the evaluate_script function in the webview state
487        {
488            let mut webviews = self.bindgen.webview.borrow_mut();
489            let webview_state = webviews
490                .get_mut(&self.webview_id)
491                .expect("The webview state was created in WryBindgen::spawner");
492            webview_state.evaluate_script = Box::new(evaluate_script);
493        }
494
495        let start_future = move || {
496            let run_app_in_runtime = async move {
497                let run_app = app();
498                let wait_for_events = handle_callbacks();
499
500                futures_util::select! {
501                    _ = run_app.fuse() => {},
502                    _ = wait_for_events.fuse() => {},
503                }
504            };
505
506            let runtime = Runtime::new(self.ipc, self.webview_id);
507            let mut maybe_runtime = Some(runtime);
508            let poll_in_runtime = async move {
509                let mut run_app_in_runtime = pin!(run_app_in_runtime);
510                poll_fn(move |ctx| {
511                    let (new_runtime, poll_result) =
512                        in_runtime(maybe_runtime.take().unwrap(), || {
513                            run_app_in_runtime.as_mut().poll(ctx)
514                        });
515                    maybe_runtime = Some(new_runtime);
516                    poll_result
517                })
518                .await
519            };
520
521            Box::pin(poll_in_runtime) as Pin<Box<dyn Future<Output = ()> + 'static>>
522        };
523
524        PreparedApp {
525            id: self.webview_id,
526            future: Box::new(start_future),
527        }
528    }
529}
530
531/// Create a blank HTTP response.
532pub fn blank_response() -> http::Response<Vec<u8>> {
533    http::Response::builder()
534        .status(200)
535        .body(vec![])
536        .expect("Failed to build blank response")
537}
538
539/// Create an error HTTP response.
540pub fn error_response() -> http::Response<Vec<u8>> {
541    http::Response::builder()
542        .status(400)
543        .body(vec![])
544        .expect("Failed to build error response")
545}
546
547/// Create a JavaScript module HTTP response.
548pub fn module_response(content: &str) -> http::Response<Vec<u8>> {
549    http::Response::builder()
550        .status(200)
551        .header("Content-Type", "application/javascript")
552        .header("access-control-allow-origin", "*")
553        .body(content.as_bytes().to_vec())
554        .expect("Failed to build module response")
555}
556
557/// Create a not found HTTP response.
558pub fn not_found_response() -> http::Response<Vec<u8>> {
559    http::Response::builder()
560        .status(404)
561        .body(b"Not Found".to_vec())
562        .expect("Failed to build not found response")
563}